Skip to content

fix(mem-wal): reject mismatched shard specs before epoch claim - #7949

Open
u70b3 wants to merge 1 commit into
lance-format:mainfrom
u70b3:fix/memwal-shard-spec-conflict
Open

u70b3 wants to merge 1 commit into
lance-format:mainfrom
u70b3:fix/memwal-shard-spec-conflict

Conversation

@u70b3

@u70b3 u70b3 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • reject an incoming shard_spec_id that differs from the immutable identity already stored in a shard manifest, before advancing manifest version or writer epoch
  • revalidate sealed status and shard identity after write conflicts, preserving the distinguishable sealed-shard error
  • keep rejection side-effect free and include the shard id plus incoming and stored spec ids in the error
  • cover matching automatic identity, both current 0/1 mismatch directions, sealed manifests, and concurrent first claims

Scope

This is an independent defensive invariant over the identities Lance currently emits: 0 identifies manually managed shards and 1 is the sole automatic sharding spec. The check does not require monotonic spec allocation and does not define revisions or activation.

#8112 (merged) is the complementary creation-side half. It resolves a dataset writer's default 0 to the current automatic spec at the Dataset::mem_wal_writer boundary, before a fresh manifest is created; this PR prevents any caller from changing an identity once the manifest exists. The layers compose: #8112 guards the API entry point, while this PR guards the stored manifest at claim_epoch — catching direct ShardWriter::open callers that bypass the API boundary.

shard_spec_id=0 is an identity, not a wildcard.

Fixes #7945.

Testing

Behavior change / rollout

Previously, a writer opening an existing shard with a mismatched shard_spec_id (e.g. the default 0 opening an auto-sharded shard stored as 1) was silently tolerated past claim_epoch. It now fails fast with InvalidInput naming the shard id and both spec ids — this is the fix intent of #7945. With #8112 merged, writers going through Dataset::mem_wal_writer have their default resolved before claiming, so the hard error only affects callers that bypass the API boundary with an explicitly mismatched id.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

claim_epoch now centralizes sealed-manifest and shard-spec mismatch validation, applies it before and after write conflicts, and adds single-threaded and concurrent tests for matching and mismatched shard specifications.

Changes

Shard claim validation

Layer / File(s) Summary
Centralized claim validation and conflict handling
rust/lance/src/dataset/mem_wal/manifest.rs
validate_claimable rejects sealed manifests and shard-spec mismatches. claim_epoch applies it before epoch calculation and after failed writes.
Claim behavior tests
rust/lance/src/dataset/mem_wal/manifest.rs
Tests verify matching claims, mismatched specifications, concurrent claim consistency, and sealed-manifest refusal.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: hamersaw

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: rejecting mismatched shard specs before epoch claim.
Description check ✅ Passed The description matches the code changes and issue scope.
Linked Issues check ✅ Passed The changes satisfy #7945 by rejecting spec mismatches, revalidating after conflicts, and preserving successful claims.
Out of Scope Changes check ✅ Passed No clear out-of-scope changes are indicated beyond the manifest validation fix and related tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot added the bug Something isn't working label Jul 23, 2026

@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/src/dataset/mem_wal/manifest.rs-445-447 (1)

445-447: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document sealed-manifest refusal in # Errors.

claim_epoch now returns an error for sealed manifests, but this public API’s error contract only documents epoch conflicts, shard-spec conflicts, and contention. Document the sealed refusal and its no-claim effect. As per coding guidelines, “Ensure doc comments match actual semantics; distinguish mutates-in-place (&mut self) from returns-new-value behavior.”

🤖 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/src/dataset/mem_wal/manifest.rs` around lines 445 - 447, Update
the public `claim_epoch` documentation comment’s `# Errors` section to include
refusal for sealed manifests and state that this path makes no claim or
mutation. Keep the existing documentation for epoch conflicts, shard-spec
conflicts, and contention, and accurately describe the method’s in-place `&mut
self` behavior.

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/src/dataset/mem_wal/manifest.rs`:
- Around line 445-447: Update the public `claim_epoch` documentation comment’s
`# Errors` section to include refusal for sealed manifests and state that this
path makes no claim or mutation. Keep the existing documentation for epoch
conflicts, shard-spec conflicts, and contention, and accurately describe the
method’s in-place `&mut self` behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: eddae87c-eea7-4d35-949b-1203ad196d84

