Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: StackExchange/StackExchange.Redis
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 3.3.1
Choose a base ref
...
head repository: StackExchange/StackExchange.Redis
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 3.4.0
Choose a head ref
  • 10 commits
  • 60 files changed
  • 4 contributors

Commits on Oct 1, 2026

  1. 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.
    mgravell authored Oct 1, 2026
    Configuration menu
    Copy the full SHA
    64e2d15 View commit details
    Browse the repository at this point in the history

Commits on Oct 2, 2026

  1. Configuration menu
    Copy the full SHA
    c137c64 View commit details
    Browse the repository at this point in the history

Commits on Oct 7, 2026

  1. 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]>
    mgravell and philon-msft authored Oct 7, 2026
    Configuration menu
    Copy the full SHA
    0fd86a1 View commit details
    Browse the repository at this point in the history

Commits on Oct 8, 2026

  1. 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.
    mgravell authored Oct 8, 2026
    Configuration menu
    Copy the full SHA
    0eccaeb View commit details
    Browse the repository at this point in the history

Commits on Oct 9, 2026

  1. 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.
    mgravell authored Oct 9, 2026
    Configuration menu
    Copy the full SHA
    3b3b9a4 View commit details
    Browse the repository at this point in the history
  2. 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
    uglide authored Oct 9, 2026
    Configuration menu
    Copy the full SHA
    731ecc6 View commit details
    Browse the repository at this point in the history
  3. Configuration menu
    Copy the full SHA
    61867e6 View commit details
    Browse the repository at this point in the history
  4. 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.
    mgravell authored Oct 9, 2026
    Configuration menu
    Copy the full SHA
    897120c View commit details
    Browse the repository at this point in the history
  5. 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
    mgravell authored Oct 9, 2026
    Configuration menu
    Copy the full SHA
    c65cbcd View commit details
    Browse the repository at this point in the history
  6. 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.
    mgravell committed Oct 9, 2026
    Configuration menu
    Copy the full SHA
    e7c43a2 View commit details
    Browse the repository at this point in the history
Loading