Skip to content

fix(core): bound SlotBackoff retry sleeps and fix slot overflow - #7883

Merged
wjones127 merged 5 commits into
lance-format:mainfrom
LuciferYang:fix/bound-slot-backoff-commit-sleep
Jul 27, 2026
Merged

fix(core): bound SlotBackoff retry sleeps and fix slot overflow#7883
wjones127 merged 5 commits into
lance-format:mainfrom
LuciferYang:fix/bound-slot-backoff-commit-sleep

Conversation

@LuciferYang

Copy link
Copy Markdown
Contributor

Summary

SlotBackoff::next_backoff doubled the slot count on every attempt with no ceiling, and computed the sleep as (slot_i * self.unit) as u64 — a u32 * u32 multiply that widens to u64 only after the product is formed. Two problems followed:

  1. At a high attempt count the slot_i * unit product overflows u32: a debug-build panic, or in release a wrapped, wrong sleep duration.
  2. The slot count, and therefore the backoff, grew without bound as attempts climbed.

This is reachable on the default commit-retry path (20 retries). unit is set to the first attempt's latency plus 10% (commit.rs), so once the first commit takes a few seconds, a deep retry drives slot_i into the range where the product overflows.

Change

Cap the slot count at MAX_SLOTS (128) and widen the operands to u64 before multiplying. 128 slots already exceeds any realistic number of concurrent committers, so further doubling only lengthens the wait without reducing collisions. Every backoff is now bounded by (MAX_SLOTS - 1) * unit.

Capping the slot count rather than the resulting duration is deliberate: SlotBackoff spreads contending writers across random slots so they don't collide, and a duration clamp would map every high-attempt writer onto the same instant, recreating the thundering herd the type exists to prevent. A fixed 128-slot grid keeps writers uniformly distributed. Attempts 0-4 are unchanged (their slot counts are already below 128); the cap only affects attempt 5 and beyond, where the extra slots bought no throughput anyway (commit throughput is one per unit regardless of slot count).

Follow-up (out of scope)

The cap is proportional to unit, not absolute, so a slow first attempt can still produce a multi-minute single sleep. The commit-path sleep has no timeout wrapper, unlike the write-retry path which bounds its sleep against a deadline. Bounding wall-clock regardless of unit is tracked separately in #7882.

Test plan

  • test_slot_backoff (existing) — low-attempt slot distributions, unchanged.
  • test_slot_backoff_high_attempt_is_bounded — every backoff stays within (MAX_SLOTS - 1) * unit across many high attempts; fails if the cap is removed.
  • test_slot_backoff_large_unit_does_not_overflow — with unit = u32::MAX, asserts a lower bound that a reverted u32 multiply would violate in release (wraps to a smaller value) as well as debug (panics).
  • cargo fmt --all and cargo clippy -p lance-core --tests -- -D warnings are clean.

SlotBackoff doubled its slot count every attempt with no ceiling, so the
`slot_i * unit` product (a u32 multiply widened to u64 only afterward)
could overflow at high attempt counts, and the sleep grew without limit.
This is reachable on the default commit-retry path (20 retries): once the
first attempt takes a few seconds, `unit` is large enough that a deep
retry overflows — a debug panic, or in release a wrapped, wrong sleep.

Cap the slot count at MAX_SLOTS (128), which already exceeds any realistic
number of concurrent committers, and widen the multiply before it runs.
Capping the slot count rather than the resulting duration preserves the
uniform-slot spreading that keeps contending writers from colliding; a
duration clamp would pile every high-attempt writer onto the same instant.
Every backoff is now bounded by `(MAX_SLOTS - 1) * unit`.

The cap is proportional to `unit`, so a slow first attempt can still
produce a multi-minute single sleep. Bounding wall-clock regardless of
`unit` needs a timeout around the commit-path sleep (as the write-retry
path already has); that is a separate change.
@github-actions github-actions Bot added the bug Something isn't working label Jul 21, 2026
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

SlotBackoff caps retry slots at MAX_SLOTS, prevents overflow during duration calculation, saturates attempt progression, and adds tests for these behaviors.

Changes

Slot Backoff

