Skip to content

Commit f434776

Browse files
Ingvordclaude
andcommitted
Step 6: restore AvailabilityAnalyzer state from MariaDB on restart
AttributeAvailability.restore(state, since) sets state and consecutiveFailures silently (no domain events emitted). For DOWN state, since becomes downtimeStart so a subsequent recovery correctly closes the existing tabDowntime Interval row. AvailabilityAnalyzer.seed(attributeId, state, since) creates (or finds) the per-attribute state machine and calls restore on it. MariaDbSink.loadCurrentStates() queries tabCurrent State and returns a map of CurrentState records (attributeId, state, since). Main calls loadCurrentStates() + seed() after the engine is built but before engine.start(), so no live events race with the restore. Recovery failure is non-fatal: the server starts fresh with a warning. Covered by 4 new tests in AvailabilityAnalyzerTest (seed DOWN/STALE/UP paths and continued failure accumulation after restore). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent fa66da9 commit f434776

5 files changed

Lines changed: 100 additions & 1 deletion

File tree

src/main/java/wpn/hdri/ss/Main.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,20 @@ public static void main(String[] args) throws Exception {
8080
logger.warn("Failed attributes will not be monitored: {}", factory.getFailedAttributes());
8181
}
8282

83-
// Register attribute names for human-readable DB records
8483
if (mariaDbSink != null) {
8584
final MariaDbSink sink = mariaDbSink;
85+
86+
// Register attribute names for human-readable DB records
8687
engine.getAttributes().forEach(attr -> sink.registerAttribute(attr.id, attr.fullName));
88+
89+
// Restore persisted availability state before the engine starts collecting
90+
try {
91+
sink.loadCurrentStates().forEach((id, cs) ->
92+
analyzer.seed(id, cs.state(), cs.since()));
93+
logger.info("Availability state restored from MariaDB");
94+
} catch (Exception e) {
95+
logger.warn("Could not restore state from MariaDB, starting fresh: {}", e.getMessage());
96+
}
8797
}
8898

8999
// --- HTTP server ---

src/main/java/wpn/hdri/ss/engine2/AttributeAvailability.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ AvailabilityState state() {
4747
return state;
4848
}
4949

