-
Notifications
You must be signed in to change notification settings - Fork 2.6k
feat(csc): full-duplex reader-miss coalescing + invalidation batching #3965
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
Open
ndyakov
wants to merge
31
commits into
feature/csc-refresh-and-miss-coalescing
Choose a base branch
from
ndyakov/csc-coalesce-modes
base: feature/csc-refresh-and-miss-coalescing
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 4 commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
eb8681e
feat(csc): reader-miss coalescing modes (workers + full-duplex) and i…
ndyakov 2f6a9e5
fix(csc): address #3965 review and lint in coalescing modes
ndyakov a5a231a
fix(csc): FD apply-and-gate on id/gen mismatch; drain buffered pushes…
ndyakov 5cae5e1
fix(csc): retry uncached on mid-miss disable; guard cancelled fetches
ndyakov e4975ba
fix(csc): drop batched deletes on flush; rebuild batcher on window ch…
ndyakov 636ca53
ci(govulncheck): use stable Go to pick up security patches
ndyakov 68aced9
fix(csc): preserve queued deletes on rebuild; honor configured timeouts
ndyakov 62ec782
fix(csc): batcher stop/release safety; FD idle release; flush budget
ndyakov f2af313
fix(csc): close coalescer lifecycle races; honor pool budget on acquire
ndyakov 62a626b
fix(csc): coalescer latency, shutdown and clone fixes
ndyakov d3fddc4
fix(csc): make FD session I/O interruptible; refresh rebuild; stat CAS
ndyakov 296622e
fix(csc): join FD supervisor before release; drain queue in GC cleanup
ndyakov fef79a8
fix(csc): clear the push-probe deadline for negative ReadTimeout
ndyakov 00b7903
refactor(csc): full-duplex is the only miss-coalescer engine
ndyakov aa68099
fix(csc): held-conn probe safety and lifetime bounds
ndyakov 12e9709
fix(csc): scope idle-drain fallback to held conns; drainer deadline h…
ndyakov c644e8b
refactor(csc): drop the public coalescer stats API
ndyakov 968fa02
refactor(csc): drop the public coalescer stats API
ndyakov ad6e68e
fix(csc): bound the recycle-path drain like the stop path
ndyakov 883be56
fix(csc): progress-based drain backstop; departial test flake
ndyakov 5c759f5
fix(pool): clear residual read deadline in checkForData
ndyakov 6c7c6fc
fix(csc): claim FD writes; progress-aware drain backstop
ndyakov e844cc0
fix(csc): snapshot miss wire form at enqueue
ndyakov eb220c9
fix(csc): harden FD session recycle and probe paths
ndyakov 8771aa7
fix(csc): epoch inval drop; refresh ownership; close order
ndyakov 911bf54
fix(csc): serialize inval apply with drop; FD handler adapter
ndyakov 37a130c
fix(csc): retry-uncached drains; refresh stack; weak close
ndyakov d97d831
fix(csc): write-side backstop; probe gate; clone bypass
ndyakov 95ea487
fix(csc): honor lifetime jitter in the FD recycle age
ndyakov e802dc7
fix(csc): fail the FD session on push-processor errors
ndyakov ce21ffb
feat(csc): native OTel attribution for coalesced misses
ndyakov File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| package redis | ||
|
|
||
| import ( | ||
| "testing" | ||
| "time" | ||
| ) | ||
|
|
||
| // TestCSCCoalesceModeRejectsPinnedPublicly pins that the buggy "pinned" PROTOTYPE | ||
| // engine (no idle invalidation drain; can serve stale) is NOT selectable from the | ||
| // public ClientSideCacheCoalesceMode option — it falls back to "workers" — while | ||
| // the internal benchmark hook can still force it. | ||
| func TestCSCCoalesceModeRejectsPinnedPublicly(t *testing.T) { | ||
| if got := cscCoalesceMode(&Options{ClientSideCacheCoalesceMode: "pinned"}); got != "workers" { | ||
| t.Fatalf("public \"pinned\" => %q, want \"workers\"", got) | ||
| } | ||
| if got := cscCoalesceMode(&Options{ClientSideCacheCoalesceMode: "fullduplex"}); got != "fullduplex" { | ||
| t.Fatalf("\"fullduplex\" => %q, want \"fullduplex\"", got) | ||
| } | ||
| if got := cscCoalesceMode(&Options{ClientSideCacheCoalesceMode: ""}); got != "workers" { | ||
| t.Fatalf("\"\" => %q, want \"workers\"", got) | ||
| } | ||
| if got := cscCoalesceMode(nil); got != "workers" { | ||
| t.Fatalf("nil opt => %q, want \"workers\"", got) | ||
| } | ||
|
|
||
| cscForcePinned = true | ||
| defer func() { cscForcePinned = false }() | ||
| if got := cscCoalesceMode(&Options{ClientSideCacheCoalesceMode: "pinned"}); got != "pinned" { | ||
| t.Fatalf("forced \"pinned\" => %q, want \"pinned\" (benchmark hook)", got) | ||
| } | ||
| } | ||
|
|
||
| // TestUniversalOptionsSimpleCopiesCSCCoalesce guards that the new CSC miss- | ||
| // coalescing / invalidation-batching knobs reach a standalone Client through | ||
| // UniversalOptions.Simple() (they were previously Options-only, so UniversalClient | ||
| // users could not enable them). | ||
| func TestUniversalOptionsSimpleCopiesCSCCoalesce(t *testing.T) { | ||
| u := &UniversalOptions{ | ||
| ClientSideCacheRefreshOnInvalidate: true, | ||
| ClientSideCacheCoalesceMisses: true, | ||
| ClientSideCacheCoalesceMode: "fullduplex", | ||
| ClientSideCacheCoalesceWorkers: 5, | ||
| ClientSideCacheInvalidationBatchWindow: 7 * time.Millisecond, | ||
| } | ||
| o := u.Simple() | ||
| if !o.ClientSideCacheRefreshOnInvalidate { | ||
| t.Error("Simple() dropped ClientSideCacheRefreshOnInvalidate") | ||
| } | ||
| if !o.ClientSideCacheCoalesceMisses { | ||
| t.Error("Simple() dropped ClientSideCacheCoalesceMisses") | ||
| } | ||
| if o.ClientSideCacheCoalesceMode != "fullduplex" { | ||
| t.Errorf("Simple() ClientSideCacheCoalesceMode = %q, want \"fullduplex\"", o.ClientSideCacheCoalesceMode) | ||
| } | ||
| if o.ClientSideCacheCoalesceWorkers != 5 { | ||
| t.Errorf("Simple() ClientSideCacheCoalesceWorkers = %d, want 5", o.ClientSideCacheCoalesceWorkers) | ||
| } | ||
| if o.ClientSideCacheInvalidationBatchWindow != 7*time.Millisecond { | ||
| t.Errorf("Simple() ClientSideCacheInvalidationBatchWindow = %v, want 7ms", o.ClientSideCacheInvalidationBatchWindow) | ||
| } | ||
| } |
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| package redis | ||
|
|
||
| import ( | ||
| "sync" | ||
| "time" | ||
| ) | ||
|
|
||
| // Windowed background invalidation batcher. | ||
| // | ||
| // Normally the invalidate handler deletes cache entries INLINE, on whatever | ||
| // goroutine read the RESP3 "invalidate" push — which, for the coalescer's | ||
| // reply reader, means invalidation work steals time from reading miss replies | ||
| // and inflates the low-concurrency churn p99 tail. | ||
| // | ||
| // When Options.ClientSideCacheInvalidationBatchWindow>0 the handler instead | ||
| // ENQUEUES invalidated keys (cheap, off the read path) and a single background | ||
| // goroutine applies the deletes in batches once per window. Deferring an | ||
| // invalidation's application by <= window means a reader may see the | ||
| // pre-invalidation value for <= window, which is exactly what MaxStaleness=window | ||
| // already licenses; set the window <= MaxStaleness to stay within contract (a | ||
| // nonzero window with MaxStaleness=0 is an explicit strictness relaxation). | ||
|
|
||
| const cscInvalBatchMax = 4096 // size-cap flush regardless of the timer | ||
|
|
||
| type cscInvalBatcher struct { | ||
| h *invalidateHandler | ||
| window time.Duration | ||
| ch chan string | ||
| stopCh chan struct{} | ||
| stopOnce sync.Once | ||
| } | ||
|
|
||
| // stop signals run() to flush and exit; idempotent. It only closes a channel — | ||
| // it never touches h.mu and does not wait for the goroutine — so it is safe to | ||
| // call while holding the handler lock (see releaseLocked). | ||
| func (b *cscInvalBatcher) stop() { | ||
| b.stopOnce.Do(func() { close(b.stopCh) }) | ||
| } | ||
|
|
||
| // enqueue hands a namespaced key to the batcher without blocking the caller. On | ||
| // a full queue it applies the delete inline so an invalidation is never dropped. | ||
| func (b *cscInvalBatcher) enqueue(nsKey string) { | ||
| select { | ||
| case b.ch <- nsKey: | ||
| default: | ||
| b.apply([]string{nsKey}) | ||
| } | ||
| } | ||
|
ndyakov marked this conversation as resolved.
|
||
|
|
||
| // apply deletes the given namespaced keys and feeds any evicted-hot entries to | ||
| // the refresher. Reads the handler binding under its lock, same as the inline path. | ||
| func (b *cscInvalBatcher) apply(keys []string) { | ||
| h := b.h | ||
| h.mu.RLock() | ||
| cache, refresh := h.cache, h.refresh | ||
| h.mu.RUnlock() | ||
| if cache == nil { | ||
| return | ||
| } | ||
| lc, canRefresh := cache.(*LocalCache) | ||
| canRefresh = canRefresh && refresh != nil | ||
| var hot []cscRefreshTarget | ||
| for _, k := range keys { | ||
| if !canRefresh { | ||
| cache.DeleteByRedisKey(k) | ||
| continue | ||
| } | ||
| hot = lc.deleteByRedisKeyCollectingHot(k, refresh.sinceToken.Load(), hot[:0]) | ||
|
ndyakov marked this conversation as resolved.
|
||
| for i := range hot { | ||
| refresh.offer(hot[i]) | ||
| } | ||
| } | ||
|
ndyakov marked this conversation as resolved.
ndyakov marked this conversation as resolved.
|
||
| } | ||
|
|
||
| func (b *cscInvalBatcher) run() { | ||
| t := time.NewTimer(b.window) | ||
| defer t.Stop() | ||
| pending := make([]string, 0, 256) | ||
| seen := make(map[string]struct{}, 256) | ||
| flush := func() { | ||
| if len(pending) == 0 { | ||
| return | ||
| } | ||
| b.apply(pending) | ||
| pending = pending[:0] | ||
| for k := range seen { | ||
| delete(seen, k) | ||
| } | ||
| } | ||
| for { | ||
| select { | ||
| case <-b.stopCh: | ||
| // Last user released the binding: flush what is pending (a no-op once | ||
| // the cache is nil) and exit so the goroutine does not live on | ||
| // re-arming the timer forever. | ||
| flush() | ||
| return | ||
|
ndyakov marked this conversation as resolved.
|
||
| case k := <-b.ch: | ||
| if _, dup := seen[k]; !dup { | ||
| seen[k] = struct{}{} | ||
| pending = append(pending, k) | ||
| } | ||
|
ndyakov marked this conversation as resolved.
Outdated
|
||
| if len(pending) >= cscInvalBatchMax { | ||
| flush() | ||
| t.Reset(b.window) | ||
| } | ||
| case <-t.C: | ||
| flush() | ||
| t.Reset(b.window) | ||
| } | ||
|
ndyakov marked this conversation as resolved.
|
||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.