tidaldb/k8s/cluster
jordan 897c6086f5 fix(cluster): size the read fan-out budget for the transport it crosses
Restoring the three-node cluster for a first production consumer surfaced this
immediately: EVERY cross-shard read came back

  {"items":[...],"scatter_gather":{"degraded":true,
    "unavailable_shards":["tidaldb-0","tidaldb-2"],"shards_queried":1,
    "elapsed_ms":50,"shard_deadline_ms":45}}

HTTP 200, one shard of three, partial results. Replication itself was healthy -
/cluster/status showed all three regions reachable, lag_events 0, 13.3M events
applied each - so nothing in the quorum, election, or ship metrics moved.

Measured on the live cluster: a COLD peer fetch (TCP + TLS handshake + remote
1536-D search) takes ~50ms; a warm one takes ~1ms. DEFAULT_DEADLINE_MS is 50
(spec §7.4) and NETWORK_OVERHEAD_MS is 5, leaving a 45ms per-shard budget -
just under the cold cost. Proven by parameter sweep against one pod:

  deadline_ms=50   -> degraded, 1/3 shards, 0 items
  deadline_ms=250  -> healthy,  3/3 shards, elapsed 51ms
  deadline_ms=1000 -> healthy,  3/3 shards, elapsed 1ms (warm)

The 50ms spec figure budgets a shard READ, not establishing a connection to
another pod. m11p7 put TLS on that hop and the default never followed, so the
first query after any rollout, idle period, or pod restart answered from a third
of the corpus. Fixed with a transport-aware default: 50ms in-process,
TLS_DEFAULT_DEADLINE_MS (250ms) once inter-node TLS is configured. An explicit
`?deadline_ms=` still wins in both directions, and MAX_DEADLINE_MS is unchanged.

The worse half was silence. A degraded fan-out is the one cluster failure that
answers 200 OK: the caller gets a ranked list assembled from a subset of the
corpus with `degraded: true` buried in response metadata. Nothing incremented,
so no alert could exist - a feed quietly ranking over one third of its
candidates looked identical to a healthy one. Added
tidaldb_cluster_scatter_degraded_total and
tidaldb_cluster_scatter_shard_unavailable_total, emitted from both HTTP fan-out
paths, so partial answers are now a countable correctness signal.

Also sizes the cluster StatefulSet for a consumer instead of the endurance gate:
requests 2 cores -> 300m per voter (limit 2 cores). The 2-core reservation was
the 200 rps soak envelope and needed 6,000m plus 2,000m free on each of three
PV-pinned nodes; the fleet is 82-91% committed, so that contract could not be
placed and the cluster stayed parked for a gate nobody is waiting on. 300m is
what the tightest pinned node can reserve, with the quorum/write-pool alerts as
the detector if real load outgrows it.

Tests: default_read_budget_covers_a_cold_inter_node_tls_hop pins the budget
against the measured cold hop and the explicit-override path; the cluster-metrics
render test covers both new counters.
2026-08-17 20:28:21 -06:00
..
certs.yaml fix(m12p6): complete T4 TLS scale-up — two-tier PKI + join_boot grpc_tls fallback 2026-06-14 22:41:59 -06:00
kustomization.yaml feat(m11): cluster security (m11p7) + perf instrumentation floor 2026-06-13 01:25:35 -06:00
namespace.yaml feat(m11): membership, snapshot install, and reseed (m11p5) 2026-06-12 19:55:54 -06:00
poddisruptionbudget.yaml feat(m11): membership, snapshot install, and reseed (m11p5) 2026-06-12 19:55:54 -06:00
schema-configmap.yaml bench(1536): production-shape capacity — read path is cheap, quorum write is the ceiling 2026-06-13 21:40:58 -06:00
secret.example.yaml feat(m11): cluster security (m11p7) + perf instrumentation floor 2026-06-13 01:25:35 -06:00
service-client.yaml feat(m11): membership, snapshot install, and reseed (m11p5) 2026-06-12 19:55:54 -06:00
service-peers.yaml feat(m12p4): sharded ingestion — scatter-gather pool + cross-shard unified reads (L4) 2026-06-14 15:17:35 -06:00
statefulset.yaml fix(cluster): size the read fan-out budget for the transport it crosses 2026-08-17 20:28:21 -06:00
topology-configmap.yaml feat(m12p4): sharded ingestion — scatter-gather pool + cross-shard unified reads (L4) 2026-06-14 15:17:35 -06:00