-
Notifications
You must be signed in to change notification settings - Fork 134
Wait for an in-flight replay before closing the log watcher #1347
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ad4e3de
78f3670
2e5a957
a96842f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -748,6 +748,68 @@ void inFlightCommandOnStopIsRecordedAndNotReplayed() throws Exception { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * A replay still in flight when the executor stops must finish and record its progress durably, | ||
| * because closing the log watcher cancels its tasks with an interrupt. Otherwise the command is | ||
| * applied to the local data while last_revision is not advanced, and the next start-up applies it | ||
| * a second time. | ||
| */ | ||
| @Test | ||
| @Timeout(120) | ||
| void inFlightReplayOnStopIsRecordedAndNotReplayed() throws Exception { | ||
| final CountDownLatch replayEntered = new CountDownLatch(1); | ||
| final CountDownLatch proceed = new CountDownLatch(1); | ||
| final AtomicInteger replayCount = new AtomicInteger(); | ||
| final AtomicInteger replicaIndex = new AtomicInteger(); | ||
| final Supplier<Function<Command<?>, CompletableFuture<?>>> delegateSupplier = () -> { | ||
| final boolean replaying = replicaIndex.getAndIncrement() == 1; | ||
| final Function<Command<?>, CompletableFuture<?>> base = newMockDelegate(); | ||
| return command -> { | ||
| if (replaying && command != null && command.type() == CommandType.CREATE_REPOSITORY) { | ||
| // Park inside replayLogs() so we can stop while the replay is in flight. | ||
| replayCount.incrementAndGet(); | ||
| replayEntered.countDown(); | ||
| return CompletableFuture.supplyAsync(() -> { | ||
| try { | ||
| proceed.await(); | ||
| } catch (InterruptedException e) { | ||
| throw new RuntimeException(e); | ||
| } | ||
| return null; | ||
| }, CommonPools.blockingTaskExecutor()); | ||
| } | ||
| return base.apply(command); | ||
| }; | ||
| }; | ||
|
|
||
| try (Cluster cluster = Cluster.builder().numReplicas(3).build(delegateSupplier)) { | ||
| final Replica origin = cluster.get(0); | ||
| final Replica replaying = cluster.get(1); | ||
| origin.commandExecutor().execute(Command.createProject(Author.SYSTEM, "p")).join(); | ||
| await().untilAsserted(() -> assertThat(replaying.localRevision()).isEqualTo(0L)); | ||
|
|
||
| // Park the replay of revision 1, then stop while it is in flight. | ||
| origin.commandExecutor().execute(Command.createRepository(Author.SYSTEM, "p", "r")).join(); | ||
| assertThat(replayEntered.await(10, TimeUnit.SECONDS)).isTrue(); | ||
| final CompletableFuture<Void> stopFuture = replaying.commandExecutor().stop(); | ||
| // The shutdown must not finish while the replay is parked; that is the barrier doing its job. | ||
| assertThatThrownBy(() -> stopFuture.get(3, TimeUnit.SECONDS)) | ||
| .isInstanceOf(TimeoutException.class); | ||
| proceed.countDown(); | ||
| stopFuture.join(); | ||
|
Comment on lines
+793
to
+799
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Synchronize the test with the shutdown boundary.
Make the replay future record an interrupt in its 🤖 Prompt for AI Agents |
||
|
|
||
| // The replay finished before the log watcher was closed, so its progress is durable. | ||
| assertThat(replaying.localRevision()).isEqualTo(1L); | ||
|
|
||
| // Catch up on a later revision to prove revision 1 was not applied a second time. | ||
| replaying.commandExecutor().start().join(); | ||
| origin.commandExecutor().execute(Command.createProject(Author.SYSTEM, "p2")).join(); | ||
| await().untilAsserted(() -> assertThat(replaying.localRevision()).isEqualTo(2L)); | ||
| assertThat(replayCount).hasValue(1); | ||
| assertThat(replaying.commandExecutor().isWritable()).isTrue(); | ||
| } | ||
| } | ||
|
|
||
| private static <T> void awaitUntilReplicated(Cluster cluster, Command<T> command) { | ||
| for (int i = 0; i < cluster.size(); i++) { | ||
| final Replica replica = cluster.get(i); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.