Skip to content

fix(dwctl): release the advisory lock after a failed leadership gain - #1566

Closed
pjb157 wants to merge 1 commit into
mainfrom
peter/leader-election-retry-test
Closed

pjb157 wants to merge 1 commit into
mainfrom
peter/leader-election-retry-test

Conversation

@pjb157

@pjb157 pjb157 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Extracts the leader-election fix from #1481 into its own change and reworks its test so it is deterministic in CI.

Production change (dwctl/src/leader_election.rs):

  • A failed on_gain_leadership now clears is_leader, runs on_lose_leadership, and releases the advisory lock via release_leader_connection. Previously the replica kept the lock on a pooled session with the flag set, so no other replica could take over.
  • A failed keepalive ping releases the lock explicitly and closes the session on unlock error, rather than returning a possibly-still-locked connection to the pool.
  • Shutdown releases the lock explicitly.
  • The loop moved into run_leader_election, which takes the election cadence as a Stream<Item = ()>. leader_election_task keeps its signature and feeds it the same 30 s interval.

Test rework (failed_gain_cleans_up_unlocks_and_retries):

  • No more tokio::time::pause() / advance. The test feeds election ticks over an mpsc channel, so the retry cannot happen until the test asks for it.
  • The failed gain's unlock is observed with a blocking pg_advisory_lock on a separate session, which PostgreSQL grants as part of the leader session's release. No polling against a real database under a fake clock.
  • Assertions are unchanged: lose callback once, advisory lock released, retry gains leadership, shutdown calls lose a second time and releases the lock.
  • #[ignore] removed.

Verification

  • 14 consecutive local passes (cargo test and cargo nextest), ~0.15 s each.
  • Mutation check: removing the failed-gain unlock makes the test fail cleanly with failed gain must release its advisory lock: Elapsed inside the wait budget, not hang.

🤖 Generated with Claude Code

A gain callback that errors left the replica holding the leader advisory
lock on a pooled session with is_leader still true, so no other replica
could take over. Now a failed gain runs the lose callback, unlocks, and
returns the connection; a ping failure closes the session instead of
handing a possibly-still-locked connection back to the pool; and shutdown
unlocks explicitly.

The covering test is driven deterministically: the election cadence is a
stream the test feeds tick by tick instead of a paused tokio clock, and
the unlock is observed with a blocking pg_advisory_lock that PostgreSQL
grants as part of the leader session's release. The previous paused-clock
version was #[ignore]d after hanging on three consecutive CI runs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 4, 2026 09:38
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying control-layer with  Cloudflare Pages  Cloudflare Pages

Latest commit: 4e10e5a
Status: ✅  Deploy successful!
Preview URL: https://eb233c2d.control-layer.pages.dev
Branch Preview URL: https://peter-leader-election-retry.control-layer.pages.dev

View logs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The keepalive-failure path releases the advisory lock before running on_lose_leadership, which can permit overlapping “leader” work, and there is a redundant drop(conn) likely to trip Clippy.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes a leader-election failure mode in dwctl where an instance could retain a PostgreSQL advisory lock after failing to fully assume leadership, preventing other replicas from taking over. It also refactors the election loop to accept an externally-driven tick stream so the leader-election test can be deterministic in CI.

Changes:

  • Added explicit advisory-lock cleanup (release_leader_connection) and invoked it on failed gain, keepalive failure, and shutdown paths.
  • Refactored the election loop into run_leader_election(Stream<Item = ()>) while keeping leader_election_task’s public signature (using a 30s interval stream in production).
  • Reworked the leader-election test to drive election ticks via an mpsc channel and validate unlock behavior with a blocking pg_advisory_lock observation.
File summaries
File Description
dwctl/src/leader_election.rs Ensures advisory locks are released on failure/shutdown and makes election cadence injectable for deterministic testing.
Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 214 to +218
is_leader.store(false, Ordering::Relaxed);
leader_conn = None;
// The ping failing does not prove the session is dead: a
// statement timeout leaves it alive and still holding the
// advisory lock. Never return such a connection to the pool.
release_leader_connection(&mut leader_conn, lock_id).await;
Comment on lines +55 to +56
drop(conn);
}
@pjb157 pjb157 closed this Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants