diff --git a/src/main/java/home/gateway/application/CommandService.java b/src/main/java/home/gateway/application/CommandService.java index 9c342c1..afd9927 100644 --- a/src/main/java/home/gateway/application/CommandService.java +++ b/src/main/java/home/gateway/application/CommandService.java @@ -53,6 +53,7 @@ public class CommandService implements Commands { private final Map restores = new LinkedHashMap<>(); private final Map lastWrite = new HashMap<>(); private final Map backoffs = new HashMap<>(); + private boolean unsaved; private record Backoff(int failures, long retryAtNanos) { } @@ -171,6 +172,9 @@ public class CommandService implements Commands { public void runDueRestores() { lock.lock(); try { + if (unsaved) { + persist(); + } var now = clock.instant(); restores.values().stream() .filter(restore -> !restore.due().isAfter(now)) @@ -286,6 +290,7 @@ public class CommandService implements Commands { restores.put(restore.register(), restore); try { store.save(List.copyOf(restores.values())); + unsaved = false; backoffs.remove(restore.register()); } catch (IOException e) { if (previous == null) { @@ -303,11 +308,14 @@ public class CommandService implements Commands { persist(); } + /** A failed save is retried on the next tick: a stale file would bring a done restore back after a restart. */ private void persist() { try { store.save(List.copyOf(restores.values())); + unsaved = false; } catch (IOException e) { - LOGGER.error("could not save the restore list: {}", e.getMessage()); + unsaved = true; + LOGGER.error("could not save the restore list, will retry: {}", e.getMessage()); } } diff --git a/src/test/java/home/gateway/application/CommandServiceTest.java b/src/test/java/home/gateway/application/CommandServiceTest.java index f608479..39df555 100644 --- a/src/test/java/home/gateway/application/CommandServiceTest.java +++ b/src/test/java/home/gateway/application/CommandServiceTest.java @@ -645,6 +645,40 @@ class CommandServiceTest { assertEquals(48, service.pendingRestores().getFirst().appliedValue()); } + @Test + void aFailedSaveAfterARestoreIsRetriedOnTheNextTick() throws IOException { + var service = boosted(); + clock.advance(HOUR); + storeFails = true; + + service.runDueRestores(); + + assertEquals(45, registers.values.get(4144)); + assertTrue(service.pendingRestores().isEmpty()); + assertEquals(1, stored.size()); + + service.runDueRestores(); + assertEquals(1, stored.size()); + storeFails = false; + service.runDueRestores(); + + assertTrue(stored.isEmpty()); + } + + @Test + void aFailedSaveAfterALastingCommandIsRetriedOnTheNextTick() throws IOException { + var service = boosted(); + clock.advance(Duration.ofMinutes(2)); + storeFails = true; + + assertEquals(Status.APPLIED, service.apply(command("cwu_comfort", 40, null)).status()); + assertEquals(1, stored.size()); + storeFails = false; + service.runDueRestores(); + + assertTrue(stored.isEmpty()); + } + private static final class MutableClock extends Clock { private Instant now = T0; private long nanos;