Skip to content

[ENH](wal3): pin fragment generations for conditional writes - #7672

Merged
rescrv merged 3 commits into
mainfrom
rescrv/tx-perf
Sep 2, 2026
Merged

[ENH](wal3): pin fragment generations for conditional writes#7672
rescrv merged 3 commits into
mainfrom
rescrv/tx-perf

Conversation

@rescrv

@rescrv rescrv commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Description of changes

S3-backed conditional writes previously validated the committed log
against a moving target: a concurrent publish could open and close a
fragment generation between validation and append, forcing a retry.
Introduce FragmentPin so a caller can hold the next fragment generation
open while it reads the manifest and prepares its append, ensuring it
validates against the generation its append will join.

Add FragmentPin, a generation-scoped, must-use reservation:

  • acquire_fragment_pin waits for the active generation to finish, then
    reserves bytes against the fragment size limit and returns a pin.
  • Work submitted with a pin joins the pinned generation; dropping a pin
    releases its reservation so cancelled or invalidated requests make
    progress without an explicit cleanup call.
  • take_work holds selected work until every pin submits or drops, and
    bumps pin_generation when a batch is selected for write.

Thread the pin through push_work, append_many_with_options, and
LogWriterTrait, and add acquire_fragment_pin to the trait and its
S3/replicated implementations. Supplying a pin disables transparent
contention retries because a pin cannot transfer to a recovered writer
generation.

In log-service, S3-backed conditional pushes now acquire a fragment pin
before validation and record the wait in a new
conditional_write_fragment_pin_wait_us histogram; replicated logs stay
serialized in Spanner and skip pinning. Factor the repeated writer
shutdown blocks in handle_errors_and_contention into shutdown_epoch.

Test plan

CI

Migration plan

No change to disk structures.

Observability plan

Counters + tracing

Documentation Changes

N/A

Co-authored-by: AI

S3-backed conditional writes previously validated the committed log
against a moving target: a concurrent publish could open and close a
fragment generation between validation and append, forcing a retry.
Introduce FragmentPin so a caller can hold the next fragment generation
open while it reads the manifest and prepares its append, ensuring it
validates against the generation its append will join.

Add FragmentPin, a generation-scoped, must-use reservation:
- acquire_fragment_pin waits for the active generation to finish, then
  reserves bytes against the fragment size limit and returns a pin.
- Work submitted with a pin joins the pinned generation; dropping a pin
  releases its reservation so cancelled or invalidated requests make
  progress without an explicit cleanup call.
- take_work holds selected work until every pin submits or drops, and
  bumps pin_generation when a batch is selected for write.

Thread the pin through push_work, append_many_with_options, and
LogWriterTrait, and add acquire_fragment_pin to the trait and its
S3/replicated implementations.  Supplying a pin disables transparent
contention retries because a pin cannot transfer to a recovered writer
generation.

In log-service, S3-backed conditional pushes now acquire a fragment pin
before validation and record the wait in a new
conditional_write_fragment_pin_wait_us histogram; replicated logs stay
serialized in Spanner and skip pinning.  Factor the repeated writer
shutdown blocks in handle_errors_and_contention into shutdown_epoch.

Co-authored-by: AI

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@rescrv
rescrv requested a review from dbeglord September 2, 2026 17:25
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

Reviewer Checklist

Please leverage this checklist to ensure your code review is thorough before approving

Testing, Bugs, Errors, Logs, Documentation

  • Can you think of any use case in which the code does not behave as intended? Have they been tested?
  • Can you think of any inputs or external events that could break the code? Is user input validated and safe? Have they been tested?
  • If appropriate, are there adequate property based tests?
  • If appropriate, are there adequate unit tests?
  • Should any logging, debugging, tracing information be added or removed?
  • Are error messages user-friendly?
  • Have all documentation changes needed been made?
  • Have all non-obvious changes been commented?

System Compatibility

  • Are there any potential impacts on other parts of the system or backward compatibility?
  • Does this change intersect with any items on our roadmap, and if so, is there a plan for fitting them together?

Quality

  • Is this code of a unexpectedly high quality (Readability, Modularity, Intuitiveness)

@rescrv

rescrv commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

@claude review once

Comment thread rust/log-service/src/lib.rs Outdated
@rescrv

rescrv commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

@claude review once

@blacksmith-sh

blacksmith-sh Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Found 1 test failure on Blacksmith runners:

Failure

Test View Logs
worker/
work_queue::tests::integration::tests::test_k8s_integration_work_queue_fifo_and_filteri
ng
View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

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

Code review found no issues

No high-confidence issues detected in this change.


#[async_trait::async_trait]
impl LogWriterTrait for FakeLogWriter {
async fn acquire_fragment_pin(

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.

Head of line blocking. If one transaction is very slow, and holds the generation open, it'll block all future generations right? Or can multiple generations be open at once?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If one transaction is very slow you will get HOL on the generation. This is not a problem as there are a finite number of requests allowed in at once, and wal3 alredy does HOL blocking on the batch.

Comment on lines +82 to +85
fn can_reserve(&self, byte_count: usize, batch_size_bytes: usize) -> bool {
let occupied_bytes = self.occupied_bytes();
occupied_bytes == 0 || occupied_bytes.saturating_add(byte_count) < batch_size_bytes
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The can_reserve logic allows bypassing the batch size limit when the generation is empty. The condition occupied_bytes == 0 short-circuits the size check, allowing arbitrarily large reservations:

fn can_reserve(&self, byte_count: usize, batch_size_bytes: usize) -> bool {
    let occupied_bytes = self.occupied_bytes();
    occupied_bytes == 0 || occupied_bytes.saturating_add(byte_count) < batch_size_bytes
}

If occupied_bytes == 0, the function returns true regardless of whether byte_count >= batch_size_bytes. This allows a single reservation to exceed the configured fragment size limit, potentially creating fragments much larger than intended.

Fix: Remove the short-circuit and consistently enforce the size limit:

fn can_reserve(&self, byte_count: usize, batch_size_bytes: usize) -> bool {
    self.occupied_bytes().saturating_add(byte_count) <= batch_size_bytes
}

Or if allowing oversized first reservations is intentional (to prevent starvation), add:

fn can_reserve(&self, byte_count: usize, batch_size_bytes: usize) -> bool {
    let occupied_bytes = self.occupied_bytes();
    occupied_bytes == 0 && byte_count <= batch_size_bytes 
        || occupied_bytes.saturating_add(byte_count) <= batch_size_bytes
}
Suggested change
fn can_reserve(&self, byte_count: usize, batch_size_bytes: usize) -> bool {
let occupied_bytes = self.occupied_bytes();
occupied_bytes == 0 || occupied_bytes.saturating_add(byte_count) < batch_size_bytes
}
fn can_reserve(&self, byte_count: usize, batch_size_bytes: usize) -> bool {
let occupied_bytes = self.occupied_bytes();
occupied_bytes == 0 && byte_count <= batch_size_bytes
|| occupied_bytes > 0 && occupied_bytes.saturating_add(byte_count) <= batch_size_bytes
}

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I saw this and the alternative is a degradation from what we support today.

@rescrv
rescrv enabled auto-merge (squash) September 2, 2026 18:44
@rescrv
rescrv merged commit 8cd485c into main Sep 2, 2026
258 of 262 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.

3 participants