Layer / File(s) Summary
Cap slots and validate duration calculations
rust/lance-core/src/utils/backoff.rs
Adds MAX_SLOTS, clamps slot selection, widens multiplication operands, saturates attempt increments, and tests bounded and overflow-safe backoff behavior.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested labels: bug

Suggested reviewers: xuanwo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: bounding SlotBackoff retries and fixing slot overflow.
Description check ✅ Passed The description is directly related to the code changes and accurately explains the backoff and overflow fixes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (1)
rust/lance-core/src/utils/backoff.rs-156-156 (1)

156-156: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Prevent attempt from wrapping.

After u32::MAX calls, this panics in debug builds and wraps to zero in release builds, restarting the low-slot distribution. Use a non-wrapping increment that preserves the capped regime, or make overflow explicit through a fallible API.

Proposed compatible fix
-        self.attempt += 1;
+        self.attempt = self.attempt.saturating_add(1);

As per coding guidelines, “Use checked_add and checked_mul instead of wrapping_add and wrapping_mul for counters and IDs, and return an error on overflow.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-core/src/utils/backoff.rs` at line 156, Update the attempt
increment in the backoff implementation to prevent u32 overflow after reaching
the maximum value. Use a non-wrapping or checked increment that preserves the
capped backoff behavior, and propagate an explicit error if the API is made
fallible; keep the existing distribution logic unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Other comments:
In `@rust/lance-core/src/utils/backoff.rs`:
- Line 156: Update the attempt increment in the backoff implementation to
prevent u32 overflow after reaching the maximum value. Use a non-wrapping or
checked increment that preserves the capped backoff behavior, and propagate an
explicit error if the API is made fallible; keep the existing distribution logic
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 5d0d2e76-080f-4c21-9a23-dbaa8c9d3342

📥 Commits

Reviewing files that changed from the base of the PR and between aea6ded and ccda739.

📒 Files selected for processing (1)
  • rust/lance-core/src/utils/backoff.rs

The `attempt` counter used a plain `+= 1`, which would panic in debug and
wrap to zero in release after u32::MAX calls, restarting the low-slot
distribution. Saturate it, matching the `saturating_add` already used for
the exponent a few lines above. Not reachable in practice, but keeps the
counter monotonic and consistent with the rest of the function.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
rust/lance-core/src/utils/backoff.rs (2)

151-154: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the new slot ceiling in the public API docs.

The implementation now caps the slot count at MAX_SLOTS, but the SlotBackoff documentation should explicitly describe this limit so its exponential-growth description remains accurate.

As per coding guidelines: **/*.{rs,md,rst} requires public APIs to document semantics with synchronized examples and links.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-core/src/utils/backoff.rs` around lines 151 - 154, Update the
public documentation for SlotBackoff to state that exponential slot growth is
capped at MAX_SLOTS, and synchronize any associated example or link required by
the API documentation guidelines. Keep the implementation unchanged.

Source: Coding guidelines


261-284: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make this overflow regression test deterministic. The current loop depends on SmallRng::from_os_rng() eventually picking a non-zero slot within 200 draws, so it can fail independently of the overflow fix. Seed the RNG or test the slot-multiplication path directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-core/src/utils/backoff.rs` around lines 261 - 284, Make
test_slot_backoff_large_unit_does_not_overflow deterministic by removing its
dependence on SlotBackoff’s OS-seeded random draws. Seed the RNG used by
SlotBackoff or directly exercise the slot-multiplication path with a known
non-zero slot, while preserving assertions that the widened product does not
wrap below unit.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/lance-core/src/utils/backoff.rs`:
- Line 156: Add a focused unit test for the backoff type that initializes
attempt to u32::MAX, calls next_backoff(), and asserts attempt remains u32::MAX,
directly covering saturating counter behavior.

---

Outside diff comments:
In `@rust/lance-core/src/utils/backoff.rs`:
- Around line 151-154: Update the public documentation for SlotBackoff to state
that exponential slot growth is capped at MAX_SLOTS, and synchronize any
associated example or link required by the API documentation guidelines. Keep
the implementation unchanged.
- Around line 261-284: Make test_slot_backoff_large_unit_does_not_overflow
deterministic by removing its dependence on SlotBackoff’s OS-seeded random
draws. Seed the RNG used by SlotBackoff or directly exercise the
slot-multiplication path with a known non-zero slot, while preserving assertions
that the widened product does not wrap below unit.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 1b2d6390-971b-41cf-ac40-ca699bed334a

📥 Commits

Reviewing files that changed from the base of the PR and between ccda739 and 4a9b7c0.

📒 Files selected for processing (1)
  • rust/lance-core/src/utils/backoff.rs

Comment thread rust/lance-core/src/utils/backoff.rs
Directly exercise the saturating counter: set attempt to u32::MAX, call
next_backoff, and assert it stays put. Fails if the increment reverts to
`+= 1` (debug panic / release wrap).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rust/lance-core/src/utils/backoff.rs (1)

260-285: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Make the overflow regression test deterministic.

This test only detects a reverted u32 multiplication when a slot greater than 1 is drawn; slot 1 still produces u32::MAX. Seed the RNG or otherwise force a known slot so the test cannot pass or fail based on random draws.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-core/src/utils/backoff.rs` around lines 260 - 285, Make
test_slot_backoff_large_unit_does_not_overflow deterministic by controlling
SlotBackoff’s RNG or slot selection so a slot greater than 1 is guaranteed
during the test. Preserve the overflow assertion while removing reliance on
random draws and eliminate the saw_nonzero loop/state if the forced slot makes
it unnecessary.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@rust/lance-core/src/utils/backoff.rs`:
- Around line 260-285: Make test_slot_backoff_large_unit_does_not_overflow
deterministic by controlling SlotBackoff’s RNG or slot selection so a slot
greater than 1 is guaranteed during the test. Preserve the overflow assertion
while removing reliance on random draws and eliminate the saw_nonzero loop/state
if the forced slot makes it unnecessary.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: de4a3112-92fd-469e-a392-6cc604d094ce

📥 Commits

Reviewing files that changed from the base of the PR and between 4a9b7c0 and 56fdcd8.

📒 Files selected for processing (1)
  • rust/lance-core/src/utils/backoff.rs

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Seed the RNG so the drawn slots are reproducible, and assert the backoff
is an exact multiple of unit (a wrapped u32 product is not) instead of a
lower bound that a slot of 1 would satisfy. Removes the reliance on random
draws flagged in review.
@LuciferYang
LuciferYang marked this pull request as draft July 21, 2026 13:59
`cargo clippy --all-targets` flags `field_reassign_with_default` when a
field is assigned after `Default::default()`. Build the SlotBackoff with a
struct literal (`..Default::default()`) instead.
@LuciferYang
LuciferYang marked this pull request as ready for review July 22, 2026 05:57

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
rust/lance-core/src/utils/backoff.rs (1)

261-277: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test the cap boundary explicitly.

This verifies the upper bound and overflow handling, but not the promised transition where attempts 0–4 retain their original ranges and attempt 5 is the first capped range. Add deterministic assertions around attempts 4 and 5 to catch an off-by-one in the cap or exponent calculation.

As per coding guidelines, every bugfix and feature must have corresponding tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-core/src/utils/backoff.rs` around lines 261 - 277, Extend the
backoff tests around SlotBackoff to deterministically assert the uncapped range
for attempt 4 and the first capped range for attempt 5. Use a controlled RNG or
fixed inputs so the assertions verify the exact transition and detect off-by-one
errors in the cap or exponent calculation, while retaining the existing overflow
and upper-bound checks.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@rust/lance-core/src/utils/backoff.rs`:
- Around line 261-277: Extend the backoff tests around SlotBackoff to
deterministically assert the uncapped range for attempt 4 and the first capped
range for attempt 5. Use a controlled RNG or fixed inputs so the assertions
verify the exact transition and detect off-by-one errors in the cap or exponent
calculation, while retaining the existing overflow and upper-bound checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 8deb8285-91b6-4bc8-90dc-fa4704167d91

📥 Commits

Reviewing files that changed from the base of the PR and between 56fdcd8 and 6c05003.

📒 Files selected for processing (1)
  • rust/lance-core/src/utils/backoff.rs

@wjones127

Copy link
Copy Markdown
Contributor

Thanks!

@wjones127
wjones127 merged commit b29e602 into lance-format:main Jul 27, 2026
34 checks passed
@LuciferYang

Copy link
Copy Markdown
Contributor Author

Thank you @wjones127

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants