Repository navigation
Cluster epoch: isolate GarnetEpoch and replace the busy-spin barrier with a jittered self-poll - #2203
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The solution references a missing project, and the CPU diagnoser uses an incompatible operation count that understates its metrics.
Review effort: Balanced
Findings: 3
Open (5)
What changed in this PR
This PR isolates cluster epoch tracking, adds an experimental wake-based barrier while preserving production spin behavior, and introduces tests and benchmarks.
Changes:
- Extracts epoch tracking and observer logic into dedicated types.
- Adds the wake signal, barrier implementation, and component tests.
- Adds CPU/allocation benchmarks and performance-gate configuration.
| File | Description |
|---|---|
Garnet.slnx |
Registers an epoch litmus project. |
libs/common/Synchronization/AsyncManualResetSignal.cs |
Adds a reusable async signal. |
libs/cluster/Session/ClusterSession.cs |
Reads epochs through GarnetEpoch. |
libs/cluster/Server/Epochs/GarnetEpoch.cs |
Implements spin and wake barriers. |
libs/cluster/Server/Epochs/EpochObserver.cs |
Abstracts session quiescence scans. |
libs/cluster/Server/ClusterProvider.cs |
Delegates epoch management. |
test/cluster/Garnet.test.cluster/GarnetEpochWakeBarrierTests.cs |
Tests wake-barrier behavior. |
benchmark/BDN.benchmark/Diagnostics/CpuDiagnoser.cs |
Adds process CPU metrics. |
benchmark/BDN.benchmark/Cluster/EpochWakeBarrier.cs |
Benchmarks release-path overhead. |
benchmark/BDN.benchmark/Cluster/EpochBumpContention.cs |
Compares spin and wake contention. |
benchmark/BDN.benchmark/Cluster/EpochBenchParams.cs |
Defines composite benchmark parameters. |
test/BDNPerfTests/BDN_Benchmark_Config.json |
Adds allocation expectations. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Vasileios Zois (vazois)
requested review from
Ted Hart (TedHartMS),
Badrish Chandramouli (badrishc) and
kevin-montrose
as code owners
October 5, 2026 21:27
Vasileios Zois (vazois)
force-pushed
the
vazois/garnet-epoch-improv
branch
from
October 6, 2026 02:35
a248cd7 to
10944c8
Compare
kevin-montrose
requested changes
Oct 6, 2026
kevin-montrose
left a comment
Contributor
There was a problem hiding this comment.
Couple notes for efficiency
Vasileios Zois (vazois)
force-pushed
the
vazois/garnet-epoch-improv
branch
from
October 6, 2026 23:06
7d24fa0 to
9d9fbf8
Compare
The litmus harness was a dev scratch artifact never committed; BDN (EpochBumpContention) demonstrates the spin-vs-wake CPU issue cleanly. Co-authored-by: Copilot <[email protected]> Copilot-Session: a071df49-2309-4a9f-a4c4-dc6a77ea05aa
Vasileios Zois (vazois)
force-pushed
the
vazois/garnet-epoch-improv
branch
from
October 6, 2026 23:41
9d9fbf8 to
45eb622
Compare
kevin-montrose
self-requested a review
October 7, 2026 17:18
kevin-montrose
approved these changes
Oct 7, 2026
Tiago Nápoli (tiagonapoli)
approved these changes
Oct 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Problem
Adding a primary to a live cluster can wedge the donor:
MIGRATE ... SLOTSRANGEpins CPU (~3598% observed) and makes no progress for an hour. The donor stops answeringPINGand logs connection failures while recipients still own zero slots. Root cause (see investigation): the config-epoch barrier that a bump waits on is an uncapped cooperative busy-spin (await Task.Yield()over a full session scan) with no backoff, so a bump that cannot immediately converge burns whole cores indefinitely.This PR isolates the epoch barrier and replaces the busy-spin with a jittered, capped self-poll that idles instead of pinning cores, and wires production to it.
What this PR does
1. Isolate the epoch into its own instance. Extracts epoch state out of
ClusterProviderinto a dedicatedGarnetEpoch<TEpochObserver>(libs/cluster/Server/Epochs/). Quiescence is determined through a newIEpochObserverabstraction, withServerEpochObserverSourcewrapping the liveStoreWrapperscan. This isolates and clarifies the current API and makes the barrier testable without a live server.TEpochObserveris constrained tostructso the JIT specializes the generic and the quiescence check devirtualizes/inlines.2. Replace the busy-spin with a self-poll barrier (enabled in production).
BumpAndWaitForEpochTransitionAsyncincrements the epoch, then:SpinWait(40 iterations) covers the common case where sessions drain immediately.Task.Delay) of the sameIEpochObserverquiescence predicate.There is no waker. A releasing session does nothing — the session hot path is untouched (zero locking/allocation on release). Each parked bump re-scans on its own jittered schedule, so concurrent bumps need no coordination (correctness rests on quiescence monotonicity: once all sessions cross the target epoch, every later re-scan still observes quiescence), and a genuinely stuck bump idles at the capped poll rate (
maxParkDelay, default 50 ms) instead of burning a core. The bounded slice is also the liveness backstop: a session that quiesces is always seen by the next re-scan. Per-bump jitter decorrelates concurrent waiters (no thundering herd).Production (
ClusterProvider.BumpAndWaitForEpochTransitionAsync) now calls this path directly. The legacy busy-spin is retained asBumpAndSpinWaitForEpochTransitionAsynconly as an A/B baseline for the benchmark below; it shares the exact sameIEpochObserverpredicate, so the two differ only in how they wait, not in what they wait for.3. Add a reusable backoff primitive.
ExponentialBackoff(libs/common/) — a lock-free, allocation-free struct implementing capped exponential backoff with equal-jitter in the upper portion of each slice (Random.Shared). The epoch self-poll derives each park slice from it.4. Prove the problem and the fix (BDN + custom diagnoser).
CpuDiagnoser— a custom BenchmarkDotNetIDiagnoserreporting the per-op CPU the wall-clock mean cannot see, split into KernelMode CPU / UserMode CPU / Total CPU columns (kernel =PrivilegedProcessorTime, i.e. syscalls/scheduler). CPU is normalized over the actual workload iterations only.EpochBumpContention— an A/B benchmark that runs the identical contention under both strategies (WaitMode = Spin | Poll), measuring the cost the bumping thread (the waiter) pays to complete one config-epoch bump and wait for every session to cross the new epoch, whileAcquiringThreadsbackground sessions continuously acquire/release their epoch.5. Component tests.
GarnetEpochBarrierTests(7 tests) covering the fast path, idle/ahead sessions, poll completion on blocking-session release, re-acquire at/above target, cancellation, concurrent bumps each converging, and a release-race soak.Benchmark evidence
Environment: BenchmarkDotNet v0.15.8, net10.0, Windows 11, AMD Ryzen 7 PRO 7840U (16 logical / 8 physical cores),
IterationCount=12, WarmupCount=3. TheCpuDiagnosercolumns are whole-process CPU per operation, summed across all threads, so a value larger than the wall-clock mean means more than one core was busy.Waiter penalty and CPU burn (
EpochBumpContention)What it measures: the cost paid by the bumping thread (the waiter) to complete one config-epoch bump and wait for every session to cross the new epoch, while
AcquiringThreadsbackground sessions continuously acquire and release their epoch (randomized short holds). TheWaitModeaxis runs the identical contention under both strategies. Waiter penalty is the mean wall-clock time of one bump-and-wait; the CPU columns are whole-process CPU consumed per bump.Reading it: the legacy Spin barrier burns ~90–121 ms of CPU per bump — several cores for a ~15–20 ms wait, and the user-mode burn grows with thread count (90 → 106 → 121 ms) — and allocates 140–165 B per bump (
Task.Yieldcontinuation boxing). This is the production ~3598%-CPU saturation in miniature. The Poll barrier cuts bump-wait CPU to ~0.2–0.4 ms (≈250–600× less) and allocates nothing, at the cost of a modest, bounded ~9 ms of extra wall-clock per bump (the jittered park quantum replacing frantic spinning).Why this trade-off is right: the config-epoch barrier fires rarely and sits off the data hot path. Trading ~9 ms of wall-clock per bump to eliminate a full-core CPU burn is exactly what keeps the donor answering
PINGand lets the migration scan make progress. CPU is the axis that matters on a live server — it is the difference between serving traffic during migration and starving.Notes / follow-ups