Skip to content

fix: demand pipeline at production scale - #364

Merged
beinan merged 1 commit into
lance-format:mainfrom
beinan:fix/demand-at-scale
Oct 9, 2026
Merged

beinan merged 1 commit into
lance-format:mainfrom
beinan:fix/demand-at-scale

Conversation

@beinan

@beinan beinan commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

Part of #333. Fixes all six findings of review round 5 (credit: production review), which brought real numbers: 13,405 tables, 354 with pending, 318 generic.

# Finding Fix
1 [P1] Full prefix read of ~27k records exceeds the 4 MiB etcd client cap Every planner read paged (512 keys) at one revision. Reconcile detached from the leader loop → keepalive cannot starve; one pass in flight.
2 [P1] Scan read only sealed; a lost completion event left a merged shard pending forever; old table's first contact → gen 1001 = 1001 pending all_shard_watermarks reads the base MemWAL index's merged watermark per shard; scan events carry it.
3 generic:generic:<name> entry.name already has the prefix.
4 1-byte reservations when pending_bytes == 0 (all real publishers) Unknown reserves the full per-pass cost (2 × ROLLOUT_APPEND_MAX_BYTES); counted.
5 "disagreement ≈ 0" is not a takeover gate planner_shadow_comparison{outcome ∈ discovery_miss, placement_gap, phantom_task, agree}, each a precise question; documented as observations. Old counter kept as the sum.
6 128 pending, merge 64 → age unknown / wrong after rebuild; slow reconcile starves keepalive Times map bounded by cap only (lowest 63 + newest), filtered by merged on read; answer carries Exact/Upper; only Exact promotes. Executor events no longer seed flush times. Keepalive: see #1.

Validation

  • Production-shape test: 13,405 × 2 shards seeded; asserts the unpaged read fails (so the test cannot pass vacuously), paged read returns 26,810 at one revision, fold counts them, leader completes a reconcile and holds its lease 12 s past it, cleanup verified. Gated on PLANNER_SCALE_TEST=1 (run alone; it starves a shared single-node etcd).
  • Demand: 128/64 case → Upper bound, same after rebuild and in reverse; randomized rebuild == fold still holds; scan-with-merged folds.
  • Scoring: a bound never promotes or boosts.
  • 16 demand, 5 scoring, 17 etcd (planner/executor/scan/resident), 9 sweeper green on clean etcd; clippy -D warnings, fmt, typos clean. Spec §1.1/§1.2a/§1.3/§2.3 updated.

🤖 Generated with Claude Code

Review round 5 brought production numbers: 13,405 tables, 354 with
pending WAL, 318 generic. Six findings, all confirmed and fixed here.

1. Full prefix reads fail past the etcd client's 4 MiB cap (~27k shard
   records). Every planner read (demand-events, demand, executors,
   assignments, queue, running) is now paged at 512 keys at one revision.
   Reconcile runs detached from the leader loop so a slow pass cannot
   starve the lease keepalive; one pass in flight, skipped ticks counted.
2. The scan read only sealed, so a lost merge-completion event left a
   fully merged shard pending forever and an old table's first contact
   read generation 1001 as 1001 pending. all_shard_watermarks now also
   reads the base table's merged watermark per shard and the scan event
   carries it.
3. Scan events for generic stores were published under
   generic:generic:<name>; entry.name already carries the prefix.
4. Shadow placement reserved 1 byte when pending_bytes was unknown
   (every real publisher reports 0). An unknown now reserves the full
   per-pass cost (2 x ROLLOUT_APPEND_MAX_BYTES) and is counted.
5. One "disagreement" counter mixed discovery, capacity and ownership
   effects. Replaced by planner_shadow_comparison{outcome} with
   discovery_miss, placement_gap, phantom_task and agree, each a precise
   question, documented as observations and not a takeover gate; the old
   counter remains as their sum.
6. Age after merging past the kept entries: 128 pending, merge 64 gave
   "unknown" in memory and generation 128's time after rebuild. The
   times map is now bounded by cap only (pruning by merged made the kept
   set order dependent), keeps the lowest 63 plus the newest, and is
   filtered by merged on read. The answer carries a bound - Exact when
   the oldest pending generation's time is kept, Upper otherwise - and
   only an Exact age drives scoring and promotion. Executor events no
   longer seed flush times (their timestamp is observation time).

Tests: a production-shape test seeds 13,405 tables x 2 shards, asserts
the unpaged read fails, the paged read returns all 26,810 records at one
revision, the fold counts them, a leader completes a reconcile while its
lease survives twelve seconds past the pass, and cleans up after itself
(gated on PLANNER_SCALE_TEST=1; run alone). Demand tests cover the
128/64 case as a flagged Upper bound identical after rebuild and in
reverse order, scan-with-merged folding, and the randomized
rebuild-equals-fold property still holds with the new bookkeeping.
Scoring asserts a bound never promotes or boosts. 16 demand, 5 scoring,
17 planner/executor/scan/resident etcd tests and 9 sweeper tests pass
on a clean etcd. Clippy -D warnings, fmt, typos clean. Spec updated.

Refs lance-format#333. Review round 5.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@beinan
beinan merged commit baf2230 into lance-format:main Oct 9, 2026
11 checks passed
beinan added a commit that referenced this pull request Oct 9, 2026
Part of #333. Follow-up to #364.

Review round 5 measured production: **13,405 tables, ~354 with pending,
318 generic, 6 masters**. The design assumed "tens → low hundreds
active" — wrong by two orders of magnitude — and that assumption
directly produced the unpaged etcd reads and 1-byte reservations fixed
in #364.

Corrects §2 (workload table, with the error acknowledged), §4.5
(per-cycle cost now stated with paging and measured at production
shape), §6 (when the single leader would need watch-driven folding or
sharding), and marks backlog open question 1 answered.

Docs only; typos clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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