Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
829 commits
Select commit Hold shift + click to select a range
0be8396
D2.5: keyspace subscriptions move too, and the ownership flag gets it…
mgravell Oct 1, 2026
8376f5c
Toward one engine: the server-scoped GET moves, and a test-only build…
mgravell Oct 1, 2026
9a09341
D2.5: sharded and key-routed subscriptions follow their redirects, on…
mgravell Oct 1, 2026
00718f9
D2.5 done: IdentifyEndpoint moves, and the channel prefix joins the slot
mgravell Oct 1, 2026
39ca13f
D2.5 notes: stopping the subscription bridge is the next gate, and he…
mgravell Oct 1, 2026
fdc4d11
Merge remote-tracking branch 'origin/main' into marc/v4-core-operation
mgravell Oct 1, 2026
0457619
Absorb main, with the engine-flag counterparts both of its commits ne…
mgravell Oct 1, 2026
dd08f73
Subscriptions: an in-flight subscribe is handled, not absent
mgravell Oct 1, 2026
9d1942e
Subscriptions: ask at the write whether this one shares the connection
mgravell Oct 1, 2026
32d0362
Subscriptions: dial a subscription socket on downgrade, and only when…
mgravell Oct 1, 2026
dccff60
Subscriptions: this core now takes every channel shape
mgravell Oct 1, 2026
86e9654
Stop constructing the subscription bridge under the engine flag
mgravell Oct 1, 2026
f92f35b
Keep the configuration channel when the subscription bridge goes
mgravell Oct 1, 2026
021bd7a
Subscriptions: three cluster fixes the retired decline exposed
mgravell Oct 1, 2026
81a43e3
Subscriptions: act on a stranded shard channel the way the shipped co…
mgravell Oct 1, 2026
e4a6bf7
Carry the configuration channel under RESP2, and hold the multiplexer…
mgravell Oct 2, 2026
6f24486
Stop a new-core connection from rooting the multiplexer
mgravell Oct 2, 2026
d065679
Keep the eager-connect precondition honest about the configuration ch…
mgravell Oct 2, 2026
2dac224
Let the subscription registry agree whose subscription it is
mgravell Oct 2, 2026
829c816
Bound the subscription in-flight window
mgravell Oct 2, 2026
70b8633
Don't route a slot to an owner this core knows it cannot reach
mgravell Oct 2, 2026
afc7657
Assert the stronger idleness property where the premise no longer holds
mgravell Oct 2, 2026
ad7a3f1
Record that the flagged path is now noisy rather than specifically br…
mgravell Oct 2, 2026
76a969d
Record a third wedge sighting, and rule out a damaged test topology
mgravell Oct 2, 2026
a1169c2
Correct the record: D2.8's discovery gate is not closed, and itemise …
mgravell Oct 2, 2026
bb3dca5
Learn the product variant from this core's handshake
mgravell Oct 2, 2026
60ca051
Measure the coupled move: the blocker is ReconfigureAsync's connect v…
mgravell Oct 2, 2026
719d602
Send the availability tracer on the core that carries the commands
mgravell Oct 2, 2026
c61f532
Census: 88% of Message traffic is off the old core, and the connect p…
mgravell Oct 3, 2026
72172f9
The connect path is solved: 15,081ms to 72ms
mgravell Oct 3, 2026
0d5e9bc
Learn the server mode from INFO as well as HELLO
mgravell Oct 3, 2026
c829fc4
Tolerate a refused AUTH, and read the tie-breaker on this core's hand…
mgravell Oct 3, 2026
a913905
Learn the replication role from this core's handshake
mgravell Oct 3, 2026
ee63f7d
Keep this core's connections alive, and let the surface name them
mgravell Oct 3, 2026
44a7b78
Size the coupled move honestly: 126, and maintenance is 56% of it
mgravell Oct 3, 2026
44c607d
Ask for maintenance notifications from this core's handshake
mgravell Oct 3, 2026
65188ba
Share the maintenance notification parse between both cores
mgravell Oct 3, 2026
991668a
Settle maintenance notifications once the protocol is known
mgravell Oct 3, 2026
841b8bb
Recycle this core's connections on a maintenance handoff
mgravell Oct 3, 2026
3ef002b
Wire this core's push dispatcher before anything that provokes a push
mgravell Oct 3, 2026
5439299
Re-measure the coupled move: 126 to 59, and the shape has changed
mgravell Oct 4, 2026
67bf280
Split the exception detail assertions by which core measured it
mgravell Oct 4, 2026
979dc87
Issue HELLO on RESP2 too, and let it carry credentials when AUTH cannot
mgravell Oct 4, 2026
b7820f9
Refuse placeholder endpoints in the slot map, and probe for the role …
mgravell Oct 4, 2026
a103e68
Move CLIENT KILL onto the RESP context, beside CLIENT LIST
mgravell Oct 4, 2026
f34fcc6
Publish this core's topology findings, and share one CLUSTER SLOTS pa…
mgravell Oct 4, 2026
b3d9a9d
Notice, retry and announce a connection this core has lost
mgravell Oct 4, 2026
a1a8e21
Record 9af-9ag: the refresh scope, the failure-detection group, and o…
mgravell Oct 4, 2026
8c1db08
Record 9ah: the dial belongs in ActivateServer
mgravell Oct 4, 2026
94d6fdb
Make a failed connect explain itself, and let this core narrate its o…
mgravell Oct 5, 2026
d3c64da
Record 9ai: triaging the last fourteen
mgravell Oct 5, 2026
f207a80
Convert a NOAUTH reply into the connection failure it is
mgravell Oct 5, 2026
7ef5b23
Record 9ai: triaging the last fourteen, and where the auth conversion…
mgravell Oct 5, 2026
f7f3640
Delete the redundant heartbeat retry, and correct the case made again…
mgravell Oct 5, 2026
0ff78a7
Add the V4 alpha plan: goal, three gates, ordered work queue, operati…
mgravell Oct 5, 2026
3941e4f
Never let a confirmation push answer an unrelated command
mgravell Oct 5, 2026
0ddb0da
Stop a redirect to a dead node from driving a reconfiguration storm
mgravell Oct 5, 2026
0ad2788
Match a subscription reply to the command waiting for it, not to a guess
mgravell Oct 5, 2026
b005602
Retract the RESP2-negotiation finding: two interleaved runs, not a cl…
mgravell Oct 5, 2026
ac25fd7
Land the coupled patch: it is now the engine-flag behaviour
mgravell Oct 5, 2026
cc8aaa4
Port the connect log to the new core under the shipped event ids
mgravell Oct 5, 2026
836f138
Move the remaining non-Sentinel IServer commands off Message
mgravell Oct 5, 2026
b3a5bba
Plan: gate 2 done; gate 3 re-probed at 221 sites, and the flip must p…
mgravell Oct 5, 2026
c12228e
Clear the engine-flag triage list: Sentinel, retry policy, a phantom …
mgravell Oct 5, 2026
72d63e8
Name the head-of-line command in new-core timeout reports
mgravell Oct 5, 2026
c7a6e76
Stop the engine flag building shipped bridges: 179 per run to 20, all…
mgravell Oct 5, 2026
87dd9e8
Make the new core the default
mgravell Oct 5, 2026
874fe2f
Plan: backlog the trusted-callback completion experiment (Respire hyp…
mgravell Oct 5, 2026
c093f37
Remove the off-switch: the new core is unconditional
mgravell Oct 5, 2026
9a65982
Move shared helpers out of the old core's files ahead of deleting them
mgravell Oct 5, 2026
0bd36a1
Remove the Message fallbacks from pub/sub, and the small dead flag br…
mgravell Oct 5, 2026
50e88c5
Delete the old core
mgravell Oct 5, 2026
2b5ebfa
Retarget the parked parser-test harness at IRespHandler, and drop the…
mgravell Oct 5, 2026
93ae50a
Retarget the stream, scan, geo, vector-set, LMOVEM and LCS parser tes…
mgravell Oct 5, 2026
57ebcd8
Retarget the diagnostics, sentinel, condition and handshake parser te…
mgravell Oct 5, 2026
8af8e04
Stream handlers reject malformed replies again; nil VLINKS is an empt…
mgravell Oct 5, 2026
22eb8d3
Give RESPite.Tests and Build.Tests the GitHubActions test logger CI a…
mgravell Oct 5, 2026
c3d24d6
Retarget the scalar/aggregate/lease parser tests at the new-core hand…
mgravell Oct 5, 2026
e758f7a
TYPE reads nil as None, and a scalar reply to an array command as one…
mgravell Oct 5, 2026
31b9a1e
Retarget the parked round-trip, retry-category and optional-arg tests…
mgravell Oct 5, 2026
3331299
COPY REPLACE and explicit-id XREADGROUP get their retry categories ag…
mgravell Oct 5, 2026
46e1f59
Delete ResultBox: ConditionResult created one that nothing completed …
mgravell Oct 5, 2026
06d88f1
Rewrite two-core comments for the single-core world
mgravell Oct 5, 2026
5fba290
Remove code that only reconciled two cores
mgravell Oct 5, 2026
ef89317
Pin that a dropped multiplexer's in-flight work still times out
mgravell Oct 5, 2026
1db33c1
Write the storm log again, from the new core's pending queue
mgravell Oct 5, 2026
36dd2ed
Count fire-and-forget commands again for the multiplexer's status
mgravell Oct 5, 2026
b5f2533
Keep the circular op-count snapshot again for each endpoint's profile
mgravell Oct 5, 2026
241bc58
Plan: the deletion's diagnostic gaps are ported
mgravell Oct 5, 2026
735f924
CI: name and kill a hung test instead of waiting out the six-hour job…
mgravell Oct 5, 2026
4fc459c
BufferedStreamWriterTests: bound the waits for a blocked write or flush
mgravell Oct 5, 2026
c242056
SelectInjectionTests: wait for the flush as well as the bytes before …
mgravell Oct 5, 2026
33f2271
RetirementUnderMaintenanceTests: poll for the subscription to leave t…
mgravell Oct 5, 2026
0c70b4f
CI: 30-minute backstop on the Windows test step, now that a healthy r…
mgravell Oct 5, 2026
9143b41
Close two socket leaks: the RESP2 subscription socket on dispose, and…
mgravell Oct 5, 2026
058e6d6
Tests: poll for the script-loaded belief, and take the best of severa…
mgravell Oct 5, 2026
93b2524
IsConnected means the interactive connection, as it did in v3
mgravell Oct 5, 2026
89aeb87
SubscribeToWrongServerAsync: read a non-sharded placement before the …
mgravell Oct 5, 2026
f90b68c
SweepIfDueHonoursTheInterval: assert 'not due' only while it is still…
mgravell Oct 5, 2026
169c7c4
GetClients: wait for the background-dialled subscription socket befor…
mgravell Oct 5, 2026
d5b6a72
Log a pub/sub delivery that matches no subscription, and show it in t…
mgravell Oct 5, 2026
331ef7e
Tests: an exact-size pool for the page-boundary writer tests; wait fo…
mgravell Oct 5, 2026
91bfcbd
Plan: first fully green CI with the old core deleted
mgravell Oct 5, 2026
db3e484
ClusterPubSub: wait for the queue's consumer before reading what it c…
mgravell Oct 5, 2026
50ab20e
Fix two real bugs found chasing flakes: stale database on a pooled op…
mgravell Oct 5, 2026
5babc7b
Tests that CLIENT PAUSE the server wait out their own pause
mgravell Oct 5, 2026
4247cf6
Port DedicatedThreads to the new core, and honour WriteMode and Reque…
mgravell Oct 5, 2026
9194995
Docs and plan: what DedicatedThreads does and does not yet do in the …
mgravell Oct 5, 2026
9607aaf
Wake blocked synchronous callers without the thread-pool: the waiting…
mgravell Oct 5, 2026
b5e7a07
RespEndToEndTests: poll for the script-loaded belief, as ScriptLoadPa…
mgravell Oct 6, 2026
dc4ea32
Plan: CI green with DedicatedThreads and the sync pump
mgravell Oct 6, 2026
ad081e5
Remove the TransitionalDatabase fallback, and give the round-trip tes…
mgravell Oct 6, 2026
2ca83a9
Context-surface batch and transaction: RespBatch/RespTransaction (SER…
mgravell Oct 6, 2026
6ee006c
v4 branch: CI on pushes to v4, and restart the 4.0 alpha patch number…
mgravell Oct 6, 2026
990dccd
Preserve binary members as VRANGE bounds (from main's #3256, reimplem…
mgravell Oct 6, 2026
c176699
Merge main into v4: VectorSetRange binary bounds (#3256)
mgravell Oct 6, 2026
76309ac
design: a short v4 ledger of pending work and open decisions; the alp…
mgravell Oct 6, 2026
d45eadf
Announce an endpoint as connected only after the connection is published
mgravell Oct 6, 2026
6bd74a6
ledger: the net481 GetServer failure, and batch buffer packing (cross…
mgravell Oct 6, 2026
6889922
ledger: unobserved faults on the transitional Task surface
mgravell Oct 6, 2026
c9b2a21
Give a synchronous wait a backstop, rather than trusting the executor…
mgravell Oct 6, 2026
96f3e66
Mark a faulted transitional task observed as it faults, as v3 did
mgravell Oct 6, 2026
cacab8c
Set an awaiter's flags before registering it, and only on its own life
mgravell Oct 6, 2026
24a4838
Carry the frame scan across receives instead of rescanning from the f…
mgravell Oct 6, 2026
05294e3
Drive Drain through RespFrameScanner, as the scanner is meant to be d…
mgravell Oct 6, 2026
ddcfdea
Treat the sync pump's 'done' as a hint: a call that gave up can no lo…
mgravell Oct 6, 2026
752f481
Keep the preamble-pair stress test as a long-running reproduction; re…
mgravell Oct 6, 2026
4c63de7
ledger: an age of zero means the timeout was built for a recycled ope…
mgravell Oct 6, 2026
7aa7f81
Act through the pending queue only on the life that was written
mgravell Oct 6, 2026
846355a
Never let an inline completion leak to the read loop: the '~5s nothin…
mgravell Oct 6, 2026
fe647da
Write a preamble pair to its own database, with its own SELECT
mgravell Oct 6, 2026
26b7f6f
ledger: alternative client-side-cache invalidation sources, and opt-i…
mgravell Oct 6, 2026
220d480
Retire the RespTaskBatch spike, and the IRespRunExecutor it alone used
mgravell Oct 6, 2026
0895fa2
TheObjectFamilyAgreesWithTheOldPath: compare idle times within a seco…
mgravell Oct 6, 2026
f114891
ledger: status
mgravell Oct 6, 2026
de30137
Reset a CycleBuffer segment's start trim when it is recycled
mgravell Oct 6, 2026
3cc99f6
Split socket fill from parsing in the async reader (PR #3251's intent…
mgravell Oct 6, 2026
7582744
Release workflow: upload the packages from the .nupkgs dot-folder
mgravell Oct 6, 2026
cdc52bf
ledger: client-side cache across a geo/active-active failover
mgravell Oct 6, 2026
cdfcbf7
ConfigTests.BeforeSocketConnect: wait for the background subscription…
mgravell Oct 7, 2026
2fb5542
Pin that the RESP2 subscription socket is lazy without a config channel
mgravell Oct 7, 2026
c5ee9fc
Reinstate v3's lost log events, and the dead-socket recovery event 90…
mgravell Oct 7, 2026
a1673a9
Name the v4 types for what they do, not how we got there
mgravell Oct 7, 2026
7769cf4
ledger: retry overload defaults, stale retry comments, executor decor…
mgravell Oct 7, 2026
e384142
ledger: move the retry follow-ups to the backlog; add the 'new core' …
mgravell Oct 7, 2026
af05cd6
ledger: Respire's completion mechanism, confirmed from source
mgravell Oct 7, 2026
0907e41
ledger: the work league measures the cost of Respire's inline completion
mgravell Oct 7, 2026
a0175a6
ledger: docs to prefer the new API; package book-keeping for the benc…
mgravell Oct 7, 2026
8fa02f6
Package book-keeping: v3 is the central baseline, and RESPite.Benchma…
mgravell Oct 7, 2026
4dd6dd7
ContextApiBenchmark: the caller is an IRespKeyspaceTarget
mgravell Oct 7, 2026
a4c8a28
Docs lead with the grouped API
mgravell Oct 7, 2026
da13525
Group receivers by value, and the generic accessors build their group…
mgravell Oct 7, 2026
dcbf763
RespHashImportProbeTests: warm the connection, and say what the gate …
mgravell Oct 7, 2026
ef8564d
IBatch and ITransaction can be executed again, as they always could
mgravell Oct 7, 2026
cd0da6e
ledger D7: the RESP3 config-channel subscribe came from main (#3255, …
mgravell Oct 7, 2026
aa5f3d8
Fewer objects per send: a typed operation, a shared request buffer, a…
mgravell Oct 7, 2026
d5081f2
Experimental CombineWrites flag: a busy write lock hands the command …
mgravell Oct 7, 2026
9d77f0f
One writer-lock hold per command, and a pool that gives up after a fe…
mgravell Oct 7, 2026
1ee9682
OperationBackstop off by default; the test suite turns it on
mgravell Oct 7, 2026
cec3794
Fold the request into the operation: small requests are copied, and m…
mgravell Oct 7, 2026
a80cfe2
Void sends return the operation too, not an async adapter over it
mgravell Oct 7, 2026
a244057
A slimmer operation: 306 -> 266 bytes, and one clock read per command
mgravell Oct 7, 2026
66eb0c1
A connection the server closes is reported and recovered from
mgravell Oct 7, 2026
2b8666d
IDatabase task bridge: register on our own operation with a static ca…
mgravell Oct 7, 2026
41afb04
Split read loop: a single-waiter semaphore for the filler/parser hand…
mgravell Oct 7, 2026
cddee74
ledger: remaining performance ideas, and what warm-up turned out to be
mgravell Oct 7, 2026
0b1cfc1
IDatabase task bridge: the operation's own promise task, without a co…
mgravell Oct 7, 2026
6cfb132
Typed batches, and a completion that pulses only for a real waiter
mgravell Oct 7, 2026
11ab4ec
Fix a lost batch completion: recycle only after the last request refe…
mgravell Oct 7, 2026
0edf3e1
ledger: batch-mode profile; gathered sends and inline batch attach tr…
mgravell Oct 7, 2026
03bbb05
A per-thread cache in front of the operation pool
mgravell Oct 7, 2026
d6f9ba0
IDatabase tasks complete through the operation itself, not a pool wra…
mgravell Oct 7, 2026
dbf5d12
Batched commands parse as their replies arrive, not when awaited
mgravell Oct 7, 2026
ff5df60
An adaptive parser spin: halve on parking, reset on success
mgravell Oct 8, 2026
e9218be
Adaptive spin: a floor of 32 polls, not 4
mgravell Oct 8, 2026
41d1abc
Queued operations act through the token they were queued with
mgravell Oct 8, 2026
2a30ea4
Parser spin: 40 polls at full, decayed by how long a parked wait took
mgravell Oct 8, 2026
e184cfa
ledger: backlog - test dropping the non-generic group accessors for t…
mgravell Oct 8, 2026
7c5af99
Experimental InlineSends: a caller that finds the writer idle sends i…
mgravell Oct 8, 2026
c7f21ee
InlineSends: only for a request alone in flight
mgravell Oct 8, 2026
014ed4b
InlineSends: the alone check on every send path
mgravell Oct 8, 2026
48783cf
ledger: InlineSends status and the open default question
mgravell Oct 8, 2026
6de99ad
InlineSends on by default
mgravell Oct 8, 2026
bf5ff42
Merge main into v4: maintenance notifications auto-enlist and leave S…
mgravell Oct 8, 2026
87fc1d0
Docs: a page on command groups, and building a command in pieces with…
mgravell Oct 8, 2026
17b3936
Synchronous() contexts: a supported sync path for the command groups
mgravell Oct 8, 2026
b87d2a3
One accessor per command group: the contexts are targets
mgravell Oct 8, 2026
286170a
Rename Synchronous() to Blocking()
mgravell Oct 9, 2026
1b82353
StreamConsumerGroupAutoClaim_MultiStream: count the delivery a slow s…
mgravell Oct 9, 2026
70ce510
IDatabase sync onto Blocking(); paired commands block too; retire Syn…
mgravell Oct 9, 2026
dbb38f8
DedicatedThreads: connect and handshake synchronously, so the socket …
mgravell Oct 9, 2026
e1532a2
PublishWhenSettledAsync: cancel the settle timer once settling wins
mgravell Oct 9, 2026
f30f204
SyncCompletionTests: bound the dedicated-connection pool items at a q…
mgravell Oct 9, 2026
0ef6441
ledger: D2 approved, D3 reflects the cut alpha
mgravell Oct 9, 2026
d9e0d0c
SyncCompletionTests: assert the absolute pool-item bound on Linux only
mgravell Oct 9, 2026
5a19845
Retrying contexts block too: synchronous retry in RespRetryExecutor
mgravell Oct 9, 2026
bfc4aaa
Contexts expose their multiplexer; WithRetry on a context resolves th…
mgravell Oct 9, 2026
804da1c
Unflake two recurring tests: HashImport probe isolation, lease pool i…
mgravell Oct 9, 2026
725137a
ledger: watch item for the one CI 'nothing inbound' timeout in RespCa…
mgravell Oct 9, 2026
69c6384
Public task bridge for extenders: IDatabaseAsync.AsTask(ValueTask) ex…
mgravell Oct 9, 2026
c5eba6c
Client-side cache: never answer a read inside a transaction; VRANDMEM…
mgravell Oct 9, 2026
004a354
Docs: a page on client-side caching
mgravell Oct 9, 2026
9f00514
ledger: the WithRetry default mismatch closed in bfc4aaa8
mgravell Oct 9, 2026
4e0eef1
Client-side cache: never cache reads that drift without a write
mgravell Oct 9, 2026
55a2565
Docs, client-side caching: INFO commandstats as the per-command check…
mgravell Oct 9, 2026
e8b337c
Docs, extending: ship a down-level accessor, the signature convention…
mgravell Oct 9, 2026
c57ec7e
Emulated scans honour WithCancellation
mgravell Oct 9, 2026
bc5bce3
[Resp] declares commands too: partial RespCommand properties, generated
mgravell Oct 9, 2026
48f31cb
ledger: BacklogTests.TotalOutstandingIncludesBacklogQueue is a pre-ex…
mgravell Oct 9, 2026
c15f12c
Merge main (3.4.0) into v4
mgravell Oct 9, 2026
09bbeaf
Client-side caching: the grace period honours TimeToLive and WithMaxC…
mgravell Oct 9, 2026
f8915bb
ConnectionAttemptCompleted: sequence and timestamp agree on order; un…
mgravell Oct 9, 2026
74c9a52
Issue922_ReconnectRaised: wait for the subscription leg before listen…
mgravell Oct 9, 2026
c537026
Cache hits and blocking sends no longer route to ask a constant
mgravell Oct 9, 2026
7e34322
Benchmarks: take the cache-hit path apart, and measure how it scales
mgravell Oct 9, 2026
7363d6e
Benchmarks: c8 parses through the executor's own Parse; c1's buffer f…
mgravell Oct 9, 2026
04ef29d
Client-side cache: entries own GC arrays, so a hit writes nothing shared
mgravell Oct 10, 2026
0023c9e
Client-side cache: entry ages on a coarse clock
mgravell Oct 10, 2026
6548bff
Benchmarks: split the group layer - the accessor is free; the gap is …
mgravell Oct 10, 2026
8a665ff
ledger: cache hit-path performance - paused for a RespFest re-run aga…
mgravell Oct 10, 2026
28f6213
Benchmarks: cache-scaling miss - RespFest's cache-miss-conc64 as a lo…
mgravell Oct 10, 2026
5686303
Benchmarks: cache-scaling miss builds on net481, and measures misses
mgravell Oct 10, 2026
1bd841e
Client-side cache: replies live in slabs, so a miss stops feeding the GC
mgravell Oct 10, 2026
6ab3683
ledger: slabs restore the cache miss path (1bd841e6)
mgravell Oct 10, 2026
a5af2de
Client-side cache: the miss path - fix an unbounded eviction queue, a…
mgravell Oct 10, 2026
a732552
Client-side cache: CacheOptions.Admission - store a reply only on a r…
mgravell Oct 10, 2026
93c415e
ledger: cache performance - what shipped, and the ideas still to explore
mgravell Oct 10, 2026
20b8a3d
Client-side cache: MaxBytes charges each entry's bookkeeping, so it b…
mgravell Oct 10, 2026
f3f9c95
Client-side cache: the cache key lives in the slab, beside its reply
mgravell Oct 10, 2026
046f7a8
Client-side cache: a single dependency is held inline in the entry
mgravell Oct 10, 2026
cc95aee
Benchmarks: cache-scaling zipf - a skewed workload, with the hit rate
mgravell Oct 10, 2026
1701c0d
ledger: Zipf settles admission on the numbers; the default is Marc's …
mgravell Oct 10, 2026
d59d4e2
PFCOUNT is a read: pinned retryable and cacheable; the design notes c…
mgravell Oct 10, 2026
cc2c362
Client-side cache: admission on a repeated miss is the default
mgravell Oct 10, 2026
40ca69e
Scans hand their cancellation token to the page in flight; CanCancel …
mgravell Oct 10, 2026
44da418
ledger: CanWritePreamble is the last routing capability query; route-…
mgravell Oct 10, 2026
8a027bf
Byte hash: mix every bit; the doorkeeper no longer sees every request…
mgravell Oct 10, 2026
a22ba16
Doorkeeper: every miss ages it, not only first sightings
mgravell Oct 12, 2026
ea92c29
ledger: RespFest confirms the doorkeeper fix - the 1M and 16M tables …
mgravell Oct 12, 2026
5d4a498
Merge main into v4: #3270 (v4 had the fix already; main's comment wor…
mgravell Oct 12, 2026
2b05b06
ledger: two CI flakes recorded - the Sentinel recovery test, and the …
mgravell Oct 12, 2026
378a103
design: client-side-cache.md - the cache as built, why, and what was …
mgravell Oct 12, 2026
4c206f3
design: command-flow.md brought up to date - cache step, retry execut…
mgravell Oct 12, 2026
86a988b
design: client-side-cache.md - single-flight is for asynchronous call…
mgravell Oct 12, 2026
a784c71
ledger: D1 and D2 closed (the split reader is the default; sync conne…
mgravell Oct 12, 2026
746a065
Group cache: our own writes stamp every member's cache, so a failback…
mgravell Oct 12, 2026
97ec53a
Synchronous Send<T> over a pre-built frame defaults its handler, as i…
mgravell Oct 12, 2026
5667da9
docs: Basics.md mentions cancellation tokens, and points to AsyncTime…
mgravell Oct 12, 2026
cc327de
docs: Basics.md - cancellation is about the caller no longer caring, …
mgravell Oct 12, 2026
3cf425d
docs: cancellation from the host - ASP.NET Core's RequestAborted and …
mgravell Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
27 changes: 27 additions & 0 deletions .github/actions/run-tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,33 @@ runs:
--logger trx
--logger "GitHubActions;summary-include-passed=false;summary-include-skipped=false"
--results-directory ./test-results/
--blame-hang-timeout 10m
--blame-hang-dump-type none
/p:CI=true

# Neither of these needs a server, and between them they took about four seconds - but nothing ran
# them until now, so a namespace rename silently broke all 18 SER309 tests and nobody found out.
- name: RESPite.Tests
shell: bash
run: >-
dotnet test tests/RESPite.Tests/RESPite.Tests.csproj
--no-build
-c Release
-f net10.0
--logger trx
--logger "GitHubActions;summary-include-passed=false;summary-include-skipped=false"
--results-directory ./test-results/
/p:CI=true

- name: StackExchange.Redis.Build.Tests
shell: bash
run: >-
dotnet test tests/StackExchange.Redis.Build.Tests/StackExchange.Redis.Build.Tests.csproj
--no-build
-c Release
--logger trx
--logger "GitHubActions;summary-include-passed=false;summary-include-skipped=false"
--results-directory ./test-results/
/p:CI=true

- uses: dorny/test-reporter@v3
Expand Down
78 changes: 67 additions & 11 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ run-name: "CI${{ github.event.inputs.client-libs-test-image-tag != '' && format(
on:
pull_request:
push:
branches: [ 'main', 'v3' ]
branches: [ 'main', 'v3', 'v4' ]
paths:
- '**'
- '!/docs/*' # Don't run workflow when files are only in the /docs directory
Expand Down Expand Up @@ -145,23 +145,33 @@ jobs:
TOPOLOGY='${{ github.event.inputs.windows-topology || 'full' }}'
echo "topology: $TOPOLOGY"

redis-server ../Basic/primary-6379.conf &
redis-server ../Basic/replica-6380.conf &
redis-server ../Basic/secure-6381.conf &
redis-server ../Failover/primary-6382.conf &
redis-server ../Failover/replica-6383.conf &
# Replicas load the initial sync from the socket, never from disk. These data directories
# sit on the Windows mount (/mnt/...), where a replica receives the RDB to a file and then
# fails to load it ("Failed trying to load the MASTER synchronization DB from disk"), retrying
# forever - so no replica on this job had ever synced. That went unnoticed while the client
# read replicas from CLUSTER NODES, which lists them regardless; CLUSTER SLOTS rightly leaves
# out a replica that has never synced, so the new core saw a cluster with no replicas.
LOAD="--repl-diskless-load swapdb"

redis-server ../Basic/primary-6379.conf $LOAD &
redis-server ../Basic/replica-6380.conf $LOAD &
redis-server ../Basic/secure-6381.conf $LOAD &
redis-server ../Failover/primary-6382.conf $LOAD &
redis-server ../Failover/replica-6383.conf $LOAD &
PORTS="6379 6380 6381 6382 6383"
REPLICAS="6380 6383"

if [ "$TOPOLOGY" = "full" ]; then
for p in 7000 7001 7002 7003 7004 7005; do
redis-server "../Cluster/cluster-$p.conf" --dir ../Cluster &
redis-server "../Cluster/cluster-$p.conf" --dir ../Cluster $LOAD &
done
redis-server ../Sentinel/redis-7010.conf &
redis-server ../Sentinel/redis-7011.conf &
redis-server ../Sentinel/redis-7010.conf $LOAD &
redis-server ../Sentinel/redis-7011.conf $LOAD &
for p in 26379 26380 26381; do
redis-server "../Sentinel/sentinel-$p.conf" --sentinel &
done
PORTS="$PORTS 7000 7001 7002 7003 7004 7005 7010 7011 26379 26380 26381"
REPLICAS="$REPLICAS 7003 7004 7005 7011"
fi

# Poll rather than `sleep 5`: on a fast machine this returns almost immediately, and on a
Expand Down Expand Up @@ -197,11 +207,53 @@ jobs:
exit 1
fi

# ...and a replica that is listening but has never synced is just as misleading, and quieter:
# replica-routing tests then pass or fail on what the client happens to read, not on the
# client. Fatal for the same reason as above.
unsynced=""
for p in $REPLICAS; do
synced=""
for _ in $(seq 1 30); do
if redis-cli -p "$p" INFO replication 2>/dev/null | grep -q "master_link_status:up"; then
synced="yes"; break
fi
sleep 1
done
if [ -n "$synced" ]; then
echo "replica $p synced"
else
echo "replica $p DID NOT SYNC:"
redis-cli -p "$p" INFO replication 2>&1 || true
unsynced="$unsynced $p"
fi
done
if [ -n "$unsynced" ]; then
echo "::error::replicas not synced:$unsynced"
exit 1
fi

# CLUSTER SLOTS lists a replica only once its primary has heard, by gossip, that it has an
# offset - a second or so after the sync above. Wait for all six nodes to be listed.
if [ "$TOPOLOGY" = "full" ]; then
listed=0
for _ in $(seq 1 30); do
listed=$(redis-cli -p 7000 CLUSTER SLOTS 2>/dev/null | grep -c '^700[0-5]$' || true)
[ "$listed" -ge 6 ] && break
sleep 1
done
echo "CLUSTER SLOTS lists $listed of 6 nodes"
if [ "$listed" -lt 6 ]; then
redis-cli -p 7000 CLUSTER NODES 2>&1 || true
echo "::error::CLUSTER SLOTS does not list every replica"
exit 1
fi
fi

- name: .NET Build
run: dotnet build Build.csproj -c Release /p:CI=true
# The analyzer and the props that configures it reach consumers only through the package, and a
# packaging regression is silent: no diagnostics, ever, for anybody, with a build that still succeeds.
# The real pack below runs only on pushes to main/v3, so check it here where PRs will see it too.
# The real pack below runs only on pushes to main/v3/v4, so check it here where PRs will see it too.
- name: Verify package contents
run: |
$out = "${env:GITHUB_WORKSPACE}\.packcheck"
Expand All @@ -222,10 +274,14 @@ jobs:
exit 1
}
- name: StackExchange.Redis.Tests
# A hung test otherwise holds the job until GitHub's six-hour limit, and says nothing about which
# test it was. --blame-hang names it (in the run's Sequence file and the log) and kills it, so the
# rest still report; the step timeout is the backstop if even that does not return.
timeout-minutes: 30
run: |
$exitCode = 0
foreach ($tfm in @("net10.0", "net481")) {
dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj --no-build -c Release -f $tfm --logger trx --logger GitHubActions --results-directory ./test-results/ /p:CI=true
dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj --no-build -c Release -f $tfm --logger trx --logger GitHubActions --results-directory ./test-results/ --blame-hang-timeout 10m --blame-hang-dump-type none /p:CI=true
if ($LASTEXITCODE -ne 0) {
$exitCode = $LASTEXITCODE
}
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ jobs:
with:
name: packages
path: .nupkgs/*.nupkg
include-hidden-files: true # .nupkgs is a dot-folder, which upload-artifact skips by default
if-no-files-found: error # a dry run that uploads nothing should fail, not pass
- name: NuGet login (OIDC to temp API key)
if: github.event_name == 'release'
uses: NuGet/login@v1
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ BenchmarkDotNet.Artifacts/

# local planning/design notes (not for publication)
planning/

# agent worktrees (see .claude/); these are separate checkouts, never repo content
.claude/worktrees/
10 changes: 6 additions & 4 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project>
<Project>
<PropertyGroup>
<VersionPrefix>2.0.0</VersionPrefix>
<Copyright>2014 - $([System.DateTime]::Now.Year) Stack Exchange, Inc.</Copyright>
Expand All @@ -10,9 +10,11 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<CodeAnalysisRuleset>$(MSBuildThisFileDirectory)Shared.ruleset</CodeAnalysisRuleset>
<MSBuildWarningsAsMessages>NETSDK1069</MSBuildWarningsAsMessages>
<!-- SER001/SER002/SER003/SER006/SER007/SER008/SER010 retired (vector sets, Redis 8.4/8.6/8.8/8.10 features,
geo-redundant failover and maintenance notifications, no longer experimental); IDs reserved, see Experiments.cs -->
<NoWarn>$(NoWarn);NU5105;NU1507;SER004;SER005;SER009</NoWarn>
<!-- SER001/SER002/SER003/SER006/SER007/SER008/SER010/SER013 retired (vector sets, Redis 8.4/8.6/8.8/8.10
features, geo-redundant failover, maintenance notifications, and the interpolated writer, no longer
experimental); IDs reserved, see Experiments.cs.
SER011 is deliberately absent: hand-written RespFragment is meant to be seen at every call site. -->
<NoWarn>$(NoWarn);NU5105;NU1507;SER004;SER005;SER009;SER012;SER014</NoWarn>
<PackageReleaseNotes>https://github.com/StackExchange/StackExchange.Redis/releases</PackageReleaseNotes>
<PackageProjectUrl>https://seredis.dev/</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.9.50" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="StackExchange.Redis" Version="2.13.17" />
<!-- the current shipped v3: what the *Baseline projects and RESPite.Benchmark (TargetVer=3) compare against -->
<PackageVersion Include="StackExchange.Redis" Version="3.3.1" />
<PackageVersion Include="StackExchange.Redis.StrongName" Version="1.2.6" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.5" />
Expand Down
131 changes: 131 additions & 0 deletions design/admitsim.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""Single-threaded model of the v4 client cache under RespFest's Zipf stream: no Redis, no .NET, no concurrency.

Written to ask whether default ~= OnFirstMiss at 64 callers came from the access stream or from parallelism: the
stream (it reproduces 72.5% either way, with zero refusals), and through it a doorkeeper that aged only by first
sightings. `count_all` is the fix: every miss ages the filter.

Model (mirrors RespClientCache):
* doorkeeper: 2^20-bit Bloom filter, two probes from a 32-bit hash, cleared after 131,072 first sightings - or,
with count_all, 131,072 misses (sized from MaxBytes / EntryOverheadBytes = 32 MiB / 256)
* storage: slabs in fill order; the oldest slab is evicted whole when over budget; hits touch nothing
* budget: 32 MiB counting whole 1 MiB slabs plus 256 bytes per live entry
"""
import random
import sys

sys.path.insert(0, "/home/marc/code/RespFest")
from harness.keyseq import zipf # noqa: E402

import struct # noqa: E402

KEYSPACE = 200_000
SLAB = 1 << 20
ENTRY = 1024 + 30 # reply + key, both stored in the slab
OVERHEAD = 256
BUDGET = 32 << 20
BITS = 1 << 20
CLEAR_AFTER = BITS // 8

rng = random.Random(1)
HASH = [rng.getrandbits(32) for _ in range(KEYSPACE)] # a well-mixed hash per request


def run(stream, n_ops, measure_from, admission, clear_after=CLEAR_AFTER, count_all=False):
words = bytearray(BITS // 8)
recorded = 0
live = {} # key -> slab id
slabs = {} # slab id -> list of keys stored there
oldest = 0
current = 0
used = 0 # bytes used in the current slab
slabs[0] = []
live_count = 0
hits = misses = stores = refused = 0

def seen_before(h):
nonlocal recorded, words
a = h & (BITS - 1)
b = ((h >> 16) | (h << 16)) & 0xFFFFFFFF & (BITS - 1)
if words[a >> 3] >> (a & 7) & 1 and words[b >> 3] >> (b & 7) & 1:
if count_all: # age by every query, not only by first sightings
recorded += 1
if recorded >= clear_after:
recorded = 0
words = bytearray(BITS // 8)
return True
words[a >> 3] |= 1 << (a & 7)
words[b >> 3] |= 1 << (b & 7)
recorded += 1
if recorded >= clear_after:
recorded = 0
words = bytearray(BITS // 8)
return False

for t in range(n_ops):
k = stream(t)
measuring = t >= measure_from
if k in live:
if measuring:
hits += 1
continue
if measuring:
misses += 1
if admission != "first" and not seen_before(HASH[k]):
if measuring:
refused += 1
continue
# store into the current slab
if used + ENTRY > SLAB:
current += 1
slabs[current] = []
used = 0
slabs[current].append(k)
used += ENTRY
live[k] = current
live_count += 1
if measuring:
stores += 1
# evict oldest sealed slabs while over budget
while (current - oldest + 1) * SLAB + live_count * OVERHEAD > BUDGET and oldest < current:
for victim in slabs.pop(oldest):
if live.get(victim) == oldest:
del live[victim]
live_count -= 1
oldest += 1
total = hits + misses
return hits / total, stores, refused, len(live)


def true_zipf(count, seed):
"""Independent Zipf(0.99) draws over the keyspace: no table, so nothing replays."""
import bisect
cumulative, total = [], 0.0
for rank in range(1, KEYSPACE + 1):
total += rank ** -0.99
cumulative.append(total)
r = random.Random(seed)
return [min(bisect.bisect_left(cumulative, r.random() * total), KEYSPACE - 1) for _ in range(count)]


def main():
seq = struct.unpack(f"<{1 << 20}I", zipf(KEYSPACE, 0.99, length=1 << 20, seed=20261010)) # the league's 1M table
fresh = true_zipf(13_000_000, 11)
k = 13_000_000
cases = [
("true zipf, 13M", lambda t: fresh[t]),
("1M table x64 callers", lambda t: seq[((t % 64) * 16384 + t // 64) % len(seq)]),
]
policies = [
("first", {}),
("repeat: clear @131k new (today)", {}),
("repeat: clear @131k misses", {"count_all": True}),
("repeat: clear @64k misses", {"count_all": True, "clear_after": 65536}),
("repeat: clear @256k misses", {"count_all": True, "clear_after": 262144}),
]
for name, stream in cases:
for policy, kw in policies:
hit, stores, refused, size = run(stream, k, k // 3, "first" if policy == "first" else "repeat", **kw)
print(f"{name:22} {policy:34} hit {hit:6.1%} stores {stores:>9,} refused {refused:>9,}", flush=True)


main()
Loading
Loading