Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
339 commits
Select commit Hold shift + click to select a range
9c74e2d
Add the result-less Wait, and write down the consumption rule
mgravell Sep 14, 2026
bf58863
AsDatabase(); record the CLIENT TRACKING decisions (6.13, 6.14)
mgravell Sep 14, 2026
934e8d2
Single-flight: concurrent misses wait instead of stampeding
mgravell Sep 14, 2026
913e810
Use RespReader.IsOK() rather than Is("OK"u8)
mgravell Sep 14, 2026
a97a58c
Correct the TTL default reasoning in 6.14
mgravell Sep 14, 2026
4f02b65
Prove invalidation delivery against a real server
mgravell Sep 14, 2026
1f8e3cb
Classify pushes the way the real connection does, and stop flaking
mgravell Sep 14, 2026
9cf7da7
CachePolicy, and a finite entry lifetime
mgravell Sep 14, 2026
8dc10d3
Do not track agent worktrees
mgravell Sep 14, 2026
1fbc0a0
Reply handlers for the shapes the string groups need
mgravell Sep 14, 2026
1b00980
Variadic holes, and frames that know which command they are
mgravell Sep 14, 2026
921aa4f
The rest of the string commands
mgravell Sep 14, 2026
2072c77
Bitmaps as a group of their own
mgravell Sep 14, 2026
ac1368e
Let the existing suites be the proof
mgravell Sep 14, 2026
a6083e2
Fire-and-forget: two bugs the re-run found immediately
mgravell Sep 14, 2026
32120ae
Pin the bytes, not just the behaviour
mgravell Sep 14, 2026
4b4a743
Execute returns RespResult: the undecoded reply as a result type
mgravell Sep 14, 2026
97ebf69
Decide: share internally, copy on the way out
mgravell Sep 14, 2026
ef2031a
Correct 6.16: "always copy" is about the MUTABLE outgoing type
mgravell Sep 14, 2026
9a1a37b
ReadOnlyLease<byte>: the lease that may share
mgravell Sep 14, 2026
30d28d7
Ad-hoc Execute returning RespResult
mgravell Sep 14, 2026
1e21fad
An open span hole, so the vocabulary stops growing per data type
mgravell Sep 14, 2026
57a7697
Reply handlers for the hash shapes
mgravell Sep 14, 2026
ea503ac
The hash commands
mgravell Sep 14, 2026
83969c3
Re-run the hash suites, and prove the re-runs are real
mgravell Sep 14, 2026
7f6e738
Pin the hash bytes
mgravell Sep 14, 2026
1d95e31
Keep the hash lease adapters on the copying form
mgravell Sep 14, 2026
4f5df39
Queue: interface-based handler lookup, and the Execute rename
mgravell Sep 14, 2026
907f27f
The set commands
mgravell Sep 14, 2026
696a5c3
RespResult shares the reply buffer instead of copying it
mgravell Sep 14, 2026
13b538a
Queue: rule out buffer-sharing into RedisValue
mgravell Sep 14, 2026
5724562
Say the RS0026 exemption once, not fifty times
mgravell Sep 14, 2026
4a70149
The sorted-set commands
mgravell Sep 14, 2026
1d16f26
Merge the hash, set and sorted-set surfaces
mgravell Sep 14, 2026
a539a53
Registration is implementing the interface
mgravell Sep 14, 2026
30db4ec
Restore the SWR queue entry, deleted by accident
mgravell Sep 14, 2026
253cc2e
Stale-while-revalidate: serve the old value, refresh behind it
mgravell Sep 14, 2026
6b94d58
Invalidation grace period: bridge the burst, do not resurrect
mgravell Sep 14, 2026
f281115
Flush the cache when a connection is lost
mgravell Sep 14, 2026
4d608dd
Host the cache on the multiplexer, and route real invalidations to it
mgravell Sep 14, 2026
e42c8d2
Refuse to cache keys outside the tracked prefixes
mgravell Sep 14, 2026
76744af
Record what the prefix rule narrows, and what it leaves
mgravell Sep 14, 2026
4358d84
An under-declared script is already broken before caching sees it
mgravell Sep 14, 2026
abd8770
Fire-and-forget is never cached, and never served from cache
mgravell Sep 14, 2026
286a461
Split CacheOptions from CachePolicy
mgravell Sep 14, 2026
e2d2ea3
Bound one entry, and make the sweep actually run
mgravell Sep 14, 2026
728e910
CacheTrackingMode: broadcast or per-key, chosen rather than assumed
mgravell Sep 14, 2026
87d5afa
Bound the cache: byte and entry quotas, with sampled eviction
mgravell Sep 14, 2026
a416320
Queue: get the arrays off the new API
mgravell Sep 14, 2026
a32a749
Queue: how the lease returns satisfy the old array API
mgravell Sep 14, 2026
bbf38e4
Queue: the legacy array bridge is a parallel internal extension method
mgravell Sep 15, 2026
682cc68
Rendering is not executing: RespContext.Execute -> Render
mgravell Sep 15, 2026
2c90504
Wipe pooled arrays whose elements can hold references
mgravell Sep 15, 2026
ae3abb1
MGET returns a pooled lease; the array shape moves to an internal sib…
mgravell Sep 15, 2026
eddb3b5
Measure when invalidations actually arrive
mgravell Sep 15, 2026
b21ad97
A write tells the cache before it is sent
mgravell Sep 15, 2026
e501a1c
Record why HIMPORT is a holdout, and why EVALSHA is not
mgravell Sep 15, 2026
bbb91af
A bulk write invalidates its own arguments, not the whole cache
mgravell Sep 15, 2026
5207627
Merge the HIMPORT/EVALSHA holdout notes
mgravell Sep 15, 2026
9625bde
Arrays off the new surface: 28 returns become ReadOnlyLease<T>
mgravell Sep 15, 2026
28d7fa3
Cacheability exclusions: say it at the command, not in a table
mgravell Sep 15, 2026
71422d5
The Keys command group
mgravell Sep 15, 2026
cc2781a
Record the framing: this is V4, not a parallel option
mgravell Sep 15, 2026
662bba0
Correct the V4 note: composition, not a second write path
mgravell Sep 15, 2026
bf221f8
The array siblings are permanent fixtures, and say so
mgravell Sep 15, 2026
db6d4e3
Plan the vexing commands as three probes, one per layer
mgravell Sep 15, 2026
940a0b2
Enforce the argument limit when a frame is closed
mgravell Sep 15, 2026
a7e8024
Count arguments in one place, and refuse the limit as they are written
mgravell Sep 15, 2026
a09f4d4
Benchmark the initial buffer estimate; leave it at 24
mgravell Sep 15, 2026
36e10b1
Settle both cacheability second opinions
mgravell Sep 15, 2026
02a5da2
DUMP's internal encoding is not our problem
mgravell Sep 15, 2026
910d6f7
EVALSHA is composition too: SCRIPT LOAD + EVALSHA, not an alternate r…
mgravell Sep 15, 2026
75b4400
NoScriptCache controls what we send, not what the server keeps
mgravell Sep 15, 2026
5d319e4
NoScriptCache is a weaker claim on the cache, not the absence of one
mgravell Sep 15, 2026
92674cc
The evictable scripts are exactly the ones nothing is believed about
mgravell Sep 15, 2026
10f7b7c
Scripts on the new surface: SCRIPT LOAD + EVALSHA as one unit
mgravell Sep 15, 2026
d399e6c
Log the two-phase reply direction: inspect on the reader, parse at th…
mgravell Sep 15, 2026
f823a62
Size the inspect/parse split: 89 pure parsers, 6 doing both
mgravell Sep 15, 2026
b779959
Render each script once, and honour NoScriptCache on the new surface
mgravell Sep 15, 2026
a4d0b23
The last five leases: read-only on the surface, writable for IDatabase
mgravell Sep 15, 2026
582fcee
Demote the writable-lease handlers, as Values was
mgravell Sep 15, 2026
96dbaab
Wire Keys and Scripts into TransitionalDatabase, and watch for the ne…
mgravell Sep 15, 2026
b56ead3
The list commands
mgravell Sep 15, 2026
2609d07
Ask the muxer what the server can do, when it is there to ask
mgravell Sep 15, 2026
bcc4c47
Cover the two commands the transitional surface respells
mgravell Sep 15, 2026
e615efc
The service slot composes; nothing replaces it
mgravell Sep 15, 2026
e09411a
The two respelt commands now ask before respelling
mgravell Sep 15, 2026
018d5ec
Merge the Keys and Scripts work, so lists absorb cleanly
mgravell Sep 15, 2026
75933c6
Issue CLIENT TRACKING from the handshake, not from the tests
mgravell Sep 15, 2026
d5bb4f3
Merge marc/lists
mgravell Sep 15, 2026
a595626
Queue: close both CLIENT TRACKING items, open the WithCache soundness…
mgravell Sep 15, 2026
80d0b1d
Make RespClientCache internal; the public opt-out is WithoutCache()
mgravell Sep 15, 2026
f828ddc
The HyperLogLog group, and a probe that moves flags rather than bytes
mgravell Sep 15, 2026
8c79064
SORT, on the key group, with SORT_RO where the server has it
mgravell Sep 15, 2026
655a33e
IMultiMessage can decline: GetMessages returns null for "write me nor…
mgravell Sep 15, 2026
1785495
Make the composed script pair actually work against a server
mgravell Sep 15, 2026
4be82fe
Stage 1c: skip SCRIPT LOAD when the endpoint is believed to hold the …
mgravell Sep 15, 2026
73bbc56
The geospatial group, with GEOSEARCH where the server has one
mgravell Sep 15, 2026
831ebe7
Pin that batch and transaction contexts already queue rather than send
mgravell Sep 15, 2026
3abb43e
Split the command-group target: keyspace groups leave IServer and ISu…
mgravell Sep 15, 2026
5f3c6bc
Queue: record the split's commit hash
mgravell Sep 15, 2026
ab1d61c
Merge marc/lists: the HyperLogLog, SORT and geospatial groups
mgravell Sep 15, 2026
abfc2ba
Use AsRedisValue for CLIENT TRACKING prefixes, so Debug builds too
mgravell Sep 15, 2026
4bfc05f
Wire IServer's context, and give ad-hoc frames their command identity
mgravell Sep 15, 2026
0994dc0
Queue: record the IServer context commit hash
mgravell Sep 15, 2026
394a226
The vector-set group, less VADD and VSIM
mgravell Sep 15, 2026
c3e7df0
VADD and VSIM, and the vector-set group is complete
mgravell Sep 15, 2026
c221a77
Queue: locking waits for transactions, and why the Lua shortcut was d…
mgravell Sep 15, 2026
b66f664
Pin what every IMultiMessage does inside a transaction
mgravell Sep 15, 2026
6fe595e
Make a dropped IMultiMessage expansion impossible to write by accident
mgravell Sep 15, 2026
0c8b961
Merge branch 'marc/interpolated-writer-design' into marc/lists
mgravell Sep 15, 2026
0535736
RespValue: a scalar reply as a window, with nothing of its own
mgravell Sep 15, 2026
dfd673f
Prove the value dies with its lease, by every route and both branches
mgravell Sep 15, 2026
0ac297f
Measure what a transaction condition costs a thread
mgravell Sep 15, 2026
a55035c
One rule for the prefix: As gives you something you keep
mgravell Sep 15, 2026
ea26ce6
Name the inspect half of SetResult
mgravell Sep 15, 2026
2f42d85
Queue: the verdict increment, and errors-as-values as a post-verdict …
mgravell Sep 15, 2026
a86e563
Notice NOSCRIPT on the frame path, so a stale script belief is not pe…
mgravell Sep 15, 2026
7d5972a
A lease can hold a second thing to give back
mgravell Sep 15, 2026
a3ad7a6
RespValue belongs in RESPite, so move it there
mgravell Sep 15, 2026
7d52169
Inspect returns a verdict, and NOSCRIPT retries in the pipeline
mgravell Sep 15, 2026
d338591
Every path retries NOSCRIPT, so the eight catch sites go
mgravell Sep 15, 2026
83ffccb
Queue: close the inspect/parse split
mgravell Sep 15, 2026
1e36e47
MGET as windows into one buffer: 77x less garbage
mgravell Sep 15, 2026
0116dba
default(RespValue) is nil, and a cache hit is read in place
mgravell Sep 15, 2026
681bc7c
Pin that keys beyond the bitmap still route
mgravell Sep 15, 2026
1839496
Queue: WATCH waits for the Message refactor, because the pulse goes w…
mgravell Sep 15, 2026
edeae93
Flip the other twenty, and gate the whole idea behind its own ID
mgravell Sep 15, 2026
c5d87d4
Decided: errors stay exceptions on the new surface
mgravell Sep 15, 2026
18679de
Queue: frame the errors decision as call vs pcall, which fixes the sh…
mgravell Sep 15, 2026
588b7e4
Merge branch 'marc/interpolated-writer-design' into marc/resp-result-…
mgravell Sep 15, 2026
4e7fca4
IRespHandler parses from a positioned reader, not a span
mgravell Sep 15, 2026
53759b1
Merge branch 'marc/interpolated-writer-design' into marc/resp-result-…
mgravell Sep 15, 2026
ab1996d
Take the positioned reader, and say why a window cannot
mgravell Sep 15, 2026
7ebc88f
Queue: the down-level story, measured across four toolchains
mgravell Sep 15, 2026
68bb062
Queue: the other three MSFT points, with the tensions each collides with
mgravell Sep 15, 2026
499103b
Queue: generating the shims was declined - build cost; a unit test en…
mgravell Sep 15, 2026
24d37ba
Async suffixes on the new surface (MSFT review)
mgravell Sep 15, 2026
81c6f98
Cancellation is per-call, and a real token is refused rather than ign…
mgravell Sep 15, 2026
8784461
Honour an already-cancelled token, and prove the buffer comes back
mgravell Sep 15, 2026
0a05dd9
Group accessors as methods too, for compilers older than C# 14
mgravell Sep 15, 2026
57ec73f
Channel prefixes compose, like key prefixes already did
mgravell Sep 15, 2026
c55559e
Record the prefix correction, and unstale two design claims
mgravell Sep 15, 2026
e8544b6
Pin that a null key prefix adds nothing rather than resetting
mgravell Sep 15, 2026
051c31b
Name the prefix methods for what they do: Append, not With
mgravell Sep 15, 2026
6c74d80
One projection per element type, shared by every aggregate form
mgravell Sep 15, 2026
73ec0bc
HIMPORT is not a third seam: it is the script seam, scoped differently
mgravell Sep 15, 2026
4194894
Pin the composed pair's refusal inside a transaction
mgravell Sep 15, 2026
f2f19b3
Batches and transactions offer the command groups by name
mgravell Sep 15, 2026
274c41e
A caller-supplied hash is EVALSHA, even without the expansion
mgravell Sep 15, 2026
9193189
Pair types derive both aggregate forms from one shape too
mgravell Sep 15, 2026
8606a91
HIMPORT injects through IMultiMessage, so the bridge hook retires
mgravell Sep 15, 2026
d1e946c
The OBJECT family moves to the Keys group
mgravell Sep 15, 2026
c015879
The stream group's scalar half
mgravell Sep 15, 2026
9992902
Park the composite-array shapes, with the wire evidence for the candi…
mgravell Sep 15, 2026
6a81bed
The array group, less ARGREP
mgravell Sep 15, 2026
f49605b
Prototype the deferred walk, and measure what it is actually worth
mgravell Sep 15, 2026
5a012da
Note the capnproto lineage, and where the analogy stops
mgravell Sep 15, 2026
98b7f06
Time the deferred walk on the nested shape, and correct a wrong attri…
mgravell Sep 15, 2026
76a175e
Decided: no indexer on the aggregate
mgravell Sep 15, 2026
de44670
Count is fine: streamed aggregates do not happen in practice
mgravell Sep 15, 2026
5f17e25
Decide where the deferred-view types live, and what they are called
mgravell Sep 16, 2026
44bf263
Decide the root the caller holds, and the way back to the old shapes
mgravell Sep 16, 2026
f24e0c9
One parse, two readers: hoist the stream parses off the generic base
mgravell Sep 16, 2026
3998d88
RespAggregate<T>: an aggregate reply as a window, children on demand
mgravell Sep 16, 2026
c7e9cfe
ToArray and ToLease on the aggregate; leave RespValue.ToString alone
mgravell Sep 16, 2026
a5adc08
Both window types announce their prefix, read not stored
mgravell Sep 16, 2026
b189039
Pin that RESP2 and RESP3 already agree, so maps must not be halved
mgravell Sep 16, 2026
187ff4c
Windows that can be captured from inside a walk, and a pair-shaped one
mgravell Sep 16, 2026
2d003e3
A typed root for a nested reply, and XRANGE on it
mgravell Sep 16, 2026
57e3f64
Thread the policy the stream parses actually read, not a protocol
mgravell Sep 16, 2026
d619e8b
A name/value pair is not a stream type, and the rule for why
mgravell Sep 16, 2026
7532533
Give Streams its own three files, out of the staging namespace
mgravell Sep 16, 2026
f605a5e
Do not lean on an optimisation to stay off System.ValueTuple
mgravell Sep 16, 2026
2744e2a
Write down when an operand struct beats Compose/Append
mgravell Sep 16, 2026
d23f19d
Correct the record: the interpolated write path is not exception-safe
mgravell Sep 16, 2026
1ae1d75
The leak belongs to the interpolated form, not to the operand
mgravell Sep 16, 2026
bc0daf7
Scan: optional token+integer is everywhere, spelled five ways
mgravell Sep 16, 2026
2325933
Name the optional-token idiom: RespFragment.When
mgravell Sep 16, 2026
bef672d
Four commands had no retry category, and nothing could have said so
mgravell Sep 16, 2026
90c6d1e
GetDefaultCategory, and queue the retry tests HIMPORT is waiting on
mgravell Sep 16, 2026
4e08db4
Wire .NET 11 runtime-async for discovery, gated so it cannot ship
mgravell Sep 16, 2026
639b804
Move the version line to v4
mgravell Sep 16, 2026
02ee66b
Measure the machinery with the socket removed
mgravell Sep 16, 2026
1cda1e8
The suspend rows were measuring the harness, not the library
mgravell Sep 16, 2026
f358a75
Measure runtime async properly, and correct the deferred-view claim
mgravell Sep 16, 2026
892f65f
Apply the retry category where the frame is, not at 255 call sites
mgravell Sep 16, 2026
346217f
XRANGE as the template: compose once, serve the array with a handler
mgravell Sep 16, 2026
5325e5f
Record why the frame keeps its ref, and the rule that goes with it
mgravell Sep 16, 2026
7062b4b
Close the Streams group: XDELEX composed once, and name the local cmd
mgravell Sep 16, 2026
5a8f70c
RespFrame becomes RespRequestFrame
mgravell Sep 16, 2026
8104c3c
Correct a queue entry that was ticked while half done
mgravell Sep 16, 2026
0e2fd31
Cancellation reaches the surface: Streams as the template
mgravell Sep 16, 2026
3706bed
Give the no-reply send a cancellation token, and lay out the dispatch…
mgravell Sep 16, 2026
abcd786
One handler class per group, not per type or in the shared registry
mgravell Sep 16, 2026
58bc59d
RespKey: a key the writer borrows rather than owns
mgravell Sep 16, 2026
3d42d91
Borrowed keys work for dynamic argument lists too
mgravell Sep 16, 2026
ec3518d
RespCommandHandler becomes RespRequestBuilder
mgravell Sep 16, 2026
dc6e665
RespKey gains a string ctor and a third state for null
mgravell Sep 16, 2026
57600d5
Cancellation reaches every command group
mgravell Sep 16, 2026
6006cd7
Every command group becomes its own files
mgravell Sep 16, 2026
e6880d7
Command text with a decision in it gets written once
mgravell Sep 16, 2026
44371ee
WithDatabase sends to the database it names
mgravell Sep 16, 2026
b21296c
Record the server-group naming decision, with the collision that forc…
mgravell Sep 16, 2026
80a19e4
The server-scoped group, and the namespace that was standing on its name
mgravell Sep 17, 2026
f868d11
A text twin for CopyTo: decode straight into a Span<char>
mgravell Sep 17, 2026
da6b7fe
Reach the no-decoder path far more often in CopyTo(Span<char>)
mgravell Sep 17, 2026
fad5068
Record why the char side gets no IBufferWriter overload
mgravell Sep 17, 2026
ab978ca
A guide for library authors extending the client
mgravell Sep 17, 2026
05013f7
Teach the new API everywhere, and keep the old one findable
mgravell Sep 17, 2026
26df799
Retire SER010: this is the 4.0 API, not an experiment
mgravell Sep 17, 2026
d03c41f
The server group is Keyspace, not Server
mgravell Sep 17, 2026
18563be
Put the toy server's names back
mgravell Sep 17, 2026
85eb97b
.Interpolated dies; the surface splits by audience
mgravell Sep 17, 2026
e34c0d0
Typed root contexts, and accessors that do not box them
mgravell Sep 17, 2026
a24005c
WIP: typed contexts own the groups - TESTS DO NOT COMPILE
mgravell Sep 17, 2026
8648b42
Merge main (3.3.0) into the V4 spike
mgravell Sep 18, 2026
8c8863f
Review feedback: IRespArgument removes the bespoke execute path
mgravell Sep 18, 2026
69f733f
Typed contexts: green
mgravell Sep 18, 2026
912da47
The extender doc teaches the accessor shape the tests actually compile
mgravell Sep 18, 2026
42d7b7e
Measure passing the context by reference, and decline it
mgravell Sep 18, 2026
630ce3c
Queue two follow-ups from review: the ROM<RedisKeyOrValue> execute is…
mgravell Sep 18, 2026
ad0a9c0
Profile the cache hit: the probe and the reply parse are the cost
mgravell Sep 18, 2026
9687819
Take the RedisKeyOrValue collection execute off the public surface
mgravell Sep 18, 2026
c2afbeb
Bind a bare string hole to the value overload
mgravell Sep 18, 2026
8ffc336
Stop default(RespCommand) framing a command called NONE
mgravell Sep 18, 2026
da0d39c
Move the builder's guards onto throw helpers, and make them return th…
mgravell Sep 18, 2026
16eac27
Move the caching types into StackExchange.Redis.Caching, and give the…
mgravell Sep 18, 2026
cc577b6
Make RespScriptCache internal, with its accessors
mgravell Sep 18, 2026
60d4c42
Seed the script cache from the multiplexer
mgravell Sep 18, 2026
07cad77
Move the command and reply-handler types into StackExchange.Redis.Pro…
mgravell Sep 18, 2026
db75a6f
Empty the Interpolated folder, and fix the SER309 code fix it was hiding
mgravell Sep 18, 2026
8adf598
Record the TransitionalDatabase gap breakdown
mgravell Sep 18, 2026
1d2d73d
Move the scalar-reply stream commands to the context surface
mgravell Sep 18, 2026
3dbb529
Move the builder's buffer growth out of line
mgravell Sep 18, 2026
58fb10f
Move XCLAIM to the context surface
mgravell Sep 18, 2026
fd3a19b
Move XPENDING to the context surface, both shapes
mgravell Sep 18, 2026
53b85cb
Move XAUTOCLAIM to the context surface, both shapes
mgravell Sep 18, 2026
37c166b
Move the single-stream XREAD and XREADGROUP to the context surface
mgravell Sep 18, 2026
d2af54f
Reach a context's plumbing by explicit cast, not a public Raw property
mgravell Sep 18, 2026
0716035
Rename IRespTarget.Raw to Context, hidden by the derived interfaces
mgravell Sep 18, 2026
7253500
Log the two shipped stream bugs, and the Context rename
mgravell Sep 18, 2026
20d2533
Write durations through a unit-bearing hole, and fix XREADGROUP CLAIM
mgravell Sep 18, 2026
3d7183c
Stop the command groups handing their context back
mgravell Sep 18, 2026
46d0783
Count the cursor scans, which SER352 cannot see
mgravell Sep 18, 2026
dc10024
Add the dual scan API, proved on SSCAN
mgravell Sep 18, 2026
5be68ea
Measure the cache-hit path by key size, before the hot-path surgery
mgravell Sep 18, 2026
397a066
Restore RedisValue.EqualityComparer, lost after the main merge
mgravell Sep 18, 2026
2a31900
Restore everything the post-merge commit discarded
mgravell Sep 18, 2026
78ce99b
Benchmark XxHash3 for the cache key, and decline it
mgravell Sep 18, 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
  •  
  •  
  •  
