Repository navigation
Comparing changes
Open a pull request
base repository: StackExchange/StackExchange.Redis
base: 3.3.1
head repository: StackExchange/StackExchange.Redis
compare: 3.4.0
- 10 commits
- 60 files changed
- 4 contributors
Commits on Oct 1, 2026
-
Recover promptly from a cluster failover; fix the configuration chann…
…el under RESP3 and with a channel prefix (#3254) (#3255) * Repair primary/replica roles after a cluster failover (#3254) After a failover the slot map can be correct while the role flag on each node is stale. A write routed to a node still believed to be a replica is refused client-side, so no -MOVED comes back and nothing prompts a refresh; writes then fail until the next scheduled INFO replication check, i.e. up to configCheckSeconds. Reconfigures during that window ran but changed nothing, as they re-read the slot map and trusted the cached roles. - On -MOVED, a target still flagged as a replica is corrected before the resend: only a primary can own a slot, and the resend would otherwise be refused as a write to a replica. Cluster mode only, and not for -ASK. - When cluster topology is applied, set each known node's role from it. CLUSTER SLOTS wins where it lists the node, as it does for the slot map, with CLUSTER NODES covering nodes that serve no slots. Never creates a server. Measured against a local 6-node OSS cluster, failing over all three replicas under a write loop: ~55s of failures before, none after, under both RESP2 and RESP3. The in-process test server gains Failover(), and READONLY/READWRITE so a promoted replica's connection can leave read mode. * Subscribe to the configuration-change channel under RESP3 (#3254) The channel (__Booksleeve_MasterChanged by default) is how a client is told by hand that the topology has moved: a PUBLISH to it makes every subscribed client refresh. It was only ever subscribed in the subscription connection's handshake, and RESP3 has no such connection, so under RESP3 nothing was listening and the broadcast reached nobody (PUBSUB NUMSUB: 0 on every node). Subscribe on the interactive connection once it is known to be RESP3. That is deliberately not part of the handshake: a connection that fell back to RESP2 must not be put into subscriber mode. The channelPrefix behaviour of this channel is unchanged. * Apply the channel prefix when the library fires the configuration channel (#3254) The configuration channel is subscribed with ChannelPrefix applied, as any pub/sub channel is, and the public PublishReconfigure publishes it that way too. The library's own broadcasts - after ReplicaOf, and from MakePrimary - passed the name as a raw value instead, so with a prefix they were published to a name nobody was subscribed to and reached no one. Publish them as a channel so the prefix applies consistently, and document that it does: a manual PUBLISH has to use the prefixed name, and an ACL needs to grant it. Measured against 2.9.25 (published 2025-09-29) for comparison: it also subscribed with the prefix, and its receive side compared the prefixed wire name with the unprefixed setting, so with a prefix no broadcast was ever acted on, whichever way it was sent. Prefixed broadcasts are acted on now. * Assert the client type a RESP3 connection now has in ClientKill (#3254) Under RESP3 the interactive connection now holds the configuration-channel subscription, and the server counts any subscribed connection as a pubsub client rather than a normal one. ClientKill filtered on TYPE normal, so under RESP3 it matched nothing and killed nothing. The test now looks the target up in CLIENT LIST and asserts both its protocol and the client type that follows from it (pubsub under RESP3, normal under RESP2), then kills by that type.
Configuration menu - View commit details
-
Copy full SHA for 64e2d15 - Browse repository at this point
Copy the full SHA 64e2d15View commit details
Commits on Oct 2, 2026
-
Configuration menu - View commit details
-
Copy full SHA for c137c64 - Browse repository at this point
Copy the full SHA c137c64View commit details
Commits on Oct 7, 2026
-
Smart client handoffs: reinstate auto-enlistment for the hosted provi…
…ders and remove [Experimental] (#3248) * Reinstate auto-enlistment for the hosted providers 3.3.0 shipped maintenance notifications purely opt-in, with the provider overrides commented out rather than deleted so that what came back would be exactly what was reviewed. This is that, pending the thumbs up. The three overrides return in their original form: Redis Cloud, Azure Managed Redis and Redis Enterprise select Auto, so a connection string naming nothing but a recognized hostname gets the feature. Auto rather than Enabled throughout, because a deployment that cannot deliver has to keep working - the opt-in is refused and the feature stays off, rather than the connection being rejected. The library default stays Disabled: OSS Redis, Valkey and Garnet do not know the subcommand, and an unsolicited error on every connection would be a poor first impression. Docs follow the behaviour back: - ServerMaintenanceEvent's "do I need to configure anything?" returns to "usually not", with the two-column now/will-be table collapsed and the temporary notice dropped. Kept the `defaults=` table row that was added while the feature was opt-in, since it is useful either way. - Configuration.md and exp/SER010.md lose their "purely opt-in" paragraphs. - DefaultOptionsTests.MaintenanceNotificationDefaultPerProvider flips its first two rows to Auto, which is what it was written to do. The last two rows stay Disabled, and they are what stops the theory passing merely because every host returns the same answer. MaintenanceOptInClientTests.DefaultIsOff needed no change: it connects to the in-process server on loopback, which no provider recognizes, so Disabled is still the right answer there. The three API entries go back into Unshipped; eng/public-api.py --check agrees the file is sorted. Also preserved each source file's existing BOM state - RedisEnterpriseOptionsProvider.cs has none, and writing one in would have put an unrelated first-line change in the diff. * Retire SER010: maintenance notifications are not experimental any more The hosted providers now enlist by default, so callers on Redis Cloud, Redis Enterprise and Azure Managed Redis get this behaviour without touching a gated member; keeping the knobs behind [Experimental] no longer made sense. Removed the attribute everywhere, the [SER010] prefix from the PublicAPI entries, and the suppression from NoWarn and the maintenance toys. SER010 is added to the reserved-IDs list in Experiments.cs so it is never recycled. docs/exp/SER010.md stays, since the help URL points at it, rewritten in the retired form. The Redis Enterprise cluster-flag prerequisite that only lived there moves into ServerMaintenanceEvent.md, which also loses its experimental note and its stale "Disabled everywhere, until auto-enlistment lands" wording. * Update docs/ServerMaintenanceEvent.md Co-authored-by: Philo <[email protected]> --------- Co-authored-by: Philo <[email protected]>
Configuration menu - View commit details
-
Copy full SHA for 0fd86a1 - Browse repository at this point
Copy the full SHA 0fd86a1View commit details
Commits on Oct 8, 2026
-
docs: fix retry-category test in the custom RetryPolicy sample (#3265)
The ReadOnlyOnlyRetryPolicy sample in Failover.md tested the category with (fault.Flags & CommandFlags.CommandRetryReadOnly) != 0. The retry category is a 5-bit numeric field (CommandRetryNever spans it), not independent bits, so that test is also true for CommandRetryWriteChecked, CommandRetryServerAdmin and CommandRetryNever. Mask the whole field and compare instead.
Configuration menu - View commit details
-
Copy full SHA for 0eccaeb - Browse repository at this point
Copy the full SHA 0eccaebView commit details
Commits on Oct 9, 2026
-
Fix duplicate LoggerMessage event ids; add a guard test (#3266)
Three source-generated log events shared an EventId with another event: 116 and 117 (maintenance notifications vs. slot-map/undialled-server events, landed a day apart on parallel branches) and 99 (the two TLS "connection established" overloads, with different templates). LogErrorConnectionFailed had no explicit id at all and was generated as 0. Renumber the newer side of each pair (124, 125, 126) and give the connection-failed event 127. Add LoggerEventIdTests, which reflects over LoggerExtensions and fails if any [LoggerMessage] lacks an explicit EventId or if any id is used twice, reporting every problem at once and logging the next free id.
Configuration menu - View commit details
-
Copy full SHA for 3b3b9a4 - Browse repository at this point
Copy the full SHA 3b3b9a4View commit details -
docs(agents): add create-implementation-plan-for-redis-api-change ski…
…ll (#3268) * docs(agents): add create-implementation-plan-for-redis-api-change skill * docs(agents): align the planning skill with the Codex review of the sibling skills - agent guidance is the procedure, not data; supervised runs default to ./PLAN.md - decision_class: none for no-change plans; add rows may name files not yet on main
Configuration menu - View commit details
-
Copy full SHA for 731ecc6 - Browse repository at this point
Copy the full SHA 731ecc6View commit details -
Configuration menu - View commit details
-
Copy full SHA for 61867e6 - Browse repository at this point
Copy the full SHA 61867e6View commit details -
Report the outcome of every physical connection attempt, identifying …
…the client certificate used (#3257) * Report the outcome of every physical connection attempt, identifying the client certificate used Adds ConfigurationOptions.ConnectionAttemptCompleted, raised exactly once per physical connection attempt (initial connect or reconnect, interactive or subscription): success once the Redis handshake completes, or failure if the attempt ends first. Unlike ConnectionFailed, failed reconnects are not suppressed while a server stays unreachable. The args identify the client certificate by subject, issuer and SHA-1/SHA-256 thumbprint, snapshotted rather than retained: the certificate actually sent after a completed handshake, or the one the selection callback returned when the handshake itself failed. See #3252. * Report the TLS host name per connection attempt; list the new ConfigurationOptions field in ExpectedFields TlsHostName is what the library asked TLS to authenticate (and sent as SNI): the resolved host, or the caller's TargetHost when SslClientAuthenticationOptions is used; null when the library did not perform TLS. * Report the stage reached and the server certificate check per connection attempt ConnectionAttemptStage (Connect, Tunnel, Tls, Handshake, Established; spaced by 100 to leave room) says how far an attempt got. ServerCertificatePolicyErrors/ChainStatus/Accepted capture the server certificate validation, observed via a wrapping callback (platform-default semantics when none is configured); when that injected callback rejects, the AuthenticationException is re-described so observing does not cost the platform's error detail. The TLS block now resolves SslClientAuthenticationOptions before constructing the SslStream, and only wraps callbacks the options do not supply: SslStream rejects a constructor callback that differs from one in the options. * Address review: never let observing break a connection; classify AUTH failures correctly - Guard the certificate snapshot per value, and the whole reporting path: reading a certificate disposed mid-attempt throws (measured: CryptographicException), and this runs on the connection's own path, including establishing a healthy connection. - Rename ConnectionAttemptEventArgs to ConnectionAttemptCompletedEventArgs, matching the event. - Add SequenceNumber and CompletedTimeUtc, since handlers on worker threads can observe outcomes out of order. - Document the classification: a wrong password is Handshake + AuthenticationFailure (the server replied), a TLS 1.3 client-certificate rejection is Handshake + SocketClosed (the server hung up); plus guidance on corroboration, retrying the primary, fallback expiry and ordering; SHA-1 and PII notes. - Tests: wrong password (plaintext, and TLS with an accepted certificate), the documented classifier, a disposed certificate, and outcome sequencing. * Compute the SHA-256 client certificate thumbprint without copying on net8.0+ Use X509Certificate2.RawDataMemory, SHA256.HashData into a stack buffer and Convert.ToHexString, rather than GetRawCertData() + a SHA256 instance; down-level TFMs are unchanged. * Report abandoned connection attempts as such; keep refused-connect exceptions; fix attempt-order race in tests - ResetNonConnected (the initial-connect retry path) abandons any attempt in flight; report that attempt as ConnectionDisposed with an explanatory exception, rather than UnableToConnect with no exception, since it says nothing about the server. ConnectionFailed is unchanged. - BeginConnectAsync's catch blocks now pass the caught exception to RecordConnectionFailed, so a refused connection carries its SocketException. - Tests: outcomes are raised on workers, so arrival order is not attempt order; select the initial attempt by SequenceNumber rather than ConcurrentQueue.First() (the cause of the CI failure in RejectedClientCertificateMatchesTheDocumentedClassification). Add coverage for abandoned attempts and for refused attempts carrying an exception. - Docs: abandoned attempts say nothing about the certificate; the classification is a lower bound. * Report heartbeat connect timeouts with their exception; log the classified attempt - OnHeartbeat abandons a connect that has outlived the retry policy; report that attempt as UnableToConnect with the ConnectTimeout exception it builds, rather than letting Dispose report it as ConnectionDisposed with no exception. Seen on Windows CI, where a refused loopback connect takes long enough for the heartbeat to fire first. - Tests: add TimedOutAttemptCarriesTheTimeout (forces the heartbeat path on any OS); drop incidental stage assertions that raced the reset; log the attempt in the classification test, which still fails on Windows. * Classify Handshake + SocketFailure as a likely client-certificate rejection Measured on Windows CI (Windows Server 2025, net10.0): with TLS 1.3, SChannel surfaces the server's rejection of the client certificate as an IOException on the first read after the TLS handshake (inner Win32Exception describing the alert), so the attempt reports Handshake + SocketFailure rather than the Handshake + SocketClosed seen on Linux. - Docs and the test's mirror of the classification accept either failure type at Handshake; both are weak signals. - The classification test also logs the inner exception's HResult / NativeErrorCode, to judge whether the Windows alert can be recognised specifically.
Configuration menu - View commit details
-
Copy full SHA for 897120c - Browse repository at this point
Copy the full SHA 897120cView commit details -
Use a dedicated pub/sub connection under RESP3 by default (#3264)
* Use a dedicated pub/sub connection under RESP3 by default (#3263) The server classifies any connection with a live subscription as a pub/sub client and applies the pubsub output-buffer limits to it (32mb hard / 8mb for 60s by default). Since #3255 subscribed the configuration channel on the shared RESP3 interactive connection, ordinary large replies or pipelined bursts could get that connection - and every in-flight command on it - closed by the server. Add ConfigurationOptions.SharedSubscriptionConnection (sharedSubscriptionConnection=, default false): under RESP3, pub/sub now gets its own connection as under RESP2, unless sharing is opted into. - route via ServerEndPoint.SharesSubscriptionConnection() rather than KnowOrAssumeResp3(); connect monitors wait for the second leg - send QUIT once when the connection is shared - tests cover both modes; new PubSubOutputBufferTests reproduces the server-side kill with an oversized MGET, without changing server config - docs: Resp3.md / Configuration.md explain the limits and advise checking the server's pubsub limits before opting in * Create bridges atomically; don't re-activate pub/sub when not sharing CI saw a third socket (two Subscription sockets at the same timestamp): ActivateServer and OnFullyEstablished both lazily created the subscription bridge via a non-atomic `??=`, and a bridge connects as soon as it is created, so the loser leaked an unreferenced connection. The previous commit made OnFullyEstablished call Activate(Subscription) on every non-shared RESP3 connect, widening a previously rare race. - GetOrCreateBridge: CompareExchange the new bridge in; the loser is disposed (closing its connection) - OnFullyEstablished only activates pub/sub for the shared-RESP3-but-got- RESP2 case it was written for; otherwise ActivateServer has done it
Configuration menu - View commit details
-
Copy full SHA for c65cbcd - Browse repository at this point
Copy the full SHA c65cbcdView commit details -
Bump to 3.4 and ship the 3.4.0 API surface
Sets version 3.4 in version.json, keeping versionHeightOffset -1 (scoped to 3.4 via versionHeightOffsetAppliesTo) so this commit computes as 3.4.0. Promotes every PublicAPI.Unshipped.txt entry to its sibling Shipped.txt via eng/public-api.py --promote, ahead of the 3.4.0 cut. Only the StackExchange.Redis root file had unshipped entries (ConnectionAttemptCompleted and its event args, SharedSubscriptionConnection, and the per-provider MaintenanceNotifications overrides); the net6.0 and RESPite files were already empty.
Configuration menu - View commit details
-
Copy full SHA for e7c43a2 - Browse repository at this point
Copy the full SHA e7c43a2View commit details
This comparison is taking too long to generate.
Unfortunately it looks like we can’t render this comparison for you right now. It might be too big, or there might be something weird with your repository.
You can try running this command locally to see the comparison on your machine:
git diff 3.3.1...3.4.0