From 069ee8fe608b453be4c03cc3d3c68951af6d4622 Mon Sep 17 00:00:00 2001
From: Lukasz Cwik <lcwik@apache.org>
Date: Wed, 3 Jan 2024 08:43:09 -0800
Subject: [PATCH] Fix data race in test code

---
 .../server/types/health_checker_test.go       | 26 ++++++----
 .../server/types/health_monitor_test.go       | 50 ++++++++++---------
 2 files changed, 42 insertions(+), 34 deletions(-)

diff --git a/protocol/daemons/server/types/health_checker_test.go b/protocol/daemons/server/types/health_checker_test.go
index eda5f12a1a..517894f1fa 100644
--- a/protocol/daemons/server/types/health_checker_test.go
+++ b/protocol/daemons/server/types/health_checker_test.go
@@ -77,8 +77,8 @@ func TestHealthChecker(t *testing.T) {
 	for name, test := range tests {
 		t.Run(name, func(t *testing.T) {
 			// Setup.
-			// Set up callback to track error passed to it.
-			callback, callbackError := callbackWithErrorPointer()
+			// Set up channel to track errors passed to it from the callback.
+			errs := make(chan (error), 100)
 
 			// Set up health checkable service.
 			checkable := &mocks.HealthCheckable{}
@@ -95,18 +95,15 @@ func TestHealthChecker(t *testing.T) {
 			healthChecker := types.StartNewHealthChecker(
 				checkable,
 				TestLargeDuration, // set to a >> value so that poll is never auto-triggered by the timer
-				callback,
+				func(err error) {
+					errs <- err
+				},
 				timeProvider,
 				TestMaximumUnhealthyDuration,
 				types.DaemonStartupGracePeriod,
 				log.NewNopLogger(),
 			)
 
-			// Cleanup.
-			defer func() {
-				healthChecker.Stop()
-			}()
-
 			// Act - simulate the health checker polling for updates.
 			for i := 0; i < len(test.healthCheckResponses); i++ {
 				healthChecker.Poll()
@@ -117,11 +114,20 @@ func TestHealthChecker(t *testing.T) {
 			checkable.AssertExpectations(t)
 			timeProvider.AssertExpectations(t)
 
+			var err error
+			select {
+			case err = <-errs:
+			case <-time.After(1 * time.Second):
+			}
+
+			// Cleanup.
+			healthChecker.Stop()
+
 			// Validate the callback was called with the expected error.
 			if test.expectedUnhealthy == nil {
-				require.NoError(t, *callbackError)
+				require.NoError(t, err)
 			} else {
-				require.ErrorContains(t, *callbackError, test.expectedUnhealthy.Error())
+				require.ErrorContains(t, err, test.expectedUnhealthy.Error())
 			}
 		})
 	}
diff --git a/protocol/daemons/server/types/health_monitor_test.go b/protocol/daemons/server/types/health_monitor_test.go
index 8ba07f748b..175c359dd8 100644
--- a/protocol/daemons/server/types/health_monitor_test.go
+++ b/protocol/daemons/server/types/health_monitor_test.go
@@ -51,16 +51,6 @@ func mockFailingHealthCheckerWithError(name string, err error) *mocks.HealthChec
 	return hc
 }
 
-// callbackWithErrorPointer returns a callback function and an error pointer that tracks the error passed to the
-// callback. This can be used to validate that a service was considered unhealthy for the maximum allowable duration.
-func callbackWithErrorPointer() (func(error), *error) {
-	var callbackError error
-	callback := func(err error) {
-		callbackError = err
-	}
-	return callback, &callbackError
-}
-
 // The following tests may still intermittently fail or report false positives on an overloaded system as they rely
 // on callbacks to execute before the termination of the `time.Sleep` call, which is not guaranteed.
 func TestRegisterService_Healthy(t *testing.T) {
@@ -100,29 +90,32 @@ func TestRegisterServiceWithCallback_Mixed(t *testing.T) {
 			// Setup.
 			ufm, logger := createTestMonitor()
 			hc := mockFailingHealthCheckerWithError("test-service", test.healthCheckResponse)
-			callback, callbackError := callbackWithErrorPointer()
+			errs := make(chan (error), 100)
 
 			// Act.
 			err := ufm.RegisterServiceWithCallback(
 				hc,
 				50*time.Millisecond,
-				callback,
+				func(err error) {
+					errs <- err
+				},
 			)
 			require.NoError(t, err)
 
-			// Cleanup.
-			defer func() {
-				ufm.Stop()
-			}()
-
 			// Give the monitor time to poll the health checkable service. Polls occur once every 10ms.
-			time.Sleep(100 * time.Millisecond)
+			select {
+			case err = <-errs:
+			case <-time.After(1 * time.Second):
+			}
+
+			// Cleanup.
+			ufm.Stop()
 
 			// Assert: no calls to the logger were made.
 			mock.AssertExpectationsForObjects(t, hc, logger)
 
 			// Assert: the callback was called or not called as expected.
-			require.Equal(t, test.healthCheckResponse, *callbackError)
+			require.Equal(t, test.healthCheckResponse, err)
 		})
 	}
 }
@@ -172,17 +165,26 @@ func TestRegisterServiceWithCallback_DoubleRegistrationFails(t *testing.T) {
 	hc := mockFailingHealthCheckerWithError("test-service", TestError1)
 	hc2 := mockFailingHealthCheckerWithError("test-service", TestError2)
 
-	callback, callbackError := callbackWithErrorPointer()
+	errs := make(chan (error), 100)
 
-	err := ufm.RegisterServiceWithCallback(hc, 50*time.Millisecond, callback)
+	err := ufm.RegisterServiceWithCallback(hc, 50*time.Millisecond, func(err error) {
+		errs <- err
+	})
 	require.NoError(t, err)
 
 	// Register a service with the same name. This registration should fail.
-	err = ufm.RegisterServiceWithCallback(hc2, 50*time.Millisecond, callback)
+	err = ufm.RegisterServiceWithCallback(hc2, 50*time.Millisecond, func(err error) {
+		errs <- err
+	})
 	require.ErrorContains(t, err, "service already registered")
 
 	// Expect that the first service is still operating and will produce a callback after a sustained unhealthy period.
-	time.Sleep(100 * time.Millisecond)
+	select {
+	case err = <-errs:
+	case <-time.After(1 * time.Second):
+		t.Fatalf("Failed to receive callback before timeout")
+	}
+
 	ufm.Stop()
 
 	// Assert no calls to the logger were made.
@@ -190,7 +192,7 @@ func TestRegisterServiceWithCallback_DoubleRegistrationFails(t *testing.T) {
 	hc2.AssertNotCalled(t, "HealthCheck")
 
 	// Assert the callback was called with the expected error.
-	require.Equal(t, TestError1, *callbackError)
+	require.Equal(t, TestError1, err)
 }
 
 // Create a struct that implements HealthCheckable and Stoppable to check that the monitor stops the service.