Skip to content

Commit 9482bf3

Browse files
thjarvinhaphut
andauthored
Healthcheck uses many threads (#371)
* AB#57775: HealthServer starts a new thread for each health checker * AB#57775: Update version to 2.0.3-RC3 * AB#57775: Add instance variable healthCheckExecutor * AB#57775: ExecutorCompletionService is used to get the failure faster * refactor: Add final for attributes * refactor: Use StandardCharsets for UTF-8 * style: Remove extra whitespace * refactor: Handle two exceptions explicitly * test: Add a start for unit tests of HealthServer * Update version to 2.0.3-RC5 --------- Co-authored-by: haphut <[email protected]>
1 parent 852b618 commit 9482bf3

File tree

3 files changed

+175
-10
lines changed

3 files changed

+175
-10
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<modelVersion>4.0.0</modelVersion>
33
<groupId>fi.hsl</groupId>
44
<artifactId>transitdata-common</artifactId>
5-
<version>2.0.3-RC4</version>
5+
<version>2.0.3-RC5</version>
66
<packaging>jar</packaging>
77
<name>Common utilities for Transitdata projects</name>
88
<properties>

src/main/java/fi/hsl/common/health/HealthServer.java

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
import java.io.IOException;
1111
import java.io.OutputStream;
1212
import java.net.InetSocketAddress;
13+
import java.nio.charset.StandardCharsets;
1314
import java.util.ArrayList;
1415
import java.util.List;
15-
import java.util.concurrent.Executors;
16+
import java.util.concurrent.*;
1617
import java.util.function.BooleanSupplier;
1718

1819
public class HealthServer {
@@ -21,7 +22,9 @@ public class HealthServer {
2122
public final int port;
2223
public final String endpoint;
2324
public final HttpServer httpServer;
24-
private List<BooleanSupplier> checks = new ArrayList<>();
25+
private final ExecutorService healthCheckExecutor =
26+
Executors.newCachedThreadPool();
27+
private final List<BooleanSupplier> checks = new CopyOnWriteArrayList<>();
2528

2629
public HealthServer(final int port, @NotNull final String endpoint) throws IOException {
2730
this.port = port;
@@ -30,14 +33,14 @@ public HealthServer(final int port, @NotNull final String endpoint) throws IOExc
3033
httpServer = HttpServer.create(new InetSocketAddress(port), 0);
3134
httpServer.createContext("/", createDefaultHandler());
3235
httpServer.createContext(endpoint, createHandler());
33-
httpServer.setExecutor(Executors.newSingleThreadExecutor());
36+
httpServer.setExecutor(healthCheckExecutor);
3437
httpServer.start();
3538
log.info("HealthServer started");
3639
}
3740

3841
private void writeResponse(@NotNull final HttpExchange httpExchange, @NotNull final int responseCode, @NotNull final String responseBody) throws IOException {
39-
final byte[] response = responseBody.getBytes("UTF-8");
40-
httpExchange.getResponseHeaders().add("Content-Type", "text/plain; charset=UTF-8");
42+
final byte[] response = responseBody.getBytes(StandardCharsets.UTF_8);
43+
httpExchange.getResponseHeaders().add("Content-Type", "text/plain; charset=" + StandardCharsets.UTF_8.name());
4144
httpExchange.sendResponseHeaders(responseCode, response.length);
4245
final OutputStream out = httpExchange.getResponseBody();
4346
out.write(response);
@@ -91,16 +94,67 @@ public void clearChecks() {
9194
}
9295

9396
public boolean checkHealth() {
94-
boolean isHealthy = true;
95-
for (final BooleanSupplier check : checks) {
96-
isHealthy &= check.getAsBoolean();
97+
try {
98+
CompletionService<Boolean> executorCompletionService
99+
= new ExecutorCompletionService<>(healthCheckExecutor);
100+
int n = checks.size();
101+
List<Future<Boolean>> futures = new ArrayList<>(n);
102+
try {
103+
for (BooleanSupplier check : checks) {
104+
futures.add(executorCompletionService.submit(checkToCallable(check)));
105+
}
106+
for (int i = 0; i < n; ++i) {
107+
try {
108+
Boolean result = executorCompletionService.take().get();
109+
if (result == null || !result) {
110+
return false; // Return false immediately if any check fails
111+
}
112+
} catch (ExecutionException e) {
113+
log.error("A health check task execution failed. Marking unhealthy.", e.getCause() != null ? e.getCause() : e);
114+
return false;
115+
} catch (InterruptedException e) {
116+
log.error("Health check interrupted. Marking unhealthy.", e);
117+
Thread.currentThread().interrupt();
118+
return false;
119+
}
120+
}
121+
} finally {
122+
for (Future<Boolean> f : futures) {
123+
f.cancel(true);
124+
}
125+
}
126+
return true; // Return true only if all checks pass
127+
} catch (Exception e) {
128+
log.error("Exception during health checks", e);
129+
return false;
97130
}
98-
return isHealthy;
99131
}
100132

101133
public void close() {
102134
if (httpServer != null) {
103135
httpServer.stop(0);
104136
}
137+
if (healthCheckExecutor != null) {
138+
healthCheckExecutor.shutdown();
139+
try {
140+
if (!healthCheckExecutor.awaitTermination(3, TimeUnit.SECONDS)) {
141+
healthCheckExecutor.shutdownNow();
142+
}
143+
} catch (InterruptedException ie) {
144+
healthCheckExecutor.shutdownNow();
145+
Thread.currentThread().interrupt();
146+
}
147+
}
148+
}
149+
150+
private static Callable<Boolean> checkToCallable(BooleanSupplier check) {
151+
return () -> {
152+
try {
153+
return check.getAsBoolean();
154+
} catch (Exception e) {
155+
log.error("Exception during health check", e);
156+
return false;
157+
}
158+
};
105159
}
106160
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package fi.hsl.common.health;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertFalse;
5+
import static org.junit.Assert.assertTrue;
6+
7+
import java.io.IOException;
8+
import java.util.concurrent.atomic.AtomicInteger;
9+
import java.util.function.BooleanSupplier;
10+
import org.junit.After;
11+
import org.junit.Before;
12+
import org.junit.Test;
13+
14+
public class HealthServerTest {
15+
16+
private HealthServer healthServer;
17+
private final int testPort = 0;
18+
private final String testEndpoint = "/healthz";
19+
20+
private static class CountingWrapper implements BooleanSupplier {
21+
22+
private final BooleanSupplier delegate;
23+
private final AtomicInteger callCount = new AtomicInteger(0);
24+
25+
public CountingWrapper(BooleanSupplier delegate) {
26+
this.delegate = delegate;
27+
}
28+
29+
@Override
30+
public boolean getAsBoolean() {
31+
callCount.incrementAndGet();
32+
return delegate.getAsBoolean();
33+
}
34+
35+
public int getCallCount() {
36+
return callCount.get();
37+
}
38+
}
39+
40+
@Before
41+
public void setUp() throws IOException {
42+
healthServer = new HealthServer(testPort, testEndpoint);
43+
}
44+
45+
@After
46+
public void tearDown() {
47+
if (healthServer != null) {
48+
healthServer.close();
49+
}
50+
}
51+
52+
@Test
53+
public void singleUnhealthyCheckReturnsFalse() {
54+
CountingWrapper unhealthyCheck = new CountingWrapper(() -> false);
55+
healthServer.addCheck(unhealthyCheck);
56+
boolean healthStatus = healthServer.checkHealth();
57+
assertFalse(
58+
"Health status should be false when one check is unhealthy.",
59+
healthStatus
60+
);
61+
assertEquals(
62+
"UnhealthyCheck should have been called once.",
63+
1,
64+
unhealthyCheck.getCallCount()
65+
);
66+
}
67+
68+
@Test
69+
public void allHealthyChecksReturnsTrue() {
70+
CountingWrapper healthyCheck1 = new CountingWrapper(() -> true);
71+
CountingWrapper healthyCheck2 = new CountingWrapper(() -> true);
72+
healthServer.addCheck(healthyCheck1);
73+
healthServer.addCheck(healthyCheck2);
74+
boolean healthStatus = healthServer.checkHealth();
75+
assertTrue(
76+
"Health status should be true when all checks are healthy.",
77+
healthStatus
78+
);
79+
assertEquals(
80+
"HealthyCheck1 should have been called once.",
81+
1,
82+
healthyCheck1.getCallCount()
83+
);
84+
assertEquals(
85+
"HealthyCheck2 should have been called once.",
86+
1,
87+
healthyCheck2.getCallCount()
88+
);
89+
}
90+
91+
@Test
92+
public void testCheckHealth_CheckThrowsException_ReturnsFalse() {
93+
final RuntimeException testException = new RuntimeException(
94+
"Simulated check failure"
95+
);
96+
CountingWrapper exceptionThrowingCheck = new CountingWrapper(() -> {
97+
throw testException;
98+
});
99+
healthServer.addCheck(exceptionThrowingCheck);
100+
boolean healthStatus = healthServer.checkHealth();
101+
assertFalse(
102+
"Health status should be false when a check throws an exception.",
103+
healthStatus
104+
);
105+
assertEquals(
106+
"ExceptionThrowingCheck should have been called once.",
107+
1,
108+
exceptionThrowingCheck.getCallCount()
109+
);
110+
}
111+
}

0 commit comments

Comments
 (0)