📥 Commits

Reviewing files that changed from the base of the PR and between 3a72f8a and 5cd2b6a.

📒 Files selected for processing (1)
  • rust/lance/src/dataset/mem_wal/manifest.rs

@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.54545% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
rust/lance/src/dataset/mem_wal/manifest.rs 94.54% 1 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@hamersaw hamersaw 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.

I think this change has the right intent, but will not fix underlying issues correctly. It's meant to disallow claiming a table with a different shard_spec_id than the base table. However, Lance currently only uses 0 and 1 as valid values to not manual and automatic sharding configuration. IMO this is part of a larger discussion, we should update the shard spec configuration so that IDs are monotonically increasing. That would make this change valid.

@u70b3

u70b3 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, I think I understand the concern. The manifest-side equality check is only meaningful if every distinct sharding-spec revision has a unique ID. Today the public initialization path assigns 1 to every automatic sharding configuration, so two different automatic specs remain indistinguishable (0 remains the manual-shard sentinel).

It sounds like the prerequisites are:

  1. Allocate sharding spec IDs monotonically and never reuse them.
  2. Ensure a writer claims with the active spec ID from the base-table metadata.
  3. Keep the claim-time validation so a stale or mismatched spec cannot advance the epoch and fence the incumbent writer.

Would you prefer that I expand this PR to implement the spec-ID lifecycle, or split the monotonic allocation/base-table validation into a prerequisite PR and rebase this change on it?

@u70b3
u70b3 force-pushed the fix/memwal-shard-spec-conflict branch 3 times, most recently from 6151ec2 to 8a49ce1 Compare July 30, 2026 14:19
@u70b3
u70b3 requested a review from hamersaw July 31, 2026 01:19
@u70b3

u70b3 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Following up on my previous comment: I split the prerequisite work into #8112.

#8112 introduces monotonic spec-ID allocation (max(existing) + 1), reserves spec ID 0 for manual sharding, validates reserved and duplicate IDs at MemWAL metadata boundaries, and resolves/validates the writer's spec against the active table spec before any shard claim. Manual sharding is restricted to spec ID 0.

The current initializer only creates the first spec, so an empty history produces spec ID 1. A future spec-revision API will provide the complete append-only history.

Once #8112 lands, I'll rebase #7949 on top of it. #7949 will remain focused on claim-time manifest validation.

@u70b3
u70b3 force-pushed the fix/memwal-shard-spec-conflict branch from 8a49ce1 to 812fe55 Compare July 31, 2026 08:06
u70b3 added a commit to u70b3/lance that referenced this pull request Jul 31, 2026
The proto contract promises sharding spec ids are never reused, but the
public init path hardcoded id 1 for every automatic sharding
configuration, leaving distinct specs indistinguishable at shard-claim
time.

- add next_spec_id(): greatest id + 1, with 0 reserved as the
  manual-shard sentinel (MANUAL_SHARD_SPEC_ID)
- reject reserved/duplicate spec ids when decoding MemWalIndexDetails
- add MemWalIndexDetails::active_sharding_spec() (greatest id)
- mem_wal_writer resolves an unset (0) shard_spec_id to the active spec
  and rejects an explicit mismatch before any shard claim

Prerequisite for lance-format#7949.
@u70b3
u70b3 force-pushed the fix/memwal-shard-spec-conflict branch from 812fe55 to 0f7bd7f Compare July 31, 2026 09:34
u70b3 added a commit to u70b3/lance that referenced this pull request Jul 31, 2026
The proto contract promises sharding spec ids are never reused, but the
public init path hardcoded id 1 for every automatic sharding
configuration, leaving distinct specs indistinguishable at shard-claim
time.

- add next_spec_id(): greatest id + 1, with 0 reserved as the
  manual-shard sentinel (MANUAL_SHARD_SPEC_ID)
- reject reserved/duplicate spec ids when decoding MemWalIndexDetails
- add MemWalIndexDetails::active_sharding_spec() (greatest id)
- mem_wal_writer resolves an unset (0) shard_spec_id to the active spec
  and rejects an explicit mismatch before any shard claim

Prerequisite for lance-format#7949.
u70b3 added a commit to u70b3/lance that referenced this pull request Jul 31, 2026
The proto contract promises sharding spec ids are never reused, but the
public init path hardcoded id 1 for every automatic sharding
configuration, leaving distinct specs indistinguishable at shard-claim
time.

- add next_spec_id(): greatest id + 1, with 0 reserved as the
  manual-shard sentinel (MANUAL_SHARD_SPEC_ID)
- reject reserved/duplicate spec ids when decoding MemWalIndexDetails
- add MemWalIndexDetails::active_sharding_spec() (greatest id)
- mem_wal_writer resolves an unset (0) shard_spec_id to the active spec
  and rejects an explicit mismatch before any shard claim

Prerequisite for lance-format#7949.
@u70b3
u70b3 force-pushed the fix/memwal-shard-spec-conflict branch from 0f7bd7f to dc95ea2 Compare July 31, 2026 11:20
u70b3 added a commit to u70b3/lance that referenced this pull request Jul 31, 2026
The proto contract promises sharding spec ids are never reused, but the
public init path hardcoded id 1 for every automatic sharding
configuration, leaving distinct specs indistinguishable at shard-claim
time.

- add next_spec_id(): greatest id + 1, with 0 reserved as the
  manual-shard sentinel (MANUAL_SHARD_SPEC_ID)
- reject reserved/duplicate spec ids when decoding MemWalIndexDetails
- add MemWalIndexDetails::active_sharding_spec() (greatest id)
- mem_wal_writer resolves an unset (0) shard_spec_id to the active spec
  and rejects an explicit mismatch before any shard claim

Prerequisite for lance-format#7949.
u70b3 added a commit to u70b3/lance that referenced this pull request Aug 2, 2026
The proto contract promises sharding spec ids are never reused, but the
public init path hardcoded id 1 for every automatic sharding
configuration, leaving distinct specs indistinguishable at shard-claim
time.

- add next_spec_id(): greatest id + 1, with 0 reserved as the
  manual-shard sentinel (MANUAL_SHARD_SPEC_ID)
- reject reserved/duplicate spec ids when decoding MemWalIndexDetails
- add MemWalIndexDetails::active_sharding_spec() (greatest id)
- mem_wal_writer resolves an unset (0) shard_spec_id to the active spec
  and rejects an explicit mismatch before any shard claim

Prerequisite for lance-format#7949.
u70b3 added a commit to u70b3/lance that referenced this pull request Aug 3, 2026
The proto contract promises sharding spec ids are never reused, but the
public init path hardcoded id 1 for every automatic sharding
configuration, leaving distinct specs indistinguishable at shard-claim
time.

- add next_spec_id(): greatest id + 1, with 0 reserved as the
  manual-shard sentinel (MANUAL_SHARD_SPEC_ID)
- reject reserved/duplicate spec ids when decoding MemWalIndexDetails
- add MemWalIndexDetails::active_sharding_spec() (greatest id)
- mem_wal_writer resolves an unset (0) shard_spec_id to the active spec
  and rejects an explicit mismatch before any shard claim

Prerequisite for lance-format#7949.
@u70b3
u70b3 force-pushed the fix/memwal-shard-spec-conflict branch from dc95ea2 to f2ab822 Compare August 3, 2026 10:05
@u70b3
u70b3 force-pushed the fix/memwal-shard-spec-conflict branch from f2ab822 to 3eaa2e5 Compare August 3, 2026 10:20
@u70b3

u70b3 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Scope and dependency reset completed and force-pushed on latest main.

I narrowed the claim to the identities current Lance code actually emits: manual 0 and sole automatic 1. The manifest guard is independently useful under that invariant: a manual caller cannot claim/fence an automatic shard, an automatic caller cannot claim/fence a manual shard, and a concurrent first claim cannot be followed by the opposite identity. Rejection remains side-effect free and is revalidated after write conflicts.

