Conversation
Confirms the premise: this is the client-side geographic failover feature family, and Lettuce, Jedis and redis-py have all shipped it. It feeds exactly the health checks our Availability/ namespace already has behind SER007. The abstractions line up almost one-to-one with redis-py's - probe, policy (all/any/majority), circuit breaker, per-member config - and two details make it land cleanly: HealthCheckProbe has no internal abstract members, so it is already subclassable from outside the assembly, and ConnectionGroupMember already carries a per-member HealthCheck override, which is what a per-cluster REST endpoint needs. Where we are better placed: HealthCheckResult has Inconclusive, which Lettuce's two-state HealthStatus lacks. Lettuce maps a failed REST call to UNHEALTHY, so a management-plane outage is indistinguishable from the data plane being down and can fail over a database that is serving fine. We can say "could not determine" instead. The awkward part is that src/ has no HTTP client and no JSON parser. JSON turns out to be avoidable if the bdb uid is configured rather than discovered, since the availability call is a status-code check. Also records the defaults disagreeing across clients: 100ms everywhere - server, docs, redis-py - but 5000ms in Lettuce.
mgravell offered to add one; the shape is what decides whether it helps. A member returning HttpMessageHandler would drag System.Net.Http into StackExchange.Redis's public API for the sake of one commercial deployment target - which is the thing keeping this in a separate package was meant to avoid, decided the expensive way. A stream-shaped member instead composes into SocketsHttpHandler.ConnectCallback, which takes exactly a ValueTask<Stream>, so the Enterprise side gets a complete HTTP hijack and core gains no HTTP types at all. Tunnel already speaks that language via BeforeAuthenticateAsync and ConnectTransportAsync, and adding a virtual is binary-safe for subclasses. Two functional arguments for Tunnel over a separate knob, beyond testing: a CONNECT proxy configured for Redis almost certainly applies to the Enterprise REST API too, so co-locating makes that correct by construction; and the test harness already assigns Tunnel, so one assignment would spoof both planes. Caveats recorded: ConnectCallback is net5+, the existing members are EndPoint/ConnectionType-shaped where HTTP wants a Uri, and widening Tunnel's remit means HttpProxyTunnel and LoggingTunnel each need an opinion about whether they apply to the management plane. Also retires open question 5: RawConfig and ConfigurationOptions.Tunnel are both already public, so a probe reaches the tunnel from the context it already gets - no HealthCheckContext change needed.
I had written that inducing "reachable but stale" on demand is not something anyone can do in CI, so the fakes were the only route to the scenario that matters. That is wrong. Redis ships a fault injection service that the client suites drive over HTTP - POST /action with a type and parameters, poll the returned action_id - and Lettuce's ActiveActiveFailoverScenarioTest injects network_latency against a bdb_id for a bounded duration, which is exactly the mechanism for pushing replication lag past the tolerance and watching the lag-aware check flip while the plain check stays green. So the authoritative end-to-end test is real cluster plus fault injector, on a road Lettuce has already built. The fakes are demoted rather than removed: they still cover machines with no Enterprise cluster, they are far faster than an injector action that budgets minutes plus a stabilisation wait, and they cover what the injector does not obviously reach - specific error_code bodies, 401/403 credential expiry, slow-but-successful responses for probe timeouts. Also records what the real clusters are uniquely good for: everything in section 1 is read off documentation, and a short session settles whether extend_check=lag is actually supported, what failure bodies really contain, whether the Host header is required, what the bdbs listing really returns - which decides whether the JSON-avoidance argument holds - and what lag real geo links show, which is the empirical way to settle 100ms versus 5000ms.
… questions It is not merged - tests/StackExchange.Redis.FaultInjector.Tests lives on marc/maint-optin-server, open as #3191 (107 files, +13160) - which is presumably why I did not find it when I grepped a branch taken fresh from main. Two pieces of it bear directly on this. FaultInjectorClient wraps the same POST /action + GET /action/{id} contract go-redis, redis-py and node-redis converged on, and adds GetValidTriggersAsync, so we can ask a deployment which lag-inducing effects it supports instead of guessing at an action name. And ClusterRestClient is already a Redis Enterprise REST client on 9443 - Basic auth, System.Text.Json, GET /v1/bdbs?fields=uid,name - so the availability call is a method on an existing class, not a new project. It also settles by observation what I had listed as open and sourced only from documentation: the management certificate is self-signed per environment and pinned to a CA from the config directory, so the probe needs CA configuration and cannot rely on the ambient trust store; auth really is Basic; and the bdbs listing returns the requested fields, so discovery is easy - which weakens the avoid-JSON argument to "it keeps JSON out of src/", not "discovery is hard". Sequencing consequence recorded: an end-to-end test is downstream of #3191. And #3191's own summary is the best argument for validating before building - same vendor, same class of API: the specifications are prose, the payloads were never published, and nearly every starting assumption was wrong.
Step 0 is to wait for #3191 to merge and build from there, rather than writing a second Redis Enterprise REST client against the same cluster and reconciling the two later - its ClusterRestClient already does Basic auth, CA pinning and GET /v1/bdbs, and its FaultInjectorClient can be asked which lag-inducing effects a deployment supports. The one thing that can start earlier is the API validation, because it produces notes rather than code. Step 1 is that validation, before any implementation: a checklist of things currently taken from documentation, including the measurement that settles 100ms versus 5000ms, with captured bodies saved as fixtures so the stubbed tests replay real payloads. Step 2 deliberately does not decide where the code lives, but records that the dependency argument is weaker than it looked - System.Net.Http needs no package reference on any target we ship - so the question is judgement, not constraint. Also records that this branch becomes the feature branch when step 0 clears, and that the way to take the merge is to rebase onto main rather than merge main in, since #3191 squash-merges.
…cess #3191 merged as dc915bb, so this branch is rebased onto it - which is what step 0 asked for, and rebased rather than merged because #3191 squash-merged and a merge would have moved the merge base without bringing its ancestry. The fault-injector tier is present here now. Also recorded where the work can happen. Step 1 is validation against a real deployment plus the fault injector, and both live on the machine holding the environment directory; this box has no ~/aws and no env_output.json anywhere, so nothing in step 1 could be run or verified here. Worth writing down rather than rediscovering, since the environment being absent looks identical to the environment being offline. Steps 2 onward are ordinary code and are not tied to a machine. Noted too that the directory is the AWS multi-cluster template, so env_output.json nests its outputs under .clusters.value[0] - a shape FaultInjectorEnvironment already handles, so it needs no work, just no surprise.
mgravell
force-pushed
the
marc/lag-aware-availability
branch
from
September 18, 2026 04:34
cce049c to
916a563
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Important
Blocked on #3191, and not only a planning branch.
This currently adds notes and no product code. It is opened as a draft now so the investigation is
reviewable while the dependency clears — this branch becomes the feature branch once
#3191 merges, with the
implementation landing on top of these same commits rather than starting again elsewhere.
The plan is a proposal. Where it leans, it says so; where the evidence does not decide, it leaves
the question open rather than inventing an answer.
What this is
Investigating whether Redis Enterprise's
lag-aware database-availability API
belongs in the geo-redundant failover health checks in
src/StackExchange.Redis/Availability/(currently behind
SER007), usingLettuce's
LagAwareStrategyas the reference implementation.
notes/lag-aware/findings.mdnotes/lag-aware/plan.mdnotes/readme.mdnotes/convention (see "overlap" below)Findings worth surfacing
LagAwareStrategy; redis-py hasLagAwareHealthCheck. It is the client-side geographic failoverfamily. The check is not liveness —
extend_check=lagasks whether a replica is synchronisedenough to be a safe failover target, which is what stops a failback onto a region that is
reachable but stale.
HealthCheckProbe↔AbstractHealthCheck,AllSuccess/AnySuccess/MajoritySuccess↔HEALTHY_ALL/ANY/MAJORITY, Ping default,circuit breaker, per-member configuration. Two details make it land cleanly:
HealthCheckProbehas no internal abstract members, so it is already subclassable from outside the assembly; and
ConnectionGroupMemberalready carries a per-memberHealthCheckoverride, which is exactly whata per-cluster REST endpoint needs.
explicit recommendation, and redis-py — but 5000 ms in Lettuce. Fifty times more permissive
than its own documentation advises. The plan measures rather than copies.
HealthCheckResulthasInconclusive;HealthStatushas two states, and Lettuce maps a failed REST call toUNHEALTHY— so amanagement-plane outage is indistinguishable from the data plane being down, and can fail over a
database that is serving traffic perfectly. Proposed as a deliberate, documented deviation.
src/may need no changes at all. The probe is subclassable,RawConfigandConfigurationOptions.Tunnelare public. The only candidate core change is an optionalTunneladdition so the management plane honours tunnels — and if we do that, it should return a stream,
composed into
SocketsHttpHandler.ConnectCallbackby the consumer, so noSystem.Net.Httptypeenters the public API.
Corrections made while investigating
Recorded rather than quietly fixed, because the reasoning is the useful part:
the only route to the scenario that matters. Wrong twice: Redis ships a fault injection service
for exactly this, and we already have a test tier built on it — in Server-native maintenance notifications: opt in, react, hand off, and recover when nothing is announced #3191.
System.Net.Httpneeds no packagereference on any target we ship.
System.Text.Jsonis the only genuinely new dependency, and itis avoidable. So "where does this live" is a judgement call about scope, not a technical
constraint.
Why step 0 is "wait"
#3191 carries
tests/StackExchange.Redis.FaultInjector.Tests, and with itClusterRestClient—already a Redis Enterprise REST client on 9443 doing Basic auth, CA pinning and
GET /v1/bdbs?fields=…— plus aFaultInjectorClientthat can be asked which lag-inducing effectsa deployment supports. Starting now would mean writing a second REST client against the same cluster
and reconciling them at merge, for a feature whose implementation is smaller than the client that
tests it.
That tier also settled three things empirically that I had only from documentation: the management
certificate is self-signed per environment (so the probe needs CA configuration, not ambient
trust), auth really is Basic, and the
bdbslisting returns the requested fields.The one thing that can start earlier is validating the availability API against a real deployment,
because it produces notes rather than code — and #3191's own summary is the argument for doing it
first: "the specifications are prose, the payloads were never published, and nearly every assumption
I started with was wrong in some way that mattered."
Deliberately undecided
plan leans toward a separate package and says why, but this is a scope judgement.
Tunneladdition happens at all — separable; the probe works without it.Overlap to be aware of
notes/readme.mdalso appears in#3241, which introduced the
notes/convention. Whichever merges second gets a one-line conflict in its Topics list; the rest ofthe file is identical.
Draft until #3191 lands.