chore: harden lifecycle, health checks and CI
CI / Backend tests (Maven / Java 21) (push) Failing after 7m9s

- add PR trigger and Maven cache in CI

- centralize shutdown cleanup in Main

- make HealthMonitor failure counter thread-safe

- fix UTF-8 response length handling in health endpoint

- improve Influx username null-safety

- reduce expected modbus read failures from ERROR to WARN
This commit is contained in:
root committed 2026-02-12 22:50:37 +01:00
1 parent e345063720
commit dd739e16c0
7 files changed
+36 -36

No files matched your search

+5 -5
View File
@@ -4,6 +4,9 @@ on:
push:
branches:
- "**"
pull_request:
branches:
- "**"
jobs:
backend:
@@ -23,10 +26,7 @@ jobs:
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '21'
- name: Set up Maven
uses: stCarolas/setup-maven@v5
with:
maven-version: 3.9.6
java-version: "21"
cache: maven
- name: Run Maven tests (parallel)
run: mvn -B -T 1C test
+13 -4
View File
@@ -41,12 +41,14 @@ public class Main {
final var useMock = config.getUseMock() != null && config.getUseMock();
ModbusService service;
ModbusConnection modbusConnection = null;
if (useMock) {
service = new ModbusService(new home.automation.modbus.MockRepository(registryMappings));
} else {
modbusConnection = new ModbusConnection(jsonFileBasedConfiguration);
service = new ModbusService(
new ModbusRtuTcpRepository(
new ModbusConnection(jsonFileBasedConfiguration),
modbusConnection,
registryMappings));
}
@@ -57,12 +59,21 @@ public class Main {
healthCheckServer.start();
// Shutdown hook for graceful cleanup
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
ScheduledExecutorService healthScheduler = Executors.newSingleThreadScheduledExecutor();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
LOGGER.info("Shutdown signal received, cleaning up resources...");
try {
scheduler.shutdownNow();
healthScheduler.shutdownNow();
healthCheckServer.close();
if (modbusConnection != null) {
modbusConnection.close();
}
connection.close();
} catch (Exception e) {
LOGGER.error("Error closing health check server", e);
LOGGER.error("Error during shutdown cleanup", e);
}
}));
@@ -70,7 +81,6 @@ public class Main {
? 300
: config.getInfluxSendIntervalSeconds();
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(
new InfluxMetricModbusSender(connection.getConnection(), pollingService),
0,
@@ -78,7 +88,6 @@ public class Main {
TimeUnit.SECONDS);
// Monitor health and trigger restart if needed
ScheduledExecutorService healthScheduler = Executors.newSingleThreadScheduledExecutor();
healthScheduler.scheduleAtFixedRate(() -> {
if (healthMonitor.hasExceededFailureThreshold()) {
LOGGER.error("Health monitor threshold exceeded ({} consecutive failures). Initiating shutdown for restart.",
@@ -36,22 +36,24 @@ public class HealthCheckServer implements AutoCloseable {
final HealthStatus status = healthMonitor.getHealthStatus();
final HealthResponse response = HealthResponse.fromHealthStatus(status);
final String jsonResponse = response.toJson();
final byte[] body = jsonResponse.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set(CONTENT_TYPE, CONTENT_TYPE_JSON);
exchange.sendResponseHeaders(response.getHttpStatusCode(), jsonResponse.length());
exchange.sendResponseHeaders(response.getHttpStatusCode(), body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(jsonResponse.getBytes(StandardCharsets.UTF_8));
os.write(body);
}
} catch (Exception e) {
LOGGER.error("Error handling health check", e);
final HealthResponse errorResponse = HealthResponse.error("Internal error");
final String jsonResponse = errorResponse.toJson();
final byte[] body = jsonResponse.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set(CONTENT_TYPE, CONTENT_TYPE_JSON);
exchange.sendResponseHeaders(errorResponse.getHttpStatusCode(), jsonResponse.length());
exchange.sendResponseHeaders(errorResponse.getHttpStatusCode(), body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(jsonResponse.getBytes(StandardCharsets.UTF_8));
os.write(body);
}
}
}
@@ -5,11 +5,12 @@ import static home.automation.health.HealthState.*;
import java.time.Instant;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
public class HealthMonitor {
private final List<HealthIndicator> indicators;
private int consecutiveFailures = 0;
private final AtomicInteger consecutiveFailures = new AtomicInteger(0);
private final int failureThreshold;
public HealthMonitor(List<HealthIndicator> indicators) {
@@ -33,9 +34,9 @@ public class HealthMonitor {
// Track consecutive failures
if (overallState != UP) {
consecutiveFailures++;
consecutiveFailures.incrementAndGet();
} else {
consecutiveFailures = 0;
consecutiveFailures.set(0);
}
final Instant lastSuccessfulRead = componentHealth.stream()
@@ -53,18 +54,18 @@ public class HealthMonitor {
.map(ComponentHealth::getComponent)
.collect(Collectors.joining(", "));
message = String.format("Unhealthy components: %s (consecutive failures: %d/%d)", affected,
consecutiveFailures, failureThreshold);
consecutiveFailures.get(), failureThreshold);
}
return new HealthStatus(overallState, lastSuccessfulRead, message, componentHealth);
}
public boolean hasExceededFailureThreshold() {
return consecutiveFailures >= failureThreshold;
return consecutiveFailures.get() >= failureThreshold;
}
public int getConsecutiveFailures() {
return consecutiveFailures;
return consecutiveFailures.get();
}
private static int statePriority(HealthState state) {
@@ -11,21 +11,15 @@ public class InfluxConnection implements AutoCloseable {
private final InfluxDB connection;
public InfluxConnection(InfluxDBConfiguration configuration) {
if (configuration.getUsername().isBlank()) {
final String username = configuration.getUsername();
if (username == null || username.isBlank()) {
this.connection = InfluxDBFactory.connect(configuration.getAddress());
} else {
this.connection = InfluxDBFactory.connect(
configuration.getAddress(),
configuration.getUsername(),
username,
configuration.getPassword());
}
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
this.close();
} catch (Exception e) {
LOGGER.error("Error closing InfluxDB connection during shutdown", e);
}
}));
}
public InfluxDB getConnection() {
@@ -18,13 +18,6 @@ public class ModbusConnection implements Connection, AutoCloseable {
public ModbusConnection(ConfigurationStrategy config) {
this.config = config;
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
this.close();
} catch (Exception e) {
LOGGER.warn("Error closing Modbus connection during shutdown: {}", e.getMessage());
}
}));
}
private Pair<InetAddress, Integer> getPortAndAddressFromConfig(ConfigurationStrategy config) {
@@ -145,7 +145,8 @@ public class ModbusRtuTcpRepository implements Repository {
LOGGER.info("Registry {}: calculated value = {}", register, value);
return new Registry(register, mapping.getDesc(), value);
} catch (Exception e) {
LOGGER.error("Cannot get registry {} due to: ", register, e);
LOGGER.warn("Cannot get registry {}: {}", register, e.getMessage());
LOGGER.debug("Cannot get registry {} stack trace", register, e);
throw new RuntimeException("Cannot get registry " + register, e);
}
}