25 changes: 25 additions & 0 deletions .github/actions/run-tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,31 @@ runs:
--results-directory ./test-results/
/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
continue-on-error: true
if: success() || failure()
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 retired (vector sets, Redis 8.4/8.6/8.8/8.10 features,
and geo-redundant failover, no longer experimental); IDs reserved, see Experiments.cs -->
<NoWarn>$(NoWarn);NU5105;NU1507;SER004;SER005;SER009;SER010</NoWarn>
<!-- SER001/SER002/SER003/SER006/SER007/SER008/SER013 retired (vector sets, Redis 8.4/8.6/8.8/8.10
features, geo-redundant failover, 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;SER010;SER012</NoWarn>
<PackageReleaseNotes>https://github.com/StackExchange/StackExchange.Redis/releases</PackageReleaseNotes>
<PackageProjectUrl>https://seredis.dev/</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
3,422 changes: 3,422 additions & 0 deletions design/interpolated-resp-writer.md

Large diffs are not rendered by default.

3,112 changes: 3,112 additions & 0 deletions design/interpolated-resp-writer.queue.md

Large diffs are not rendered by default.

68 changes: 34 additions & 34 deletions docs/Arrays.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ Use `ArraySetAsync` and `ArrayGetAsync` to write and read individual cells:
var db = conn.GetDatabase();
RedisKey key = "events";

bool inserted = await db.ArraySetAsync(key, 0, "created");
RedisValue value = await db.ArrayGetAsync(key, 0);
RedisValue missing = await db.ArrayGetAsync(key, 1);
bool inserted = await db.Arrays.SetAsync(key, 0, "created");
RedisValue value = await db.Arrays.GetAsync(key, 0);
RedisValue missing = await db.Arrays.GetAsync(key, 1);

Console.WriteLine(inserted); // True when the cell did not previously have a value
Console.WriteLine(value); // created
Expand All @@ -26,36 +26,36 @@ Console.WriteLine(missing.IsNull); // True
Array indexes use `RedisArrayIndex`, with implicit conversions from `int`, `long`, and `ulong`. This allows normal small indexes to be used directly, while still allowing the full unsigned index range when needed.

```csharp
await db.ArraySetAsync(key, 42, "answer");
await db.ArraySetAsync(key, new RedisArrayIndex(10_000_000UL), "large index");
await db.Arrays.SetAsync(key, 42, "answer");
await db.Arrays.SetAsync(key, new RedisArrayIndex(10_000_000UL), "large index");
```

## Sparse Arrays

Arrays are sparse: unset cells do not have values. `ArrayLengthAsync` reports the notional length, which is the highest used index plus one. `ArrayCountAsync` reports only cells that currently have values.

```csharp
await db.KeyDeleteAsync(key);
await db.Keys.DeleteAsync(key);

await db.ArraySetAsync(key, 0, "a");
await db.ArraySetAsync(key, 10, "b");
await db.Arrays.SetAsync(key, 0, "a");
await db.Arrays.SetAsync(key, 10, "b");

RedisArrayIndex length = await db.ArrayLengthAsync(key); // 11
RedisArrayIndex count = await db.ArrayCountAsync(key); // 2
RedisArrayIndex length = await db.Arrays.LengthAsync(key); // 11
RedisArrayIndex count = await db.Arrays.CountAsync(key); // 2
```

## Setting Multiple Values

To write a contiguous range, pass the first index and the values:

```csharp
int inserted = await db.ArraySetAsync(key, 0, ["a", "b", "c"]);
int inserted = await db.Arrays.SetAsync(key, 0, ["a", "b", "c"]);
```

To write multiple specific indexes, use `RedisArrayEntry` values:

```csharp
await db.ArraySetAsync(key,
await db.Arrays.SetAsync(key,
[
new RedisArrayEntry(0, "alpha"),
new RedisArrayEntry(5, "bravo"),
Expand All @@ -70,20 +70,20 @@ The returned `int` is the number of cells that were newly filled.
Read selected indexes with `ArrayGetAsync`:

```csharp
RedisValue[] values = await db.ArrayGetAsync(key, [0, 5, 6, 100]);
RedisValue[] values = await db.Arrays.GetAsync(key, [0, 5, 6, 100]);
```

Read a range with `ArrayGetRangeAsync`. Ranges can be read forward or backward:

```csharp
RedisValue[] forward = await db.ArrayGetRangeAsync(key, 0, 5);
RedisValue[] reverse = await db.ArrayGetRangeAsync(key, 5, 0);
RedisValue[] forward = await db.Arrays.GetRangeAsync(key, 0, 5);
RedisValue[] reverse = await db.Arrays.GetRangeAsync(key, 5, 0);
```

For sparse arrays, use `ArrayScanAsync` to return only populated cells in a range:

```csharp
RedisArrayEntry[] entries = await db.ArrayScanAsync(key, 0, 100, limit: 50);
RedisArrayEntry[] entries = await db.Arrays.ScanAsync(key, 0, 100, limit: 50);

foreach (var entry in entries)
{
Expand All @@ -96,21 +96,21 @@ foreach (var entry in entries)
Delete a single cell with `ArrayDeleteAsync`:

```csharp
bool removed = await db.ArrayDeleteAsync(key, 5);
bool removed = await db.Arrays.DeleteAsync(key, 5);
```

Delete multiple specific cells by index:

```csharp
int removedCount = await db.ArrayDeleteAsync(key, [0, 5, 100]);
int removedCount = await db.Arrays.DeleteAsync(key, [0, 5, 100]);
```

Delete one or more ranges:

```csharp
await db.ArrayDeleteRangeAsync(key, 10, 20);
await db.Arrays.DeleteRangeAsync(key, 10, 20);

await db.ArrayDeleteRangeAsync(key,
await db.Arrays.DeleteRangeAsync(key,
[
new RedisArrayRange(100, 199),
new RedisArrayRange(500, 599),
Expand Down Expand Up @@ -171,17 +171,17 @@ RedisArrayEntry[] matches = await db.ArrayGrepAsync(key, request);
Arrays have a write head used by insert operations. `ArrayInsertAsync` writes at the current write head and advances it.

```csharp
RedisArrayIndex first = await db.ArrayInsertAsync(key, "first");
RedisArrayIndex second = await db.ArrayInsertAsync(key, "second");
RedisArrayIndex first = await db.Arrays.InsertAsync(key, "first");
RedisArrayIndex second = await db.Arrays.InsertAsync(key, "second");

RedisArrayIndex? next = await db.ArrayNextAsync(key);
RedisArrayIndex? next = await db.Arrays.NextAsync(key);
```

Move the write head with `ArraySeekAsync`:

```csharp
bool moved = await db.ArraySeekAsync(key, 1_000);
RedisArrayIndex written = await db.ArrayInsertAsync(key, "later");
bool moved = await db.Arrays.SeekAsync(key, 1_000);
RedisArrayIndex written = await db.Arrays.InsertAsync(key, "later");
```

## Ring Buffers
Expand All @@ -191,34 +191,34 @@ Use `ArrayRingAsync` to keep at most a fixed number of cells and wrap writes aro
```csharp
for (int i = 0; i < 10; i++)
{
await db.ArrayRingAsync(key, maxLength: 5, value: i);
await db.Arrays.RingAsync(key, maxLength: 5, value: i);
}

RedisArrayIndex count = await db.ArrayCountAsync(key); // 5
RedisArrayIndex count = await db.Arrays.CountAsync(key); // 5
```

`ArrayLastItemsAsync` is intended for this capped ring-buffer model. It reads the last values in the ring-buffer sense, where "last" relates to the retained values after wrap-around and trimming:

```csharp
RedisValue[] last = await db.ArrayLastItemsAsync(key, count: 10);
RedisValue[] lastReversed = await db.ArrayLastItemsAsync(key, count: 10, reverse: true);
RedisValue[] last = await db.Arrays.LastItemsAsync(key, count: 10);
RedisValue[] lastReversed = await db.Arrays.LastItemsAsync(key, count: 10, reverse: true);
```

## Operations and Info

Use `ArrayOperationAsync` for simple server-side operations over a range:

```csharp
RedisValue sum = await db.ArrayOperationAsync(key, 0, 10, ArrayOperation.Sum);
RedisValue used = await db.ArrayOperationAsync(key, 0, 10, ArrayOperation.Used);
RedisValue matches = await db.ArrayOperationAsync(key, 0, 10, ArrayOperation.Match, "error");
RedisValue sum = await db.Arrays.OperationAsync(key, 0, 10, ArrayOperation.Sum);
RedisValue used = await db.Arrays.OperationAsync(key, 0, 10, ArrayOperation.Used);
RedisValue matches = await db.Arrays.OperationAsync(key, 0, 10, ArrayOperation.Match, "error");
```

Use `ArrayInfoAsync` for metadata:

```csharp
ArrayInfo info = await db.ArrayInfoAsync(key);
ArrayInfo fullInfo = await db.ArrayInfoAsync(key, full: true);
ArrayInfo info = await db.Arrays.InfoAsync(key);
ArrayInfo fullInfo = await db.Arrays.InfoAsync(key, full: true);

Console.WriteLine($"Count: {info.Count}");
Console.WriteLine($"Length: {info.Length}");
Expand Down
37 changes: 17 additions & 20 deletions docs/Basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,29 +43,32 @@ object asyncState = ...
IDatabase db = redis.GetDatabase(databaseNumber, asyncState);
```

Once you have the `IDatabase`, it is simply a case of using the [redis API](https://redis.io/commands). Note that all methods have both synchronous and asynchronous implementations. In line with Microsoft's naming guidance, the asynchronous methods all end `...Async(...)`, and are fully `await`-able etc.
Once you have the `IDatabase`, it is simply a case of using the [redis API](https://redis.io/commands). Commands are grouped by the redis data type they belong to - `db.Strings`, `db.Hashes`, `db.Lists`, `db.Sets`, `db.SortedSets`, `db.Keys` and so on - and every command is asynchronous, ending `...Async(...)` in line with Microsoft's naming guidance.

The simplest operation would be to store and retrieve a value:

```csharp
string value = "abcdefg";
db.StringSet("mykey", value);
await db.Strings.SetAsync("mykey", value);
...
string value = db.StringGet("mykey");
RedisValue value = await db.Strings.GetAsync("mykey");
Console.WriteLine(value); // writes: "abcdefg"
```

Note that the `String...` prefix here denotes the [String redis type](https://redis.io/topics/data-types), and is largely separate to the [.NET String type][3], although both can store text data. However, redis allows raw binary data for both keys and values - the usage is identical:
Note that the `Strings` group here denotes the [String redis type](https://redis.io/topics/data-types), and is largely separate to the [.NET String type][3], although both can store text data. However, redis allows raw binary data for both keys and values - the usage is identical:

```csharp
byte[] key = ..., value = ...;
db.StringSet(key, value);
await db.Strings.SetAsync(key, value);
...
byte[] value = db.StringGet(key);
RedisValue value = await db.Strings.GetAsync(key);
byte[] bytes = value!;
```

The entire range of [redis database commands](https://redis.io/commands) covering all redis data types is available for use.

> **Coming from an earlier version?** These were `db.StringSet` and `db.StringGet`, on one flat interface. That spelling still works and is still supported - see [The original `IDatabase` API](LegacyApi) for what changed, the full mapping, and the handful of commands that are not on the groups yet.

Using redis pub/sub
----

Expand Down Expand Up @@ -131,36 +134,30 @@ DateTime lastSave = server.LastSave();
ClientInfo[] clients = server.ClientList();
```

Sync vs Async vs Fire-and-Forget
Async vs Fire-and-Forget
---

There are 3 primary usage mechanisms with StackExchange.Redis:
There are 2 primary usage mechanisms with StackExchange.Redis:

- Synchronous - where the operation completes before the methods returns to the caller (note that while this may block the caller, it absolutely **does not** block other threads: the key idea in StackExchange.Redis is that it aggressively shares the connection between concurrent callers)
- Asynchronous - where the operation completes some time in the future, and a `Task` or `Task<T>` is returned immediately, which can later:
- be `.Wait()`ed (blocking the current thread until the response is available)
- have a continuation callback added ([`ContinueWith`](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.continuewith) in the TPL)
- be *awaited* (which is a language-level feature that simplifies the latter, while also continuing immediately if the reply is already known)
- Asynchronous - where the operation completes some time in the future, and a `ValueTask` or `ValueTask<T>` is returned immediately, which is then *awaited*. This is how the command groups work, and it is what you should reach for: it continues immediately if the reply is already known, and it never ties up a thread waiting for the network.
- Fire-and-Forget - where you really aren't interested in the reply, and are happy to continue irrespective of the response

The synchronous usage is already shown in the examples above. This is the simplest usage, and does not involve the [TPL][1].

For asynchronous usage, the key difference is the `Async` suffix on methods, and (typically) the use of the `await` language feature. For example:

```csharp
string value = "abcdefg";
await db.StringSetAsync("mykey", value);
await db.Strings.SetAsync("mykey", value);
...
string value = await db.StringGetAsync("mykey");
RedisValue value = await db.Strings.GetAsync("mykey");
Console.WriteLine(value); // writes: "abcdefg"
```

The fire-and-forget usage is accessed by the optional `CommandFlags flags` parameter on all methods (defaults to none). In this usage, the method returns the default value immediately (so a method that normally returns a `String` will always return `null`, and a method that normally returns an `Int64` will always return `0`). The operation will continue in the background. A typical use-case of this might be to increment page-view counts:

```csharp
db.StringIncrement(pageKey, flags: CommandFlags.FireAndForget);
await db.Strings.IncrementAsync(pageKey, flags: CommandFlags.FireAndForget);
```

A third mechanism exists and is deliberately not shown here: the **synchronous** members of the original `IDatabase` API - `await db.Strings.GetAsync(key)` and friends - which block the calling thread until the reply arrives. They still work, and for a console tool or a startup path they are fine. On a server they are the single most common cause of the timeouts this library gets reported, because the thread you block is one the reply may need: see [Sync over async](SyncOverAsync), and [The original `IDatabase` API](LegacyApi) for the mapping.

[1]: https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/task-parallel-library-tpl
[2]: https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.asyncstate
[3]: https://docs.microsoft.com/en-us/dotnet/api/system.string
Loading
Loading