Skip to content

feat(client): multi-db client with automatic endpoint failover - #3435

Draft
nkaradzhov wants to merge 10 commits into
redis:masterfrom
nkaradzhov:multi-db
Draft

feat(client): multi-db client with automatic endpoint failover#3435
nkaradzhov wants to merge 10 commits into
redis:masterfrom
nkaradzhov:multi-db

Conversation

@nkaradzhov

Copy link
Copy Markdown
Collaborator

Adds a multi-database client to @redis/client: one drop-in client backed by N member
databases (standalone/pool/cluster/sentinel). All traffic goes to one weight-selected
active member; the client fails over when it goes down. Every new export is @experimental.

Each factory returns { client, controller }. client has exactly the base client type:
commands forward to the active member, lifecycle methods fan out to all members.
controller holds the multi-db surface: topology, weights, runtime add/remove, events.

Done: per-member circuit breaker, atomic switch primitive, config defaults and
validation, connect() with initial health checks and an all/majority/one
availability gate, weighted selection, runtime add/remove/setWeight, PING health check
with ALL/MAJORITY/ANY probe policies. Unit and docker integration tests.

Remaining: failover engine, pub/sub transfer and per-topology failure signals,
background health checks and auto-fallback, pluggable detectors and the lag-aware check,
forced failover with pinning, docs and example.

Additions only — no existing exported type or behavior changes.

🤖 Generated with Claude Code

nkaradzhov and others added 5 commits August 26, 2026 15:47
Sketch of a multi-database client wrapper managing a homogeneous array of
underlying clients (standalone/pool/cluster/sentinel) behind one drop-in
client surface. Type mechanics only — failover/health/routing stubbed.

- @redis/client: dedicated factories (createMultiDbClient/Pool/Cluster/
  Sentinel) returning { client, controller }; client typed exactly as the
  base client (true drop-in), controller holds multi-db-only admin surface.
  Command forwarding via prototype-walk (no runtime Proxy on hot path).
- redis meta-package: createMultiDbClient wrapper injecting default Stack
  modules, mirroring createClient.
- playground.ts for manual poking.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…stubs

Move MultiDbManager/MultiDbController out of index.ts, add typed stubs for
config/circuit/database/failure-detector/health-check/failover-strategy/errors
per the multi-db API contract, export the public surface from the package
index, accept flat MultiDbConfig options in all factories, and drop the
playground script (superseded by the quickstart smoke script).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…per, switch primitive

- config.ts: defaults table + resolution/validation (weights in [0,1],
  unique ids with generated db-<n> fallback, health-check timing, >=1 db)
- circuit.ts: CLOSED/OPEN/HALF_OPEN machine with clock-derived HALF_OPEN,
  grace-period restart on probe failure, consecutive-probe counting
- database.ts: member wrapper (id/weight/circuit/role) with client
  lifecycle listeners feeding circuit and role; listeners are disposed
  only after close()/destroy() so a teardown-window 'error' emit cannot
  crash the process
- manager.ts: atomic switch primitive with typed controller events and
  non-awaited old-member housekeeping; per-command result hook wired
  through the forwarding closures for the upcoming failure detector
- controller.ts: typed event map (failover/fallback/database-unhealthy/
  database-recovered/all-databases-down/error), descriptor-based
  getDatabases()/getActiveDatabase()
- comments follow the constitution v1.0.0 principles: contract JSDoc
  (+@experimental) on the public surface, why-only implementation
  comments, cross-module constraints with file:symbol references
- unit tests for circuit transitions and config resolution (no docker)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Remove spec-artifact references from comments (requirement IDs, research
citations, data-model mentions) — the spec docs don't ship, so the
references would dangle for external readers; each comment now carries
its rationale inline. Task/story markers on unimplemented stubs stay as
development scaffolding and are removed by the change that implements
each stub.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- health-check.ts: default PING check, chain runner with per-probe
  timeout bounding the whole chain, ALL/MAJORITY/ANY early-exit round
  aggregation, round budget used as the member readiness bound
- manager.ts: initial connection flow — fan-out connect with bounded
  establishment, per-member initial health check, initialAvailability
  gate ('all'/'majority'/'one') with full teardown on rejection,
  weight-based active selection with config-order tiebreak; runtime
  addDatabase (pre-opened circuit until established, skipInitialHealthCheck
  honored only here), removeDatabase (active switches to replacement
  first), setWeight; repeat connect() re-probes instead of tripping
  circuits of already-open members
- index.ts: per-topology MemberAdapter (create + keyless sendCommand)
  wired through all four factories into manager construction
- controller.ts: addDatabase/removeDatabase/setWeight admin surface
- config.ts: probe-knob validation (numProbes, timeout, delays, empty
  healthChecks), shared per-member identity resolution