This PR no longer claims that monotonic spec allocation is a prerequisite or attempts to solve future sharding revisions/activation. #8112 was separately reduced to resolving the writer's default 0 before a fresh automatic manifest is created. Either PR is independently valid; together they cover creation-time resolution and stored-manifest immutability.

The issue reproducer and tests now use only 0/1; the public error docs also include sealed/no-mutation behavior. Fourteen manifest tests and full workspace clippy pass after the latest-main rebase.

The existing CHANGES_REQUESTED review predates this scope decision. Please re-review the independent defensive invariant.

@u70b3
u70b3 force-pushed the fix/memwal-shard-spec-conflict branch 3 times, most recently from fc60c80 to 27fa780 Compare August 6, 2026 12:50
@u70b3
u70b3 force-pushed the fix/memwal-shard-spec-conflict branch from 27fa780 to 87ea068 Compare August 11, 2026 06:14
@u70b3

u70b3 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

@hamersaw PTAL when you have a chance — the open CHANGES_REQUESTED predates the Aug 3 scope reset; the branch is current with main and all checks pass.

@u70b3

u70b3 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

@Xuanwo could you help review this when you have a moment? The CHANGES_REQUESTED (2026-07-27) predates the 2026-08-03 scope reset — the PR is now an independent defensive invariant over the 0/1 shard identities Lance actually emits, no longer gated on monotonic spec-id allocation (that lives in #8112). It's rebased onto current main with the #8640 conflict resolved (the claim path re-validates via refresh_latest per its uncached-read invariant), and CI is fully green. I've re-requested review from hamersaw twice over the past weeks without a response, so a fresh pair of eyes would be much appreciated.

@u70b3
u70b3 force-pushed the fix/memwal-shard-spec-conflict branch from 8e5795f to bcdf203 Compare September 7, 2026 02:11
@lance-gatekeeper lance-gatekeeper Bot removed the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 7, 2026
lance-gatekeeper[bot]

This comment was marked as outdated.

@lance-gatekeeper lance-gatekeeper Bot added the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 7, 2026
Xuanwo added a commit that referenced this pull request Sep 8, 2026
## Summary

- resolve the default dataset-writer `shard_spec_id` (`0`) to the MemWAL
index's sole automatic sharding spec before creating or claiming a shard
manifest
- accept an explicit id only when it matches that sole spec, and require
manually sharded indexes to keep id `0`
- reject metadata with multiple automatic specs because the current
writer model supports at most one
- cover the original fresh-shard bug at the public
`Dataset::mem_wal_writer` boundary plus invalid manual, mismatched, and
multiple-spec inputs

## Scope

This PR fixes the current writer path only. MemWAL initialization
already creates either no spec (manual sharding) or one automatic spec
with id `1`; the writer must record that same identity instead of its
unresolved default `0`.

It does not add monotonic allocation, spec revisions, activation
semantics, metadata validation for hypothetical producers, or a
file-format change. Those require a separate end-to-end design if a
re-shard API is introduced.

This PR complements but is not a prerequisite for #7949: this PR
resolves the table identity before a fresh manifest is created, while
#7949 defensively rejects changing an identity already stored in a
manifest.

## Testing

- `cargo fmt --all -- --check`
- `cargo test -p lance --lib
test_writer_shard_spec_resolution_rejects_invalid_identity --
--nocapture`
- `cargo test -p lance --lib
test_mem_wal_writer_uses_automatic_sharding_spec -- --nocapture`
- `cargo clippy --all --tests --benches -- -D warnings`

---------

Co-authored-by: Xuanwo <github@xuanwo.io>
@u70b3
u70b3 force-pushed the fix/memwal-shard-spec-conflict branch from bcdf203 to 35db3d1 Compare September 10, 2026 08:59
@lance-gatekeeper lance-gatekeeper Bot removed the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 10, 2026
lance-gatekeeper[bot]

This comment was marked as outdated.

@lance-gatekeeper lance-gatekeeper Bot added the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 10, 2026
@u70b3
u70b3 force-pushed the fix/memwal-shard-spec-conflict branch from 35db3d1 to 0b72a5f Compare September 10, 2026 13:37
@u70b3

u70b3 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up on codecov's patch-coverage note (94.5%, 3 lines): measured branch coverage locally with cargo +nightly llvm-cov --show-missing-lines and closed the gap. The uncovered production path was the continuation of claim_epoch's post-write-conflict revalidation — every prior test entering that branch exited at the rejection ?, so the "revalidation passes → retry claims past a benign version bump" continuation (and the retry-exhaustion path) never ran.

Added deterministic coverage (a ProxyObjectStore before-policy lands a peer manifest between the claim's read and its conditional rename, making the race deterministic instead of usually-lost):

  • test_claim_epoch_revalidates_after_write_conflict (rstest: different_shard_spec, sealed_by_concurrent_drop) — conflict → revalidation rejects with InvalidInput, manifest untouched
  • test_claim_epoch_retries_past_benign_version_bump — same-spec peer wins the version → retry claims v2 on the peer's tip
  • test_claim_epoch_exhausts_retries_when_writes_keep_failing — transient write errors exhaust the retry budget with a contention error, nothing durable
  • restructured the concurrent test's match to remove a dead panic! arm codecov counted against the patch

All patch lines are now covered (validate_claimable: sealed=2, mismatch=4 call sites exercised; several pre-existing lines gained coverage as a side effect). Validation: cargo test -p lance dataset::mem_wal::manifest --lib (28 passed), cargo fmt --all -- --check, cargo clippy --all --tests --benches -- -D warnings — all green.

@lance-gatekeeper lance-gatekeeper Bot removed the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 10, 2026
lance-gatekeeper[bot]

This comment was marked as outdated.

@lance-gatekeeper lance-gatekeeper Bot added the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 10, 2026
@u70b3
u70b3 force-pushed the fix/memwal-shard-spec-conflict branch from 0b72a5f to c0e8714 Compare September 10, 2026 14:31
@lance-gatekeeper lance-gatekeeper Bot removed the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 10, 2026
lance-gatekeeper[bot]

This comment was marked as outdated.

@lance-gatekeeper lance-gatekeeper Bot added the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 10, 2026
@u70b3

u70b3 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

CI note: "Python macOS 3.14 ARM" failed in test_mem_wal.py::test_shard_writer_lsm_scanner_includes_own_sstables with page_lookup.lance Not found under _pk_index/ — the known pre-existing main race tracked in #9035 (flush publishes the SSTable before the pk sidecar exists; macOS runners with few vCPUs are the easy hit). Unrelated to this change: the only delta on this head is a test-helper fix in the Rust manifest tests, and the same Python test suite passed on the previous head. Retriggering via close/reopen.

@u70b3 u70b3 closed this Sep 10, 2026
@u70b3 u70b3 reopened this Sep 10, 2026
@lance-gatekeeper lance-gatekeeper Bot added K-approved Latest Gatekeeper recommendation permits acceptance. and removed K-approved Latest Gatekeeper recommendation permits acceptance. labels Sep 10, 2026
@u70b3
u70b3 force-pushed the fix/memwal-shard-spec-conflict branch from c0e8714 to b76f664 Compare September 16, 2026 01:42
@lance-gatekeeper lance-gatekeeper Bot removed the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 16, 2026

@lance-gatekeeper lance-gatekeeper 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.

Gate recommendation: approve.

The rebase preserves the reviewed patch exactly and does not change the manifest store or its writer/API boundary. The guard still enforces shard identity before epoch mutation and after write conflicts, with #8112 supplying the complementary writer-side resolution. The deterministic conflict tests continue to cover mismatched or sealed state, benign retries, and bounded exhaustion under the updated dependency set.

@lance-gatekeeper lance-gatekeeper Bot added the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 16, 2026
@u70b3
u70b3 force-pushed the fix/memwal-shard-spec-conflict branch from b76f664 to 7c92d4f Compare September 17, 2026 03:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working K-approved Latest Gatekeeper recommendation permits acceptance.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: reject mismatched shard spec before epoch claim

2 participants