Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2e49b54
Fix StringSet(Expiration, ValueCondition) doc link: set, not delex (#…
mgravell Oct 12, 2026
3679bd0
Tolerate refused CLUSTER SLOTS/NODES during topology updates (#3275)
mgravell Oct 13, 2026
209c46b
Report a transport connection's failure the way a stream connection's…
MDA2AV Oct 13, 2026
d59ff9e
Lag-aware failback checks for geo-redundant failover (#3242)
mgravell Oct 13, 2026
5625693
Do not abandon a connection attempt before ConnectTimeout has elapsed…
mgravell Oct 16, 2026
1f13e2d
Read HELLO "server" before trusting "version" as the Redis API versio…
mgravell Oct 16, 2026
84765ee
Sentinel soft-fail follow-ups: discovery failure as RedisConnectionEx…
mgravell Oct 16, 2026
a3f389a
Sentinel: re-resolve the primary when it is demoted on a healthy conn…
rebaseandpanic Oct 21, 2026
7870ba9
Route as a cluster only with a slot map; take HELLO mode/role from th…
mgravell Oct 22, 2026
10caa39
Fail connection-dropped commands with RedisConnectionException, never…
mgravell Oct 22, 2026
53aab24
Tests: tolerate clock skew on absolute hash-field expiries; wait for …
mgravell Oct 22, 2026
cb09290
Connection groups: a member answering LOADING triggers an immediate r…
mgravell Oct 22, 2026
74c52bf
IBatch: complete commands refused by a poisoned connection; divert a …
mgravell Oct 22, 2026
a07aabb
Tests: don't race the connect's own reconfigures in AllClusterCommand…
mgravell Oct 22, 2026
6cbaa37
Send primary-only commands to the primary when the caller prefers a r…
AsadIqbal-Dev Oct 23, 2026
946f13a
Give multi-message sub-commands and injected prefix commands a write …
mgravell Oct 23, 2026
9d30a98
Refresh package pins: 10.0.13 servicing family and tooling (#3294)
mgravell Oct 23, 2026
9a1fabe
xunit.v3 4.0 on Microsoft.Testing.Platform (#3295)
mgravell Oct 23, 2026
aa1e5c0
StyleCop.Analyzers 1.2.0-beta.556 -> 1.3.151-beta, and the tidy-up it…
mgravell Oct 23, 2026
1af23ca
Merge origin/main into v4 (19 commits, 2026-10-12..23): textual confl…
mgravell Oct 23, 2026
da99d2a
Port main's fixes onto the new core by intent (merge follow-up): #329…
mgravell Oct 23, 2026
38014f3
StyleCop.Analyzers 1.3.151-beta on v4 (main #3296 reached the new cor…
mgravell Oct 23, 2026
25f7b14
Merge fix-up: restore v4's CI.yml (the --theirs resolution had replac…
mgravell Oct 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,8 @@ present (write "none" rather than omitting one):
malformed), the `RoundTripUnitTests` cases (exact bytes), the live tests written-not-run with
`Create(require: RedisFeatures.vX_Y_Z)`, the HLD section 8 scenarios copied and marked
`expected`, and the exact commands:
`dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -f net10.0 --filter "FullyQualifiedName~<Cmd>"`
and, when the analyzer changed, `dotnet test tests/StackExchange.Redis.Build.Tests/StackExchange.Redis.Build.Tests.csproj`.
`dotnet test --project tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -f net10.0 --filter-class "*<Cmd>*"`
and, when the analyzer changed, `dotnet test --project tests/StackExchange.Redis.Build.Tests/StackExchange.Redis.Build.Tests.csproj`.
7. **Docs / changelog / public-API files** - `docs/<Feature>.md` + `docs/index.md` yes/no,
`docs/exp/SERxxx.md` yes/no, the `PublicAPI` files, and the note that `docs/ReleaseNotes.md` is
frozen.
Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/implement-resp-command/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,6 @@ The in-process managed server (`toys/StackExchange.Redis.Server`) may also need
## Before finishing

- `dotnet build Build.csproj -c Release /p:CI=true` — analyzers + `TreatWarningsAsErrors` must pass (this catches a missing `PublicAPI.Unshipped.txt` entry).
- `dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -f net10.0 --filter "FullyQualifiedName~MyCommand"` — runs your new unit tests without any server.
- `dotnet test tests/StackExchange.Redis.Build.Tests/StackExchange.Redis.Build.Tests.csproj` — if you touched `TransactionAnalyzer`. Also needs no server, and takes seconds.
- `dotnet test --project tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -f net10.0 --filter-class "*MyCommand*"` — runs your new unit tests without any server (Microsoft.Testing.Platform filters take fully qualified names, `*` at either end).
- `dotnet test --project tests/StackExchange.Redis.Build.Tests/StackExchange.Redis.Build.Tests.csproj` — if you touched `TransactionAnalyzer`. Also needs no server, and takes seconds.
- Double-check no shipped signature changed (back-compat).
32 changes: 18 additions & 14 deletions .github/actions/run-tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,42 +71,46 @@ runs:

- name: StackExchange.Redis.Tests
shell: bash
# Microsoft.Testing.Platform `dotnet test` (selected in global.json); the GitHub Actions reporter is a platform
# extension that is off by default, and the TRX report feeds dorny/test-reporter below (the platform's
# reporter, not xunit's --report-xunit-trx, which truncated the file in 4.0.2)
run: >-
dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj
dotnet test --project tests/StackExchange.Redis.Tests/StackExchange.Redis.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/
--blame-hang-timeout 10m
--blame-hang-dump-type none
/p:CI=true
--report-trx
--report-github
--report-github-summary-include-passed false
--report-github-summary-include-skipped false
# 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
dotnet test --project 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
--report-trx
--report-github
--report-github-summary-include-passed false
--report-github-summary-include-skipped false
- name: StackExchange.Redis.Build.Tests
shell: bash
run: >-
dotnet test tests/StackExchange.Redis.Build.Tests/StackExchange.Redis.Build.Tests.csproj
dotnet test --project 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
--report-trx
--report-github
--report-github-summary-include-passed false
--report-github-summary-include-skipped false
- uses: dorny/test-reporter@v3
continue-on-error: true
Expand Down
11 changes: 7 additions & 4 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -274,14 +274,17 @@ 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.
# A hung test otherwise holds the job until GitHub's six-hour limit; the step timeout is the backstop.
# (VSTest's --blame-hang named the hung test and killed it; the Microsoft.Testing.Platform runner has
# no equivalent without its HangDump extension, so for now the timeout is all there is.)
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/ --blame-hang-timeout 10m --blame-hang-dump-type none /p:CI=true
# Microsoft.Testing.Platform `dotnet test` (selected in global.json); the GitHub Actions reporter is a platform
# extension that is off by default, and the TRX report feeds dorny/test-reporter below (the platform's
# reporter, not xunit's --report-xunit-trx, which truncated the file in 4.0.2)
dotnet test --project tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj --no-build -c Release -f $tfm --results-directory ./test-results/ --report-trx --report-github --report-github-summary-include-passed false --report-github-summary-include-skipped false
if ($LASTEXITCODE -ne 0) {
$exitCode = $LASTEXITCODE
}
Expand Down
12 changes: 8 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ StackExchange.Redis — a high-performance .NET client for RESP servers (Redis,
- `toys/` — runnable samples and an in-process RESP server (`StackExchange.Redis.Server`, used by tests as a managed fake server), Kestrel-hosted server, console tools.
- `docs/` — published documentation site source (markdown). `docs/ReleaseNotes.md` is the changelog (frozen at 3.0 — from v3 onward release notes live in GitHub Releases).

`Build.csproj` is a `Microsoft.Build.Traversal` project that references everything under `eng/src/tests/toys` — build/test/pack it to act on the whole repo. `StackExchange.Redis.slnx` is the IDE solution (XML SLNX format, not classic `.sln`).
`Build.csproj` is a `Microsoft.Build.Traversal` project that references everything under `eng/src/tests/toys` — build/pack it to act on the whole repo. `StackExchange.Redis.slnx` is the IDE solution (XML SLNX format, not classic `.sln`); it is also what `dotnet test --solution` runs, because tests use the Microsoft.Testing.Platform runner (selected in `global.json`), which cannot see a traversal project on the .NET 10 SDK.

## Build, test, pack

Expand All @@ -30,10 +30,14 @@ pwsh ./build.ps1 -StartServers
docker compose --file tests/RedisConfigs/docker-compose.yml up -d --wait

# Run the main test suite against one target framework (fastest inner loop)
dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -c Release -f net10.0
dotnet test --project tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -c Release -f net10.0

# Run a single test class / method (xUnit v3 + Microsoft.Testing.Platform)
dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -f net10.0 --filter "FullyQualifiedName~ClassName.MethodName"
# Run a single test class / method (xUnit v3 on Microsoft.Testing.Platform; names are fully qualified, '*' allowed at either end)
dotnet test --project tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -f net10.0 --filter-class StackExchange.Redis.Tests.ClassName
dotnet test --project tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -f net10.0 --filter-method StackExchange.Redis.Tests.ClassName.MethodName

# Every test project at once (what build.ps1 does)
dotnet test --solution StackExchange.Redis.slnx -c Release --no-build

# Pack the library
dotnet pack src/StackExchange.Redis/StackExchange.Redis.csproj --no-build -c Release /p:Packing=true
Expand Down
45 changes: 23 additions & 22 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<Project>
<ItemGroup>
<!-- Packages we depend on for StackExchange.Redis, upgrades can create binding redirect pain! -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.5" />
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="5.0.0" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.5" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.13" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.13" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.13" />
<PackageVersion Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" />
<PackageVersion Include="System.IO.Compression" Version="4.3.0" />
<PackageVersion Include="System.IO.Pipelines" Version="10.0.5" />
<PackageVersion Include="System.IO.Pipelines" Version="10.0.13" />
<PackageVersion Include="System.Text.Json" Version="10.0.13" />
<!-- note that this bumps System.Buffers, so is pinned in down-level in SE csproj -->
<PackageVersion Include="System.IO.Hashing" Version="10.0.12" />
<PackageVersion Include="System.IO.Hashing" Version="10.0.13" />
<!-- for RESPite -->
<PackageVersion Include="System.Buffers" Version="4.6.1" />
<PackageVersion Include="System.Memory" Version="4.6.3" />
Expand All @@ -26,35 +26,36 @@
3.3.3 - a version that cannot see descriptors declared outside the analyzer type, as ours are in
Diagnostics.cs. Pinned separately because it is a build-time analyzer we never ship: unlike the
reference above, its version has nothing to do with the host floor, so upgrade at will. -->
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.9.0" />
<!-- Packages only used in the solution, upgrade at will -->
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
<PackageVersion Include="GitHubActionsTestLogger" Version="3.0.2" />
<PackageVersion Include="GitHubActionsTestLogger" Version="3.0.5" />
<!-- analyzer test harness; deliberately newer Roslyn than the shipped floor above (the harness needs it,
and we cannot test at 4.3 without pinning the whole world) -->
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" Version="1.1.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" Version="1.1.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" Version="1.1.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" Version="1.1.4" />
<!-- lifts the transitive 9.0.6 that NuGet.Packaging (via the testing harness) brings in, which NuGet audit flags -->
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="10.0.13" />
<!-- StackExchange.Redis.CodeFixes overrides this down to the shipped floor (4.3.0); the version here is
the one the test harness needs, exactly as for the analyzer reference above -->
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.9.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="5.6.0" />
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageVersion Include="Microsoft.Testing.Platform" Version="2.1.0" />
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.9.50" />
<PackageVersion Include="Microsoft.Testing.Platform" Version="2.5.1" />
<!-- the platform's own TRX reporter (the report-trx switch); xunit's own report-xunit-trx truncated a 400KB report in 4.0.2 -->
<PackageVersion Include="Microsoft.Testing.Extensions.TrxReport" Version="2.5.1" />
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.10.94" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="NSubstitute" Version="6.2.0" />
<!-- 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" />
<PackageVersion Include="System.Reflection.Metadata" Version="10.0.5" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.3.151-beta" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.13" />
<PackageVersion Include="System.Reflection.Metadata" Version="10.0.13" />
<!-- For binding redirect testing, main package gets this transitively -->
<PackageVersion Include="System.Runtime.Caching" Version="10.0.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="xunit.v3.runner.console" Version="3.2.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="System.Runtime.Caching" Version="10.0.13" />
<PackageVersion Include="xunit.v3" Version="4.0.2" />
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions StackExchange.Redis.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<Project Path="tests/RedisConfigs/RedisConfigs.csproj" />
<Project Path="tests/RESPite.Tests/RESPite.Tests.csproj" />
<Project Path="tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj" />
<Project Path="tests/StackExchange.Redis.Build.Tests/StackExchange.Redis.Build.Tests.csproj" />
<Project Path="tests/StackExchange.Redis.FaultInjector.Tests/StackExchange.Redis.FaultInjector.Tests.csproj" />
<Project Path="tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj" />
</Folder>
Expand Down
8 changes: 5 additions & 3 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ if ($RunTests) {
& .\RedisConfigs\start-all.cmd
Write-Host "Servers Started." -ForegroundColor "Green"
}
Write-Host "Running tests: Build.csproj traversal (all frameworks)" -ForegroundColor "Magenta"
Write-Host "Running tests: StackExchange.Redis.slnx (all test projects)" -ForegroundColor "Magenta"
if ($NetCoreOnlyTests) {
dotnet test ".\Build.csproj" -c Release -f net8.0 --no-build --logger trx
# net10.0, not net8.0: the analyzer and fault-injector test projects only target net10.0, and the
# Microsoft.Testing.Platform runner fails a project that lacks the requested framework rather than skipping it
dotnet test --solution ".\StackExchange.Redis.slnx" -c Release -f net10.0 --no-build --report-trx
} else {
dotnet test ".\Build.csproj" -c Release --no-build --logger trx
dotnet test --solution ".\StackExchange.Redis.slnx" -c Release --no-build --report-trx
}
if ($LastExitCode -ne 0) {
Write-Host "Error with tests, aborting build." -Foreground "Red"
Expand Down
16 changes: 15 additions & 1 deletion docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ pointing to that primary server that will automatically be updated if the primar
var conn = ConnectionMultiplexer.Connect("localhost,serviceName=myprimary");
```

`abortConnect` applies to sentinel connections the same way it applies to everything else. With `abortConnect=true` (the default), `Connect`
throws if the sentinels cannot be reached or do not know the service. With `abortConnect=false`, `Connect` returns a multiplexer even then:
it is not connected, operations fail with a connection exception that names the problem, and it connects itself to the primary once the
sentinels become reachable and name one. If you want startup to fail when the sentinels are unavailable, keep `abortConnect=true`.

A sentinel-managed connection tracks failovers through several signals: the `+switch-master` announcement, a reconnect poll whenever the
connection to the primary is lost, and - because the announcement is delivered over pub/sub and can be missed - a periodic check that
notices when the node last named as primary is still connected but now reports itself as a replica (a failover where nothing disconnected,
such as a primary demoted in place). In that last case the client re-asks sentinel for the current primary and follows its answer, even to
a node at an address it has never seen; while sentinel still names the demoted node (sentinel itself has not yet failed over), the client
keeps asking without tearing down or reconfiguring the healthy connections.

An overview of mapping between the `string` and `ConfigurationOptions` representation is shown below, but you can switch between them trivially:

```csharp
Expand Down Expand Up @@ -412,6 +424,8 @@ The library determines whether to use RESP3 by:
- Otherwise: RESP3 is attempted if `defaultVersion` (6.0 unless overridden) is v6 or above

Note that `HELLO` is issued either way: `HELLO 2` when staying on RESP2. The reply tells us the server version, replication role, mode (standalone/sentinel/cluster) and connection
identifier, none of which we would otherwise know without `INFO` or `CONFIG GET` - both of which are in the `@dangerous` ACL category, and are commonly restricted. `HELLO` itself is
identifier, none of which we would otherwise know without `INFO` or `CONFIG GET` - both of which are in the `@dangerous` ACL category, and are commonly restricted. The role and mode
are only taken from the interactive connection (the subscription connection does no discovery), and a reported mode of `cluster` only leads to cluster *routing* once a slot map has
been obtained from `CLUSTER SLOTS`/`CLUSTER NODES`; a proxy that announces a cluster but refuses those commands is routed as a single standalone server. `HELLO` itself is
in the `@connection` category. If you need to prevent it (for example when talking to a proxy that does not understand it), disable it at the command-map level with `$hello=` in the
configuration string - or specify a `defaultVersion` below 6.0, since `HELLO` did not exist before then.
Loading
Loading