- tests: probe-policy unit matrix (no docker); docker integration suite —
  weighted selection, availability matrix with dead members, runtime
  reconfiguration incl. skip-flag semantics, repeat connect, drop-in typing

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
nkaradzhov and others added 5 commits September 2, 2026 14:38
…g, pub/sub transfer

- failure-detector.ts: sliding-window DefaultFailureDetector (count AND
  rate thresholds, 0 disables a condition, error filter counts filtered
  errors as traffic but not failures; injectable clock)
- failover-strategy.ts: WeightBasedStrategy — highest-weight CLOSED
  member, member-order tiebreak
- manager.ts: detection→failover wiring — per-command outcomes and member
  lifecycle errors feed the detector with source attribution (a stale
  in-flight rejection from the previous active must not trip the new
  one); failover procedure opens the failed circuit, switches to the
  strategy's pick, or gates traffic and retries selection up to
  maxFailoverAttempts before going permanently unavailable; commands
  fail fast with TemporarilyUnavailableError while searching and
  PermanentlyUnavailableError after exhaustion
- pub/sub transfer on switch (standalone members): listeners are removed
  from the old member and re-subscribed on the new one, so a recovering
  old member cannot double-deliver
- database.ts: per-topology signal mapping — sentinel per-node errors
  arrive via client-error and a sentinel-internal master change must not
  open the member circuit; cluster node errors aggregate into 'error'
- tests: detector/strategy unit suites; failover integration (kill
  active → event + traffic continuity, in-flight rejection, pub/sub
  survival, all-down escalation); topology integration (cross-cluster
  failover, sentinel no-false-failover during its own master change,
  cross-sentinel-deployment failover)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- manager.ts: per-member unref'd health scheduler covering the active
  member too (a silently dead server never trips the organic detector);
  OPEN members rest through their grace period, HALF_OPEN members get
  recovery probes fed straight into the circuit (closing emits
  database-recovered), CLOSED members failing a round open with
  database-unhealthy — or fail over with reason 'health-check' when
  active; recovery probing keeps running during the all-down search so
  attempts can succeed; permanent unavailability stops all timers
- auto-fallback loop (disabled by default): returns traffic to a
  strictly higher-weight healthy member, emitting 'fallback';
  controller.setAutoFallback(intervalMs | false) retunes it at runtime
- passive members that end or fail checks announce database-unhealthy
  without switching; deliberate removals stay silent
- tests: recovery closes the circuit only after grace + probes (no
  flapping on a fast-restarting member), auto-fallback on/off/runtime
  toggle, passive failure reported without failover

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…alth check

- lag-aware-health-check.ts: probes the RE cluster REST API's database
  availability endpoint with lag verification (extend_check=lag +
  availability_lag_tolerance_ms, default 5s tolerance) via the global
  fetch — no new dependency; basic auth, per-database endpoint/uid
  resolvers for members on different clusters, request timeout, and a
  requestOptions escape hatch for custom TLS dispatchers
- custom failure detectors, chained health checks and custom failover
  strategies were already threaded through config — now exercised
  end-to-end and exported (LagAwareHealthCheck added to the public
  surface)
- tests: stub-HTTP-server suite for the lag-aware check (availability,
  lag/auth failures, timeout, unreachable endpoint, per-database
  resolvers); integration cases for an error-filtering detector, a
  user-supplied detector driving failover (and being reset by the
  switch), and an all-must-pass check chain

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- manager.ts: setActiveDatabase(id) health-checks the target first —
  present reality overrides a stale OPEN circuit, so a verified target
  is closed (announced recovered) before the forced switch; the forced
  selection pins, suspending auto-fallback until releasePin(); any
  automatic switch away from the pin clears it, so a pin never traps
  traffic on a dead member; a successful force also rescues a client
  mid-search with every member down
- controller.ts: setActiveDatabase/releasePin admin surface
- tests: pin holds against auto-fallback until released; unhealthy
  target rejected; automatic failover off a dead pinned member clears
  the pin (proven by the fallback loop resuming); same-member force
  pins without a switch; force-rescue during the all-down search

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- docs/multi-db.md: quick start, selection and failover lifecycle,
  configuration reference with defaults, controller and events
  reference, custom checks incl. the lag-aware example, and the
  behavior contracts (in-flight rejection, eventual consistency,
  pub/sub loss window and per-topology transfer support, per-member
  client-side caching, resource overhead)
- examples/multi-db-failover.js: runnable two-container demo of
  failover, recovery and fallback
- @experimental on every public multi-db export
- client-side caching regression test: no stale reads across a switch,
  no flush needed
- topology coverage: pool failover, per-factory typing assertions,
  forced-pin smokes on cluster and sentinel members, sentinel
  fallback-after-recovery; the kill tests accept either automatic
  failover reason — the organic detector and the background health
  check legitimately race on slow-to-reject topologies

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant