Skip to content

compute: select the buffer pool's file store from the scratch directory - #39520

Open
antiguru wants to merge 8 commits into
pool-extents/file-modefrom
pool-extents/plumbing
Open

antiguru wants to merge 8 commits into
pool-extents/file-modefrom
pool-extents/plumbing

Conversation

@antiguru

@antiguru antiguru commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

The process-wide pool now takes its extent backend from a closure. apply_pool_config evaluates the closure exactly once, when it installs the pool.

Compute chooses the backend as follows:

  • It uses the file store in the replica's scratch directory when the new enable_column_paged_batcher_file_extents dyncfg is on.
  • It uses swap otherwise.
  • If the file store fails to open, it falls back to swap with a warning.

The backend is fixed for the life of the process, so changing the flag later has no effect.

The flag defaults to off, and mzcompose turns it on so CI exercises the file store. New metrics export the file store's counters, its read latency, and which backend is installed.

🤖 Generated with Claude Code

@antiguru
antiguru added this pull request to stack #39522 October 3, 2026 19:21
@antiguru antiguru changed the title pool extents/plumbing compute: select the buffer pool's file store from the scratch directory Oct 3, 2026
@antiguru
antiguru force-pushed the pool-extents/plumbing branch 3 times, most recently from 8acdc1d to d7be021 Compare October 4, 2026 15:34
@antiguru
antiguru force-pushed the pool-extents/plumbing branch from d7be021 to d80ce97 Compare October 5, 2026 14:24
@antiguru
antiguru force-pushed the pool-extents/plumbing branch 2 times, most recently from 596b1b0 to 63104d5 Compare October 5, 2026 17:30
@antiguru
antiguru force-pushed the pool-extents/plumbing branch 2 times, most recently from d25202c to 122fbe3 Compare October 7, 2026 06:23
@antiguru
antiguru force-pushed the pool-extents/plumbing branch from 122fbe3 to 2b47437 Compare October 9, 2026 13:37
@antiguru
antiguru force-pushed the pool-extents/plumbing branch 2 times, most recently from 2b47437 to 15c56a4 Compare October 9, 2026 14:34
@antiguru
antiguru force-pushed the pool-extents/plumbing branch from 15c56a4 to 463ef3a Compare October 9, 2026 15:12
@antiguru
antiguru marked this pull request as ready for review October 9, 2026 15:15
@antiguru
antiguru requested review from a team as code owners October 9, 2026 15:15
@def-

def- commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- File store sizes itself to the whole shared scratch volume and keeps its peak, starving lgalloc and RocksDB

src/compute/src/compute_state.rs:559

capacity_bytes: None lets the pool's file store claim nearly all of /scratch, and it keeps every block it ever allocated. The same volume backs lgalloc's arenas and the upsert RocksDB state. After a demotion peak, lgalloc page faults hit ENOSPC and clusterd dies with SIGBUS, and RocksDB writes fail, which puts upsert sources into an error state.

Details
  • None makes FileStore::open take the volume's free space minus max(1 GiB, 2%) (src/ore/src/pool/file.rs:220). That size is measured once, when the pool installs at instance creation, before lgalloc or RocksDB have grown. After that, capacity only ever goes down, after the store itself hits ENOSPC.
  • FileStore::free leaves a freed slot's blocks allocated (src/ore/src/pool/file.rs:348). The store punches holes only inside alloc, to make room for its own allocations. So the pool's disk footprint stays at its demotion high-water mark for the life of the process, even after its chunks are gone.
  • The same directory is lgalloc's path (compute_state.rs:415). enable_lgalloc and enable_columnation_lgalloc both default to true. lgalloc ftruncates sparse files and maps them MAP_SHARED, so every page it touches for the first time needs a free block. When none is left, the fault is SIGBUS. src/orchestrator-kubernetes/src/lib.rs:1362 already records exit 135 as "lgalloc runs out of disk". Upsert RocksDB lives under scratch/storage/upsert (src/storage/src/upsert.rs:588).
  • Here is a concrete path on a replica with a scratch disk and no swap, with spill and this flag on. An index hydration demotes the batcher's spilled input to the file store. The batches seal and their chunks are freed, but the blocks stay allocated. The arrangement built from those batches is then allocated through lgalloc on a volume with only the 1 GiB / 2% headroom left.

Before this change the pool never wrote to scratch, so this contention is new. Possible fixes: pass an explicit bound, for example a dyncfg fraction of the volume as capacity_bytes: Some(..). Or let the store punch warm slots above a small cap when they are freed, so it hands space back once its data is gone. Or do both.

@antiguru
antiguru force-pushed the pool-extents/plumbing branch from 463ef3a to 2ab89a8 Compare October 14, 2026 20:44

@bosconi bosconi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The selection and fallback logic looks right, but the points from the previous round are still open. Details inline.

  • Add the new flag to KNOWN_MISSING_FROM_LD in test/launchdarkly-flag-consistency/mzcompose.py next to its siblings, or the nightly launchdarkly-flag-consistency step fails unless an LD flag exists first.
  • Nothing confirms CI runs on the file store. If CI's /scratch were memory-backed or the open failed, every replica would fall back to swap with CI still green. An mzcompose or testdrive check that a replica with spill on reports mz_column_pool_backend{backend=~"file_.*"} = 1 and mz_column_pool_backend_fallback = 0 would close that.
  • def-'s comment about capacity_bytes: None sizing against the whole shared scratch volume still applies.

Some(scratch) if ENABLE_COLUMN_PAGED_BATCHER_FILE_EXTENTS.get(config) => {
ExtentBackend::File {
dir: scratch.join("pool"),
capacity_bytes: None,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

When the volume has 1 GiB or less available, FileStore::open derives a capacity of 0 and still succeeds, so build_pool installs a store that can never take an extent, extents stay in RAM, and mz_column_pool_backend_fallback reads 0. An explicit bound would cover this and def-'s case: his dyncfg fraction, or a minimum capacity below which build_pool takes the swap path.

Comment thread src/timely-util/src/pool_config.rs Outdated
Comment on lines +54 to +55
/// Whether [`apply_pool_config`] was asked for a file backend and installed
/// the pool on swap instead.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This only covers installation. After a non-ENOSPC write error the store disables writes for its lifetime with no route back to swap, so say so here and in the mz_column_pool_backend_fallback help text.

Suggested change
/// Whether [`apply_pool_config`] was asked for a file backend and installed
/// the pool on swap instead.
/// Whether [`apply_pool_config`] was asked for a file backend and installed
/// the pool on swap instead. Covers installation only: a store that later
/// disables writes on an I/O error reports through
/// `mz_column_pool_extent_file_write_errors_total`.

@antiguru
antiguru force-pushed the pool-extents/plumbing branch 3 times, most recently from 433691d to cf8db0e Compare October 28, 2026 10:43
@def-

def- commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- File store can take the shared scratch volume from lgalloc, the column pager and RocksDB

src/compute/src/compute_state.rs:567

Compute opens the file store with capacity_bytes: None, so the store's capacity is the free space on the replica's scratch volume at install time, minus max(1 GiB, 2%). The same volume holds lgalloc's mmapped files, the column pager's spill files and upsert RocksDB state, and none of them handle ENOSPC gracefully. Freed file slots keep their blocks, so a single spill peak can leave those writers without space for the rest of the process's life.

Details

Capacity is computed once, at src/ore/src/pool/file.rs:220, when the first config apply with a spill gate on installs the pool. That is near boot, before the other scratch users have grown. Once they grow by more than the 1 GiB or 2% headroom, the pool's fallocate keeps succeeding until the filesystem is actually full. The pool handles its own ENOSPC by lowering its capacity. The next writer to hit ENOSPC is one of the following, and it fails:

  • lgalloc gets the scratch path in this same function (compute_state.rs:420). It grows sparse files with ftruncate and maps them, so a write that faults on a hole in a full filesystem raises SIGBUS.
  • The column pager uses the File backend whenever scratch exists (compute_state.rs:505), and its pager::pageout_with panics on a write error (src/timely-util/src/column_pager.rs:441, :496).
  • Upsert keeps RocksDB under scratch/storage/upsert (src/storage/src/upsert.rs:589), where a write error makes the source report an error.

FileStore::free puts a slot on the warm list and leaves its blocks allocated. punch_for releases them only when the pool's own allocation would exceed its capacity, so mz_column_pool_extent_file_allocated_bytes is a high-water mark the other writers never get back. Swap replicas get no scratch disk (src/controller/src/clusters.rs:735), so the flag only matters on disk replicas, which run lgalloc and RocksDB. The comment that the subdirectory prevents collisions covers file names, not space.

A fix is to pass an explicit capacity_bytes, for example a dyncfg fraction of the volume, so the pool's share of the scratch disk is planned alongside the other users.

@antiguru
antiguru force-pushed the pool-extents/plumbing branch 2 times, most recently from 5d4964d to c362ece Compare October 29, 2026 16:25

@antiguru antiguru left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Posted by Codex.

Comment thread src/compute-types/src/dyncfgs.rs Resolved
@antiguru
antiguru force-pushed the pool-extents/plumbing branch from c362ece to 15e78e0 Compare October 29, 2026 19:19
@antiguru
antiguru requested a review from a team as a code owner October 29, 2026 19:19
antiguru and others added 8 commits November 6, 2026 17:43
The process-wide pool now takes its extent backend from a closure that
apply_pool_config evaluates exactly once, when it installs the pool. Compute
picks the file store at the replica's scratch directory when the new
enable_column_paged_batcher_file_extents dyncfg is on, and swap otherwise.
A store that fails to open falls back to swap with a warning. Since the
backend is fixed for the process, later changes to the flag have no effect.

The flag defaults off and is on in mzcompose so CI exercises the file store
wherever a replica has a scratch directory. New metrics report the file
store's counters, its read latency, and the installed backend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ytes

The pool's backend is the ore ExtentBackend, passed by value. Directory
creation and the fallback to swap happen once, when the pool is built.
apply_worker_config derives the scratch path once for both the pager and
the pool, and the store's allocated bytes are exported as a metric.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add mz_column_pool_backend_fallback, set when the pool was asked for a
file backend and installed on swap, since the backend gauge alone reads the
same as a configured swap backend. Reword the fallback warning, which also
fires when the pool's own reservation failed. Move the install-once
contract onto apply_pool_config, correct the read-latency bucket help to
"less than le", and fix the parallel workload's description of when a flag
flip takes effect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lists the file-extents flag as known missing from LaunchDarkly, and says
that the fallback flag and metric cover installation only, with later
I/O errors reported through the write-errors metric.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A store that fails to open falls back to swap with every query still
passing, so the cluster composition asserts the backend metrics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add column_paged_batcher_file_release_interval and
column_paged_batcher_file_release_bytes, defaulting to the values of
lgalloc's background worker (1 s and 128 MiB per class), and pass them
through PoolPagerConfig to the pool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ters

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antiguru
antiguru force-pushed the pool-extents/plumbing branch from 15e78e0 to 1ba4097 Compare November 6, 2026 16:49

This branch has not been deployed

No deployments
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