fix(restore): retry a failed restore-list save on the next tick
If forget() could not save after a successful restore, the file kept the done record and a restart would reload it. The failed save is now remembered and retried each tick until it goes through. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b06dde4587
commit
7263c9e3c3
2 files changed
+43
-1
No files matched your search
@@ -53,6 +53,7 @@ public class CommandService implements Commands {
|
||||
private final Map<Integer, Restore> restores = new LinkedHashMap<>();
|
||||
private final Map<Integer, Long> lastWrite = new HashMap<>();
|
||||
private final Map<Integer, Backoff> 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user