Skip to content

3.1 - Client-side geographic failover - #3025

Merged
mgravell merged 38 commits into
mainfrom
marc/aa2
Jul 31, 2026
Merged

3.1 - Client-side geographic failover#3025
mgravell merged 38 commits into
mainfrom
marc/aa2

Conversation

@mgravell

@mgravell mgravell commented Mar 5, 2026

Copy link
Copy Markdown
Collaborator
  • basic connect
  • automatic ongoing active group detection
  • command routing
  • pubsub routing
  • improved configuration for down-detection via circuit-breaker (not just health-check)
  • align config API with other libraries (slightly complicated because of multiplexer vs pool, i.e. there are some fundamental differences between the cores)
  • command retry
    • transactions and batches
  • review config APIs - there are 4 in here (retry, circuit-breaker, health-check, multi-group), and they're not 100% in alignment

discussion topics / follow up items:

  • naming; prefer "active active" over "multi group" - clearer?
  • can we support it at the single connection string level?
  • pubsub unsubscribe

@mgravell
mgravell marked this pull request as ready for review March 16, 2026 11:30

@uglide uglide left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main missing peaces at this stage:

  • Circuit breaker/Failure detector on the organic load
  • Retries
  • Per-member health checks
  • Limited configuratbility and deviations in the default values (see inline comments)

Comment thread src/StackExchange.Redis/Availability/HealthCheck.cs Outdated
Comment thread src/StackExchange.Redis/Availability/MultiGroupOptions.cs Outdated
Comment thread src/StackExchange.Redis/Interfaces/IConnectionGroup.cs
mgravell added 14 commits July 10, 2026 10:39
* move things around and write core

* bucket logic

* buildable

* nit

* add unit tests

* end-to-end tests

* docs

* improve circuit-breaker reactivity

* gate health-checks, since it can now run concurrently due to circuit-breaker

* end-to-end tests
* start dedup core

* find classes

* start stubbing class

* successfully stub out all methods (it compiles!)

* stub out execute and tuples

* generalize mutator API to include channels

* wip

* fundamentals: compile (untested)

* I *think* this is the core retry logic!

* nit

* fix build

* iterating on RetryPolicy

* nits

* start command categorization

* cleanup API

* duplicate .Database into IDatabaseAsync (from IDatabase)

* unit tests

* end to end test, but it highlights a design fault

* retry logic

* nits

* end-to-end tests

* docs

* namespace
@mgravell mgravell changed the title multi geo support Active:Active support Jul 17, 2026
@mgravell mgravell changed the title Active:Active support Active:Active support (3.1) Jul 17, 2026
* Add retryable transactions to WithRetry

Introduces a retryable transaction, obtained via WithRetry(...).CreateTransaction().

Mechanism: RetryTransaction is an [AutoDatabase] recorder. The generator funnels each
builder call into (state, projection) - the same captured unit RetryDatabase replays -
which RetryTransaction records while handing the caller a durable, still-incomplete proxy
task. ExecuteAsync runs the retry loop: each attempt spins up a fresh one-shot inner
transaction against the underlying database (for multi-group this resolves the currently
active member, so a retry after failover lands on the new member), replays the recorded
ops and constraints onto it, and awaits it. On clean completion the per-attempt outcomes
are forwarded onto the durable proxies; on a transient fault the attempt is discarded and
replayed; on terminal failure the proxies are faulted.

The transaction's effective retry category is the most side-effecting of its operations
(WATCH constraints excluded); it is injected onto the EXEC flags so the resulting fault
carries it and the shared RetryPolicy/FaultContext gating applies exactly as for a single
command. Retry policy math is extracted into a shared RetryController used by both
RetryDatabase and RetryTransaction.

Surface (all [Experimental] SER007):
- new ITransactionAsync (async-only transaction; sibling of ITransaction, no shipped-API change)
- new IRetryDatabase with CreateTransaction; WithRetry now returns it
- RedisTransaction/KeyPrefixedTransaction also implement ITransactionAsync

Includes end-to-end tests: rides out a transient EXEC failure, respects the aggregate
category gate, and does not double-apply a discarded attempt.

* Move CreateTransaction onto IDatabaseAsync

Reworks the retryable-transaction surface so WithRetry keeps returning IDatabaseAsync
and RetryDatabase never has to implement IDatabase (which would drag in the sync
surface). CreateTransaction now lives on IDatabaseAsync, returning the async-only
ITransactionAsync; IDatabase.CreateTransaction refines the return type to ITransaction
via "new", with ITransaction : IBatch, ITransactionAsync.

To avoid any ambiguity, ITransaction re-declares AddCondition/ExecuteAsync(flags) with
"new" rather than relying on inheritance, so its surface is self-contained and its
shipped-API lines are unchanged.

- IDatabaseAsync gains CreateTransaction(): ITransactionAsync ([Experimental] SER007)
- IDatabase.CreateTransaction is now "new ITransaction ..." (signature unchanged)
- ITransaction : IBatch, ITransactionAsync (re-declares its two async members)
- the source generator skips CreateTransaction/CreateBatch (not replayable round-trips)
- explicit IDatabaseAsync.CreateTransaction bridges on RedisDatabase, MultiGroupDatabase,
  KeyPrefixedDatabase; KeyPrefixed<TInner> base throws (reimplemented by the database);
  RetryTransaction throws (nested)
- WithRetry returns IDatabaseAsync again; IRetryDatabase removed

All retry/transaction/batch/keyprefix tests pass; full Release build across all TFMs.

* Make ITransactionAsync stable (drop [Experimental])

ITransaction (stable) derives from ITransactionAsync, so the type must be stable too;
the experimental gating stays on IDatabaseAsync.CreateTransaction (the new entry point).
Also drops the now-redundant explicit ITransactionAsync from RedisTransaction and
KeyPrefixedTransaction (implied by ITransaction : ITransactionAsync).

* Drop IDatabase requirement from retry transaction

Now that CreateTransaction is on IDatabaseAsync, RetryTransaction only needs an
IDatabaseAsync source: it creates each attempt's transaction via source.CreateTransaction()
and uses only AddCondition/ExecuteAsync (both on ITransactionAsync). Removes the
IDatabase cast and its NotSupportedException path, so WithRetry(...).CreateTransaction()
works over any IDatabaseAsync.
@mgravell mgravell changed the title Active:Active support (3.1) 3.1 - Geo-Redundant client failover Jul 30, 2026
@mgravell mgravell changed the title 3.1 - Geo-Redundant client failover 3.1 - Client-side geographic failover Jul 30, 2026
@mgravell
mgravell merged commit 06b2eef into main Jul 31, 2026
8 of 10 checks passed
@mgravell
mgravell deleted the marc/aa2 branch July 31, 2026 07:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants