Starup Report added and StatisticsService rolling avgs optimized to O(n)
This commit is contained in:
@@ -98,7 +98,14 @@ public class ApplicationInitializer {
|
||||
);
|
||||
|
||||
context.setProcessManagerService(new ProcessManagerService(config));
|
||||
context.getProcessManagerService().startProcesses();
|
||||
|
||||
ProcessManagerService.StartupReport startupReport =
|
||||
context.getProcessManagerService().startProcesses();
|
||||
|
||||
if (startupReport.hasFailures()) {
|
||||
context.getProcessManagerService().shutdown();
|
||||
throw new IllegalStateException(startupReport.buildFailureMessage());
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ public class DataPersistenceService {
|
||||
String query = """
|
||||
SELECT value, timestamp
|
||||
FROM binary_event
|
||||
ORDER BY timestamp DESC
|
||||
ORDER BY id DESC
|
||||
LIMIT ?
|
||||
""";
|
||||
try (Connection c = DriverManager.getConnection(DB_URL); PreparedStatement ps = c.prepareStatement(query)) {
|
||||
|
||||
@@ -13,6 +13,205 @@ import java.util.Properties;
|
||||
*/
|
||||
public class ProcessManagerService {
|
||||
|
||||
/**
|
||||
* Represents startup status for a single managed process component.
|
||||
*/
|
||||
public static final class ProcessStartStatus {
|
||||
|
||||
private final String componentName;
|
||||
private final boolean enabled;
|
||||
private final boolean started;
|
||||
private final String errorMessage;
|
||||
|
||||
/**
|
||||
* Creates an immutable process start status instance.
|
||||
*
|
||||
* @param componentName logical component name
|
||||
* @param enabled whether component startup was enabled
|
||||
* @param started whether startup completed without launch error
|
||||
* @param errorMessage optional startup error details
|
||||
*/
|
||||
private ProcessStartStatus(
|
||||
String componentName,
|
||||
boolean enabled,
|
||||
boolean started,
|
||||
String errorMessage
|
||||
) {
|
||||
this.componentName = componentName;
|
||||
this.enabled = enabled;
|
||||
this.started = started;
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a disabled status for a component.
|
||||
*
|
||||
* @param componentName logical component name
|
||||
* @return disabled status instance
|
||||
*/
|
||||
static ProcessStartStatus disabled(String componentName) {
|
||||
return new ProcessStartStatus(componentName, false, false, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a successful startup status for a component.
|
||||
*
|
||||
* @param componentName logical component name
|
||||
* @return successful status instance
|
||||
*/
|
||||
static ProcessStartStatus started(String componentName) {
|
||||
return new ProcessStartStatus(componentName, true, true, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a failed startup status for a component.
|
||||
*
|
||||
* @param componentName logical component name
|
||||
* @param errorMessage startup failure details
|
||||
* @return failure status instance
|
||||
*/
|
||||
static ProcessStartStatus failed(String componentName, String errorMessage) {
|
||||
return new ProcessStartStatus(componentName, true, false, errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether startup failed for an enabled component.
|
||||
*
|
||||
* @return {@code true} when startup failed
|
||||
*/
|
||||
public boolean hasFailed() {
|
||||
return enabled && !started;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns logical component name.
|
||||
*
|
||||
* @return component name
|
||||
*/
|
||||
public String getComponentName() {
|
||||
return componentName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether startup was enabled.
|
||||
*
|
||||
* @return enabled flag
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether startup completed without launch error.
|
||||
*
|
||||
* @return started flag
|
||||
*/
|
||||
public boolean isStarted() {
|
||||
return started;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns startup error details when available.
|
||||
*
|
||||
* @return error details or {@code null}
|
||||
*/
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregates startup status for all managed process components.
|
||||
*/
|
||||
public static final class StartupReport {
|
||||
|
||||
private final ProcessStartStatus mqttSimulator;
|
||||
private final ProcessStartStatus unreal;
|
||||
|
||||
/**
|
||||
* Creates a startup report.
|
||||
*
|
||||
* @param mqttSimulator startup status of MQTT simulator component
|
||||
* @param unreal startup status of Unreal launcher component
|
||||
*/
|
||||
StartupReport(
|
||||
ProcessStartStatus mqttSimulator,
|
||||
ProcessStartStatus unreal
|
||||
) {
|
||||
this.mqttSimulator = Objects.requireNonNull(mqttSimulator);
|
||||
this.unreal = Objects.requireNonNull(unreal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns startup status of MQTT simulator component.
|
||||
*
|
||||
* @return MQTT simulator startup status
|
||||
*/
|
||||
public ProcessStartStatus getMqttSimulator() {
|
||||
return mqttSimulator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns startup status of Unreal launcher component.
|
||||
*
|
||||
* @return Unreal startup status
|
||||
*/
|
||||
public ProcessStartStatus getUnreal() {
|
||||
return unreal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether at least one enabled process failed to launch.
|
||||
*
|
||||
* @return {@code true} when startup has failures
|
||||
*/
|
||||
public boolean hasFailures() {
|
||||
return mqttSimulator.hasFailed() || unreal.hasFailed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a compact startup failure message suitable for exceptions.
|
||||
*
|
||||
* @return failure message
|
||||
*/
|
||||
public String buildFailureMessage() {
|
||||
StringBuilder message =
|
||||
new StringBuilder("External process startup failed:");
|
||||
|
||||
appendFailure(message, mqttSimulator);
|
||||
appendFailure(message, unreal);
|
||||
|
||||
return message.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends one failed component summary to a message builder.
|
||||
*
|
||||
* @param message target message builder
|
||||
* @param status component startup status
|
||||
*/
|
||||
private static void appendFailure(
|
||||
StringBuilder message,
|
||||
ProcessStartStatus status
|
||||
) {
|
||||
if (!status.hasFailed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
message.append(System.lineSeparator())
|
||||
.append("- ")
|
||||
.append(status.getComponentName())
|
||||
.append(": ");
|
||||
|
||||
String error = status.getErrorMessage();
|
||||
if (error == null || error.isBlank()) {
|
||||
message.append("unknown startup error");
|
||||
} else {
|
||||
message.append(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface ProcessLauncher {
|
||||
Process launch(ProcessBuilder processBuilder) throws IOException;
|
||||
@@ -78,23 +277,31 @@ public class ProcessManagerService {
|
||||
|
||||
/**
|
||||
* Starts optional helper processes according to configuration flags.
|
||||
*
|
||||
* @return startup status report for all managed components
|
||||
*/
|
||||
public void startProcesses() {
|
||||
public StartupReport startProcesses() {
|
||||
|
||||
startPythonIfEnabled();
|
||||
startUnrealIfEnabled();
|
||||
ProcessStartStatus mqttSimulator = startPythonIfEnabled();
|
||||
ProcessStartStatus unreal = startUnrealIfEnabled();
|
||||
|
||||
return new StartupReport(mqttSimulator, unreal);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Starts the Python MQTT simulator if enabled.
|
||||
*
|
||||
* @return startup status for the simulator component
|
||||
*/
|
||||
private void startPythonIfEnabled() {
|
||||
private ProcessStartStatus startPythonIfEnabled() {
|
||||
|
||||
boolean enabled = Boolean.parseBoolean(
|
||||
config.getProperty("mqtt_sim.enabled", "false")
|
||||
);
|
||||
|
||||
if (!enabled) return;
|
||||
if (!enabled) {
|
||||
return ProcessStartStatus.disabled("mqtt_sim");
|
||||
}
|
||||
|
||||
try {
|
||||
String script = cleanConfigValue(config.getProperty("mqtt_sim.script"));
|
||||
@@ -107,25 +314,39 @@ public class ProcessManagerService {
|
||||
pythonProcess = processLauncher.launch(pb);
|
||||
|
||||
Logger.info("PROCESS", "Mqtt Simulator gestartet");
|
||||
} catch (IOException e) {
|
||||
return ProcessStartStatus.started("mqtt_sim");
|
||||
} catch (Exception e) {
|
||||
Logger.error("PROCESS", "Mqtt Simulator Start fehlgeschlagen", e);
|
||||
return ProcessStartStatus.failed(
|
||||
"mqtt_sim",
|
||||
exceptionMessage(e)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Starts Unreal-related processes if enabled.
|
||||
*
|
||||
* @return startup status for the Unreal component
|
||||
*/
|
||||
private void startUnrealIfEnabled() {
|
||||
private ProcessStartStatus startUnrealIfEnabled() {
|
||||
|
||||
boolean enabled = Boolean.parseBoolean(config.getProperty("unreal.enabled", "false"));
|
||||
|
||||
if (!enabled) return;
|
||||
if (!enabled) {
|
||||
return ProcessStartStatus.disabled("unreal");
|
||||
}
|
||||
|
||||
try {
|
||||
startUnrealEngine();
|
||||
return ProcessStartStatus.started("unreal");
|
||||
|
||||
} catch (IOException e) {
|
||||
} catch (Exception e) {
|
||||
Logger.error("PROCESS", "Unreal Start fehlgeschlagen", e);
|
||||
return ProcessStartStatus.failed(
|
||||
"unreal",
|
||||
exceptionMessage(e)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,6 +494,23 @@ public class ProcessManagerService {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a compact message for startup exception reporting.
|
||||
*
|
||||
* @param exception startup exception
|
||||
* @return compact exception details
|
||||
*/
|
||||
private static String exceptionMessage(Exception exception) {
|
||||
String type = exception.getClass().getSimpleName();
|
||||
String message = exception.getMessage();
|
||||
|
||||
if (message == null || message.isBlank()) {
|
||||
return type;
|
||||
}
|
||||
|
||||
return type + ": " + message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Passes optional simulator settings from app config to the Python process environment.
|
||||
*
|
||||
|
||||
@@ -5,7 +5,6 @@ import vassistent.model.RatioPoint;
|
||||
import vassistent.util.Logger;
|
||||
|
||||
import java.sql.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -31,6 +30,10 @@ public class StatisticsService {
|
||||
* @return ratio in range {@code [0, 1]} or {@code 0.0} when unavailable
|
||||
*/
|
||||
public double getRatio(int lastN) {
|
||||
if (lastN <= 0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
String query = """
|
||||
SELECT AVG(value)
|
||||
FROM (
|
||||
@@ -58,30 +61,40 @@ public class StatisticsService {
|
||||
/**
|
||||
* Computes rolling averages for chart rendering.
|
||||
*
|
||||
* @param windowSize number of previous entries considered for each point
|
||||
* The rolling window includes the current entry and up to {@code windowSize - 1}
|
||||
* previous entries.
|
||||
*
|
||||
* @param windowSize number of entries considered per rolling average window
|
||||
* @return chronological list of ratio points
|
||||
*/
|
||||
public List<RatioPoint> getLastNAverages(int windowSize) {
|
||||
|
||||
if (windowSize <= 0) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<DatabaseEntry> entries = persistenceService.getLastEntries(windowSize + 1);
|
||||
|
||||
List<RatioPoint> result = new ArrayList<>();
|
||||
|
||||
if (entries.isEmpty())
|
||||
if (entries.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
double rollingSum = 0.0;
|
||||
int windowStart = 0;
|
||||
|
||||
for (int i = 0; i < entries.size(); i++) {
|
||||
int start = Math.max(0, i - windowSize);
|
||||
rollingSum += entries.get(i).getValue();
|
||||
|
||||
double sum = 0;
|
||||
int count = 0;
|
||||
|
||||
for (int j = start; j < i; j++) {
|
||||
sum += entries.get(j).getValue();
|
||||
count++;
|
||||
if (i - windowStart + 1 > windowSize) {
|
||||
rollingSum -= entries.get(windowStart).getValue();
|
||||
windowStart++;
|
||||
}
|
||||
|
||||
double avg = count == 0 ? 0 : sum / count;
|
||||
int windowCount = i - windowStart + 1;
|
||||
double avg = rollingSum / windowCount;
|
||||
|
||||
|
||||
result.add(new RatioPoint(
|
||||
entries.get(i).getTimestamp(),
|
||||
|
||||
Reference in New Issue
Block a user