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