You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
epic(distributed): shared backends and standalone workers for multi-node deployments #613
#583 makes one process serve many tenants. This epic makes many processes serve them: every stateful layer that is embedded today gets a shared, external implementation, the background loops can run outside the API process, and a deployment chooses the implementation of each layer at boot. The embedded implementations stay the default, so a standalone operator notices no change. The work is being built alongside #583 and ships after it.
Decisions (2026-09-24)
Implementations are chosen per process, not per tenant. The boot config (config.yaml / WH_*) picks each layer's implementation; a tenant's config.json keeps only its own tunables (budgets, TTLs, enabled switches).
External NATS: WaveHouse does not own the streams. A NATS Kubernetes operator creates and owns the streams, consumers and policies, shared across tenants rather than one set per tenant. WaveHouse checks at boot that they exist and never creates or edits them. The embedded implementation keeps its per-tenant streams (feat(mq): give every tenant a queue of its own #612).
Remote dedupe stays pluggable. Whether a tenant is pinned to a home region or must be deduplicated globally is undecided. DynamoDB in one region is built first; the multi-region-strong-consistency and Scylla paths are designed and documented.
A. Tell ClickHouse failures from bad rows in batch inserts. An unreachable, overloaded or read-only ClickHouse is retried with backoff and never dead-lettered; only a row ClickHouse rejects goes through row isolation and the DLQ.
B. Coordination primitive. Leases and locks, with an in-process implementation and a distributed one, for the work in C.
E. Shared cache: Redis / Dragonfly. Redis-compatible cache.Cache with version namespaces shared across pods, so an invalidation on one pod reaches every pod.
Done: every workstream is on main; the last, D with B2, landed in #624. The embedded backends remain the default. Before production traffic relies on external NATS, the 3-node checks in #694 should run.
NATS v2 decisions (2026-09-29) — the external-NATS stack gets these on top of #654 before it lands:
Ingest streams use work-queue retention (not interest); history is fed by republish (wh.hist.<tenant>.<table>), best-effort: SSE replay may miss rows while history is unavailable, ClickHouse never does.
N partitions × V shards (V default 32, fixed per partition); a table maps to a shard by a consistent hash of tenant+table; one operator-owned durable per shard with the pinned-client priority policy, so one worker owns a shard at a time. Workers scale freely without moving data; WaveHouse may reset/unpin those durables on takeover (never creates, deletes or edits them).
Order is not promised across an N or V change, for moved tables only. A flooding tenant touches every partition; per-tenant budgets are a later follow-up.
No sweeper in NATS mode: retention is stream policy.
Real nack reconciliation of the new fields gets a one-off manual check before production.
Area: mq / cache / dedupe / ingest / app — distributed deployment epic · scoped with @EricAndrechek, 2026-09-24
#583 makes one process serve many tenants. This epic makes many processes serve them: every stateful layer that is embedded today gets a shared, external implementation, the background loops can run outside the API process, and a deployment chooses the implementation of each layer at boot. The embedded implementations stay the default, so a standalone operator notices no change. The work is being built alongside #583 and ships after it.
Decisions (2026-09-24)
config.yaml/WH_*) picks each layer's implementation; a tenant'sconfig.jsonkeeps only its own tunables (budgets, TTLs, enabled switches).Workstreams
mq.Broker. Shared streams owned by the operator, built on feat(mq): give every tenant a queue of its own #612's per-tenant interfaces.cache.Cachewith version namespaces shared across pods, so an invalidation on one pod reaches every pod.internal/app.PRs (all merged to main by 2026-09-30)
Every stack was squash-merged to
main, children collapsed into their roots: #647, #632, #616, #655, #619 (with #627), #618, #615, #622, #623, #614 (E stack), #625 (F stack), and #624 (the whole D stack plus B2: #636, #639, #644, #646, and the fix rounds #695/#696).internal/coordleases, sweeper elected → feat(app): process roles #622 C1 process roles + ops-only listenerRetryRefreshexponential backoff for clustered mode #141), on maincache.backend: rediswiring + two-instance test · fix(pipes): run write pipes every call, uncached and uncoalesced #634 write pipes uncached (fixes bug(pipes): mutation pipe results are cached and coalesced — the write silently drops on repeat calls #386), on E1dedupe.backend: dynamodbwiringmqtestconformance +ErrUnavailable· feat(mq): external nats broker with sharded, pinned ingest workers #624 D2 operator topology, verifier, manifests (S1 passed) → feat(mq): a Broker over an external NATS cluster #636 D3ExternalNATSbroker → feat(app): mq.backend selects embedded or external NATS #639 D4mq.backend: natswiring → fix(mq): drain partitions a lower N leaves; quiet close #644 close-log and partition-shrink fixes → feat(mq): coord leases on a NATS KV bucket #646 B2 NATS KV leases (carries test(mq,app): fit the unit budget; fail fast on an uncreatable store #647's commits until test(mq,app): fit the unit budget; fail fast on an uncreatable store #647 lands)internal/app/internal/mqunit tests → test(mq,app): fit the unit budget; fail fast on an uncreatable store #647 speed-up (on main, ~5 s per package) · fix(api): timeouts and memory limits on uncapped query paths are retried #620 · bug(config): an explicit false/0 in config.yaml is replaced by the field's env-default #631 YAML zero replaced by default → fix(config): keep an explicit false/0/"" from config.yaml #632 fix, on mainmake cipasses there. It is not meant to merge directly; its body lists which integration fix goes back to which PR.Related: #583, #612, #246, #247, #264, #384, #390, #393, #403, #271, #220, #222.
Status (2026-09-30)
Done: every workstream is on
main; the last, D with B2, landed in #624. The embedded backends remain the default. Before production traffic relies on external NATS, the 3-node checks in #694 should run.Follow-ups, each tracked on its own: