Wait for an in-flight replay before closing the log watcher - #1347
Wait for an in-flight replay before closing the log watcher#1347ikhoon wants to merge 2 commits into
Conversation
Motivation: When a replica shuts down, PathChildrenCache.close() cancels its in-flight tasks with an interrupt. If a replay was waiting on delegate.execute(...) at that moment, the command had already been applied to the local data but lastReplayedRevision was never advanced, because it is updated only on the success path. The replica is then left with local data ahead of <dataDir>/last_revision, and the next start-up replays the same revision again. Re-applying a command whose effect is already in the local data fails, so the replica enters read-only mode and needs a manual re-sync. Modifications: - Wait for an in-flight replay before closing the log watcher, by acquiring the same monitor that replayLogs() holds. Releasing it is safe because listenerInfo is already null, so a replay that starts afterwards returns before executing anything. - Move the log watcher shutdown ahead of delegate.stop(). The barrier waits for a replay that is itself waiting on the delegate, so the delegate must still be running. This also matches the drain that shutdown(executor) already performs for the command executor threads. - Wait uninterruptibly for the replay result, as a safeguard on the line where the failure occurred. - Log the last replayed revision once the replay is drained. Result: - A replica that shuts down while replaying no longer leaves its local data ahead of its recorded replication progress, so it no longer enters read-only mode on the next start-up.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughShutdown now rejects new replay logs, waits for in-flight replay to finish, and closes replay resources before final cleanup. A regression test verifies durable progress, restart behavior, later revisions, and writes. ChangesReplica shutdown and replay persistence
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Replica
participant ReplayExecutor
participant LogWatcher
participant ZooKeeper
Replica->>ReplayExecutor: wait for in-flight replay
ReplayExecutor-->>Replica: replay completes
Replica->>LogWatcher: close watcher
Replica->>ReplayExecutor: close executor
Replica->>ZooKeeper: shut down Curator and ZooKeeper
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@server/src/test/java/com/linecorp/centraldogma/server/internal/replication/ZooKeeperCommandExecutorTest.java`:
- Around line 793-796: Update the replay future’s get path in this test to
record when it is interrupted, then await that signal after
commandExecutor().stop() and before proceed.countDown(). Keep the existing
stopFuture.join() synchronization, ensuring the replay is released only after
the shutdown boundary has exercised the interrupted-replay failure.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 81502200-6f21-42d9-bc32-cc33dabb8177
📒 Files selected for processing (2)
server/src/main/java/com/linecorp/centraldogma/server/internal/replication/ZooKeeperCommandExecutor.javaserver/src/test/java/com/linecorp/centraldogma/server/internal/replication/ZooKeeperCommandExecutorTest.java
| assertThat(replayEntered.await(10, TimeUnit.SECONDS)).isTrue(); | ||
| final CompletableFuture<Void> stopFuture = replaying.commandExecutor().stop(); | ||
| proceed.countDown(); | ||
| stopFuture.join(); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Synchronize the test with the shutdown boundary.
stop() is asynchronous. Line 795 can release the replay before the pre-fix code closes the log watcher. The test can then pass without exercising the interrupted-replay failure.
Make the replay future record an interrupt in its get() path. Wait for that signal before releasing proceed. This makes the test fail with the previous interruptible wait and pass with getUninterruptibly(...).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@server/src/test/java/com/linecorp/centraldogma/server/internal/replication/ZooKeeperCommandExecutorTest.java`
around lines 793 - 796, Update the replay future’s get path in this test to
record when it is interrupted, then await that signal after
commandExecutor().stop() and before proceed.countDown(). Keep the existing
stopFuture.join() synchronization, ensuring the replay is released only after
the shutdown boundary has exercised the interrupted-replay failure.
Motivation
When a replica shuts down,
PathChildrenCache.close()cancels its in-flight tasks with an interrupt.If a replay was waiting on
delegate.execute(...)at that moment, the command had already been appliedto the local data but
lastReplayedRevisionwas never advanced, because it is updated only on thesuccess path. The replica is then left with local data ahead of
<dataDir>/last_revision, and the nextstart-up replays the same revision again. Re-applying a command whose effect is already in the local
data fails, so the replica enters read-only mode and needs a manual re-sync.
Modifications
replayLogs()holds. Releasing it is safe becauselistenerInfois already null, so a replay thatstarts afterwards returns before executing anything.
delegate.stop(). The barrier waits for a replay that isitself waiting on the delegate, so the delegate must still be running. This also matches the drain
that
shutdown(executor)already performs for the command executor threads.logWatcher.close()still runs beforeshutdown(logWatcherExecutor): reversing the two would letPathChildrenCachekeep submitting to an already shut-down executor, becausesubmitToExecutor()only guards on its own state, which flips in
close().Note this covers a graceful shutdown only. A
SIGKILLbetween the local apply and the progress updateleaves the same divergence; making the replay idempotent is a separate topic.
Result
replication progress, so it no longer enters read-only mode on the next start-up.