Skip to content

fix(cascade): record which beat lost the optimize commit race - #390

Merged
cyfyifanchen merged 2 commits into
mainfrom
fix/prune-conflict-log-beat-flag
Aug 5, 2026
Merged

fix(cascade): record which beat lost the optimize commit race#390
cyfyifanchen merged 2 commits into
mainfrom
fix/prune-conflict-log-beat-flag

Conversation

@gloryfromca

Copy link
Copy Markdown
Collaborator

What

Adds one field — pruned — to the benign commit-conflict log:

[debug] cascade_lancedb_optimize_conflict  kind=episode  pruned=True  error='...'

No behaviour change. Log level stays debug, the failure streak is still not
incremented, and no fallback rebuild is triggered.

Why

Lance labels both maintenance beats' commit identically:

This Rewrite transaction was preempted by concurrent transaction Delete at version 3622

Light beat (optimize()) and heavy beat (optimize(cleanup_older_than=…)) are
both Rewrite transactions, so the message cannot tell them apart. The costs do
not match:

lost beat cost
light free — compaction retries ~10s later
heavy that table skipped a whole prune cadence; superseded files stay on disk

So a run of heavy-beat conflicts is exactly what precedes an index dir growing,
and today it is indistinguishable in the logs from harmless light-beat noise.

This is not hypothetical. During the storage soak one table's cleanup went
quiet for 13 minutes with zero failures logged (optimize_failed,
write_lock_deadline_exceeded, maintenance_task_timeout all 0) while
prune_stale_seconds climbed linearly. Establishing that those were lost heavy
beats meant back-inferring beat types from the 300s cadence, then two 35-minute
debug-level reruns (~870MB of logs) to confirm. With pruned it is a grep.

The sibling failure log already carries pruned=should_prune; the conflict
branch in the same except simply missed it, and the variable is already in
scope.

Test

test_conflict_log_names_the_lost_beat drives both beats in one run (frozen
clock, so beat selection does not depend on runner uptime) and asserts the
heavy beat's conflict logs pruned=True and the light beat's pruned=False.

Mutation-verified: removing the field fails the test with KeyError: 'pruned'.

tests/unit/test_memory/test_cascade/ — 202 passed, 0 skipped. make lint clean.

🤖 Generated with Claude Code

The benign-conflict log said a commit race was lost but not by which
maintenance beat, and lance labels both beats' commit identically
("This Rewrite transaction was preempted by concurrent transaction ..."),
so the message alone cannot separate them. The costs differ sharply:

- a lost LIGHT beat is free — compaction retries ~10s later;
- a lost HEAVY beat means that table skipped a whole prune cadence, so
  its superseded files stay on disk until the next one lands.

Reading an index-dir growth incident off the logs therefore meant
back-inferring which beats were heavy from the 300s cadence. That was
done once during the storage soak to explain a 13-minute window where
one table's cleanup stalled with no failure logged, and it cost two
35-minute debug-level reruns to confirm — the field makes it a grep.

Adds `pruned` to the conflict log, mirroring the sibling failure log.
Log level (debug) and the benign-conflict semantics are unchanged: the
streak is still not incremented and no fallback rebuild is triggered.

Test asserts both beats in one run: the heavy beat's conflict logs
pruned=True and the light beat's logs pruned=False. Mutation-verified —
dropping the field fails the test with KeyError.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cyfyifanchen
cyfyifanchen merged commit 72c1fac into main Aug 5, 2026
9 checks passed
@cyfyifanchen
cyfyifanchen deleted the fix/prune-conflict-log-beat-flag branch August 5, 2026 05:48
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.

2 participants