Skip to content

fix(master): read merge progress at one etcd revision with a typed response - #341

Merged
beinan merged 1 commit into
lance-format:mainfrom
beinan:fix/merge-progress-single-revision
Oct 8, 2026
Merged

beinan merged 1 commit into
lance-format:mainfrom
beinan:fix/merge-progress-single-revision

Conversation

@beinan

@beinan beinan commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator

Part of #333 (P0.5, backlog C5).

Problem

GET /api/v1/merge-progress (#330) did four independent etcd reads — execution, progress, work report, execution again — and returned progress_changed_retry / execution_changed_retry when they disagreed, leaving the caller to retry. The body was serde_json::json! with no schema.

Approach

  • Coordinator::progress_snapshot(target): read the execution id, then one txn guarded on the execution bytes that fetches progress and work report together. All three fields come from a single revision, exposed as revision. Bounded to three attempts. A work report whose sequence trails the current progress sequence is dropped instead of being shown next to a newer heartbeat.
  • Handler returns MergeProgressReport (serde struct) with status ∈ {reported, progress_details_unavailable, progress_unknown, no_active_record}. The two *_retry statuses are removed. Absent fields are omitted, so consumers reading only status/task_id see no change.
  • docs/merge-work-progress.md updated with the schema and single-revision guarantee.

Alternatives

Reading all keys by target in one range: progress and work keys are per execution id, not per target, so a first read is unavoidable. Using WithRevision on separate gets: works, but the guarded txn also detects an execution swap for free.

Validation

  • lance-context-merge: detailed_work_keeps_legacy_progress_cas_… extended — snapshot agrees with individual reads, stale report dropped after a heartbeat, absent target → None.
  • lance-context-master: merge_progress_never_calls_a_live_legacy_task_healthy asserts on the typed struct and its serialised form.
  • Both pass locally against real etcd; clippy -D warnings, fmt, typos clean.

🤖 Generated with Claude Code

…sponse

`/merge-progress` did four independent etcd reads (execution, progress,
work report, execution again) and, when they disagreed, returned
`progress_changed_retry` or `execution_changed_retry`, pushing read
consistency onto the caller. The response was hand-assembled JSON with
no schema.

Add `Coordinator::progress_snapshot`: one read for the execution id,
then one transaction guarded on the execution bytes that fetches
progress and work report together, so all three come from a single
revision (bounded to three attempts; a target flipping executions
faster than two etcd reads is not a status-endpoint concern). A work
report whose sequence trails the current progress sequence is dropped
rather than rendered beside a newer heartbeat.

The handler returns `MergeProgressReport`, a serde struct with a fixed
field set and a `status` enum of four values; the two retry statuses
are gone. Absent fields are omitted so existing consumers that only
read `status`/`task_id` are unaffected. `revision` is new.

Validation: merge-crate test extended for the snapshot, stale-report
dropping and the absent-target case; routes test asserts on the typed
struct and its wire form. Both pass against real etcd. Clippy -D
warnings, fmt and typos clean.

Refs lance-format#333.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
beinan added a commit that referenced this pull request Oct 8, 2026
Part of #333 (P0.6, backlog O2/O3).

## What

`docs/operations/maintenance-runbook.md`: symptom-driven guide for WAL
merge / compaction / index / repair scheduling. Sections: where to look
(table of question → endpoint/metric/key), backlog growing, deciding
whether a running execution is stuck, demand but nothing scheduled
(seven admission reasons, cheapest first), compaction never commits on a
hot table, executor died holding ownership, draining, known gaps.

Leading rule: **progress decides whether something is stuck; leases,
heartbeats, Running state, retry countdowns, Ready pods and elapsed time
do not.** Recovery is fenced and automatic; never delete ownership keys
by hand.

Uses the metrics from #339 and the typed `/merge-progress` from #341.
All numeric claims (15 s retry loop, 15-failure attention, 120 s fence,
30 s lease TTL, 3-failure cooldown, 256/4096 thresholds) verified
against source.

Docs only; `typos` clean.

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@beinan
beinan merged commit 850d8ba into lance-format:main Oct 8, 2026
11 checks passed
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