feat(sherdlock): coordinate token-lock cleanup leadership across replicas - #1982
feat(sherdlock): coordinate token-lock cleanup leadership across replicas#1982sid200727 wants to merge 3 commits into
Conversation
…icas Implements LFDT-Panurus#1798: currently every replica runs the token-locks cleanup tick independently, causing wasted, overlapping work (all replicas race to delete the same expired rows). This adds a leadership check per tick so only one replica performs cleanup, following the same pattern already used for keystore cleanup (AcquireCleanupLeadership). Changes -> Added AcquireCleanupLeadership to both driver.TokenLockStore and sherdlock.Locker interfaces -> Added a shared driver.NoopCleanupLeadership for non-distributed backends (sqlite, in-memory), which always grant leadership locally -> Wired the real Postgres advisory-lock implementation via the existing NewCleanupLeaderFactory(), using a lock id independent of the schema-creation lock id already on TokenLockStore.lockID -> manager.go's cleaner() now acquires leadership before each Cleanup tick; skips the tick if not acquired; releases leadership after -> Regenerated the FakeLocker mock; added AcquireCleanupLeadership to the hand-written mockLocker test double, defaulting to always-granted so all existing tests are unaffected -> Added two new subtests to TestManager_Cleaner: skip-on-no-leadership, and leadership-released-after-cleanup Full test suite green (selector, sqlite, postgres). Signed-off-by: Siddhi Khandelwal <siddhi.200727@gmail.com>
|
Hello @sid200727, Regards, |
…leadership Per Akram's review on LFDT-Panurus#1982, adds a Postgres integration test verifying real pg_try_advisory_lock contention: two independent TokenLockStore instances (simulating two replicas) race to acquire cleanup leadership for the same lock id concurrently. Exactly one wins; the other correctly fails to acquire. After the winner releases leadership, a third attempt succeeds again, confirming clean re-acquisition. Passes consistently across repeated runs (-count=3). Signed-off-by: Siddhi Khandelwal <siddhi.200727@gmail.com>
atharrva01
left a comment
There was a problem hiding this comment.
The per-tick election looks right to me, and reusing NewCleanupLeaderFactory instead of adding a second lock primitive is the right call. One thing I think is a real problem though.
tokenLockCleanupLockID is a single global constant, but there's one Manager per TMS (SelectorService.managerLazyCache is keyed by *token.ManagementService). So a node running N TMSs against the same postgres ends up with N cleaner goroutines all fighting over the same advisory lock, even though their token_locks tables are completely separate. One TMS gets cleaned per tick and the rest just log "leadership not acquired" and skip. Right now they all clean fine every tick, so this makes multi-TMS nodes worse off. interop's topology wires 5 TMSs, so it's not a corner case.
I think the fix is to derive the id per store instead of passing it down from the manager. TokenLockStore already does this for schema creation with createTableLockID("tokenlock"), so something like createTableLockID(prefix + "tokenlock_cleanup") inside the postgres AcquireCleanupLeadership would make it per-TMS. You could then also drop the lockID param from the Locker interface, since sqlite and in-memory ignore it anyway.
Related: the keystore cleanup you're following takes its id from config (cleanup.Config.AdvisoryLockID, used at cleanup/manager.go:205). Here it's hardcoded, so if two deployments share a postgres instance there's no knob to fix it.
On the tests:
leadership released after cleanuponly checks thatClosewas called, not thatCleanupran, or ran first. It would pass if the tick acquired leadership and released without cleaning anything.- Neither new subtest calls
m.Stop(), so both cleaner goroutines keep ticking every 50ms for the rest of the run, and CI runs the race detector. The trailingassert.NotNil(t, m)isn't really asserting anything either. - The postgres test passes
replicaA.lockID(424242, the schema lock) as the cleanup id, so it's exercising the wrong constant. It can't reach the real one without an import cycle, which I think is another sign the id is in the wrong package.
Minor: the advisory lock holds a dedicated conn off writeDB for the whole tick, so a cleanup now needs two write conns where it used to need one. Fine at MaxOpenConns: 10, just worth a comment since it's operator configurable.
Happy to be wrong on the first point if TMSs are normally expected to have separate data sources.
Summary
Implements #1798: currently every replica runs the token-locks cleanup tick independently, causing wasted, overlapping work, all replicas race to delete the same expired rows at the same time. This adds a per-tick leadership check so only one replica performs cleanup, following the exact same pattern already established for keystore cleanup (
AcquireCleanupLeadership).Changes
-> Added
AcquireCleanupLeadershipto bothdriver.TokenLockStoreandsherdlock.Lockerinterfaces-> Added a shared
driver.NoopCleanupLeadershipfor non-distributed backends (sqlite, in-memory), which always grant leadership locally, no failover code needed since there's only ever one instance-> Wired the real Postgres advisory-lock implementation via the existing
NewCleanupLeaderFactory(), using a lock id independent of the schema-creation lock id already used byTokenLockStore.lockID(deliberately separate to avoid any collision between the two purposes)->
manager.go'scleaner()now acquires leadership before eachCleanuptick, skips the tick if not acquired, and releases leadership after, matching the design: per-tick election, no permanent leader, no failover code, advisory lock auto-releases if the winner crashes-> Regenerated the
FakeLockermock; extended the hand-writtenmockLockertest double, defaulting to always-granted so all pre-existing tests are unaffected-> Added two new subtests to
TestManager_Cleaner: skip-on-no-leadership, and leadership-released-after-cleanupTesting
Full test suite green (
selector,sqlite,postgres).Notes for the Reviewer
Followed the 4-step plan from the design discussion on #1798. Happy to add a Postgres integration test exercising two concurrent "replicas" if useful on top of the unit-level coverage already here, let me know if that's wanted before merge.