50+
/**
51+
* Restores persisted state on startup without emitting any domain events.
52+
* For DOWN state, {@code since} becomes the downtimeStart so a subsequent
53+
* recovery correctly closes the existing interval.
54+
*/
55+
void restore(AvailabilityState restoredState, Instant since) {
56+
this.state = restoredState;
57+
this.consecutiveFailures = restoredState == AvailabilityState.UP ? 0 : downAfter;
58+
this.downtimeStart = restoredState == AvailabilityState.DOWN ? since : null;
59+
}
60+
5061
private void handleSuccess(Instant ts) {
5162
consecutiveFailures = 0;
5263
if (state == AvailabilityState.UP) return;

src/main/java/wpn/hdri/ss/engine2/AvailabilityAnalyzer.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package wpn.hdri.ss.engine2;
22

3+
import wpn.hdri.ss.event.AvailabilityState;
34
import wpn.hdri.ss.event.DomainEvent;
45
import wpn.hdri.ss.event.EventSink;
56
import wpn.hdri.ss.event.TechnicalEvent;
67

8+
import java.time.Instant;
79
import java.util.concurrent.ConcurrentHashMap;
810

911
/**
@@ -35,6 +37,16 @@ public void onEvent(TechnicalEvent event) {
3537
.process(event);
3638
}
3739

40+
/**
41+
* Seeds a single attribute's state machine from persisted storage.
42+
* Must be called before {@code engine.start()} so no live events race with the restore.
43+
*/
44+
public void seed(int attributeId, AvailabilityState state, Instant since) {
45+
states.computeIfAbsent(attributeId,
46+
id -> new AttributeAvailability(id, staleAfter, downAfter, domainSink))
47+
.restore(state, since);
48+
}
49+
3850
@Override
3951
public String name() {
4052
return "AvailabilityAnalyzer";

src/main/java/wpn/hdri/ss/writer/MariaDbSink.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
import wpn.hdri.ss.configuration.MariaDbConfiguration;
66
import wpn.hdri.ss.event.*;
77

8+
import static wpn.hdri.ss.event.AvailabilityState.*;
9+
810
import java.sql.*;
911
import java.time.Instant;
12+
import java.util.HashMap;
1013
import java.util.Map;
1114
import java.util.UUID;
1215
import java.util.concurrent.ConcurrentHashMap;
@@ -63,6 +66,29 @@ public MariaDbSink(MariaDbConfiguration config) {
6366
this.config = config;
6467
}
6568

69+
/**
70+
* Reads all rows from {@code tabCurrent State} and returns them keyed by attribute_id.
71+
* Used at startup to restore {@link wpn.hdri.ss.engine2.AvailabilityAnalyzer} state.
72+
*/
73+
public Map<Integer, CurrentState> loadCurrentStates() throws SQLException {
74+
Map<Integer, CurrentState> result = new HashMap<>();
75+
String sql = "SELECT attribute_id, state, since FROM `tabCurrent State`";
76+
try (PreparedStatement ps = connection().prepareStatement(sql);
77+
ResultSet rs = ps.executeQuery()) {
78+
while (rs.next()) {
79+
int id = rs.getInt("attribute_id");
80+
AvailabilityState state = AvailabilityState.valueOf(rs.getString("state"));
81+
Instant since = rs.getTimestamp("since").toInstant();
82+
result.put(id, new CurrentState(id, state, since));
83+
}
84+
}
85+
logger.info("Loaded {} persisted attribute state(s) from tabCurrent State", result.size());
86+
return result;
87+
}
88+
89+
/** Snapshot of a persisted attribute availability state. */
90+
public record CurrentState(int attributeId, AvailabilityState state, Instant since) {}
91+
6692
/** Called after engine start so attribute names appear in DB records. */
6793
public void registerAttribute(int id, String fullName) {
6894
attributeNames.put(id, fullName);

src/test/java/wpn/hdri/ss/engine2/AvailabilityAnalyzerTest.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,46 @@ public void multipleAttributesTrackedIndependently() {
145145
assertEquals(3, aEvents);
146146
}
147147

148+
// --- recovery tests ---
149+
150+
@Test
151+
public void seedDownStateClosesDowntimeOnRecovery() {
152+
Instant downtimeStart = Instant.now().minusSeconds(300);
153+
analyzer.seed(ATTR_ID, AvailabilityState.DOWN, downtimeStart);
154+
155+
succeed();
156+
157+
assertEquals(2, emitted.size());
158+
assertTransition(emitted.get(0), AvailabilityState.DOWN, AvailabilityState.UP);
159+
DowntimeClosed closed = (DowntimeClosed) emitted.get(1);
160+
assertEquals(downtimeStart, closed.openedAt());
161+
}
162+
163+
@Test
164+
public void seedStaleStateTransitionsToUpOnRecovery() {
165+
analyzer.seed(ATTR_ID, AvailabilityState.STALE, Instant.now().minusSeconds(60));
166+
167+
succeed();
168+
169+
assertEquals(1, emitted.size());
170+
assertTransition(emitted.get(0), AvailabilityState.STALE, AvailabilityState.UP);
171+
}
172+
173+
@Test
174+
public void seedUpStateEmitsNothingOnSuccess() {
175+
analyzer.seed(ATTR_ID, AvailabilityState.UP, Instant.now());
176+
succeed();
177+
assertTrue(emitted.isEmpty());
178+
}
179+
180+
@Test
181+
public void seedDownStateContinuesAccumulatingFailures() {
182+
analyzer.seed(ATTR_ID, AvailabilityState.DOWN, Instant.now().minusSeconds(60));
183+
// more failures while DOWN should not emit anything new
184+
fail(3);
185+
assertTrue(emitted.isEmpty());
186+
}
187+
148188
// --- assertion helpers ---
149189

150190
private static void assertTransition(DomainEvent event, AvailabilityState from, AvailabilityState to) {

0 commit comments

Comments
 (0)