perf(rollout): reuse a resident MemWAL ShardWriter across appends#159
Merged
Conversation
RolloutStore::add opened a fresh ShardWriter per append and immediately close()d it. Each open cold-rebuilt the object store (ObjectStore::from_uri constructs a fresh registry -> new connection pool), so every append paid a cold DNS resolution + TCP/TLS handshake and a redundant claim_epoch. On Azure blob this dominated append latency (p50 ~1.5s, tail 12-100s) and left the pod with a pile of TIME_WAIT sockets and zero long-lived connections. Hold one resident ShardWriter per shard instead. add() now does put -> force_seal_active -> wait_for_flush_drain, which still commits a flushed generation to the shard manifest before returning, so cross-instance read-after-write visibility is unchanged; only the per-append open/claim/ reconnect churn is removed. The epoch is claimed once at open and the connection pool is reused. Correctness: - merge_own_shard claims the shard epoch (which fences a live writer), so it now closes and clears the resident writer first; the next add reopens against the fresh epoch. - add retries once, reopening the writer, if an operation is fenced. - ShardWriter has no Drop, so add close() for graceful draining plus a best-effort Drop impl that spawns a detached close() when a runtime exists, avoiding background-task leaks on LRU eviction. Tests: resident-writer epoch stays stable across appends with each append immediately visible; merge reopens the writer without fencing; close drains and add reopens.
This was referenced Jul 21, 2026
beinan
added a commit
that referenced
this pull request
Jul 21, 2026
## Background The Rust Tests job has been consistently slow (~23min). Two unrelated PRs (#151, #159) showed the same pattern, which pointed to a CI config issue rather than anything a specific change introduced. ## Root cause 1. **Wrong rust-cache path** (the real culprit): `workspaces` pointed at `crates/lance-context-core` and `python`, but this is a **single root workspace** whose build output lives in `./target`. Those subdirs have no `target/`, so the cache was always empty and every run rebuilt all dependencies from scratch — including the rocksdb C++ library compiled from source. 2. **Doctest step has near-zero value**: `lance-context-core` has a single doctest (~7s locally), but the step re-links the entire rocksdb-bearing crate, costing ~6.85min per run. ## Changes - `workspaces: "."` — cache the actual `./target`. - Remove the `Run doc tests` step. ## Expected impact - Drops the doctest step outright: **~-7min**. - Once the cache is warm, dependencies (especially rocksdb) are no longer rebuilt every run. The first run still has to populate the cache; from the **second run onward** compile time drops substantially. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
beinan
added a commit
that referenced
this pull request
Jul 21, 2026
## Summary - add a first-class DatagenStore backed by one append-only Lance log, avoiding cross-dataset consistency hazards - model item lifecycle, field deltas, failures, terminals, no-op step completion, deterministic retry ids, fold, and trajectory reads - use multi-shard MemWAL ingest with a resident writer, retry deduplication, per-shard cleanup, and row-id blob point reads - document the single-log schema and re-export the public API from lance-context ## Testing - cargo fmt --all -- --check - cargo clippy --workspace --all-targets -- -D warnings - cargo test -p lance-context-core datagen --no-fail-fast (8 passed) - cargo test -p lance-context-core --no-fail-fast: 148 unit tests passed; the existing rollout wal_merge_generation_cleanup integration test fails on current main after #159 with 32 rows observed for 30 appends - uv run ruff format --check python/ - uv run ruff check python/ - uv run pyright - uv run pytest: 181 passed, 5 skipped, 1 xfailed; 10 unrelated current-main failures in stale DummyInner signatures and moto S3 bucket visibility
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
RolloutStore::addopened a brand-newShardWriterfor every append and immediatelyclose()d it. Eachopencold-rebuilds the object store —ObjectStore::from_uriconstructs a freshObjectStoreRegistry::default(), i.e. a new connection pool — so every append paid a cold DNS resolution + TCP/TLS handshake plus a redundantclaim_epoch, then threw the connection away.On Azure blob (where DNS resolution is the slow part) this dominated append latency (p50 ~1.5s, tail 12–100s) and left pods with a pile of
TIME_WAITsockets and zero long-lived connections.This holds one resident
ShardWriterper shard and reuses it across appends, so the epoch is claimed once and the object-store connection pool is reused (DNS resolved once). Per append we now doput→force_seal_active→wait_for_flush_drain, which still commits a flushed generation to the shard manifest before returning — cross-instance read-after-write visibility is unchanged. Only the per-append open/claim/reconnect churn is removed.Note: because we keep strong cross-instance visibility, the generation-per-visible-append (and the existing count/time-triggered self-merge that collapses them) is intentionally unchanged. This PR reduces latency, not generation count.
Details
write_writer: Option<ShardWriter>field, opened lazily viaensure_write_writer, reused acrossadd.merge_own_shardcallsclaim_epoch, which fences a live writer, so it nowclose()s and clears the resident writer first; the nextaddtransparently reopens against the fresh epoch.addretries once (reopening the writer) if an op returns a fence error.ShardWriterhas noDrop, so addedpub async fn close()for graceful draining plus a best-effortDropimpl that spawns a detachedclose()when a Tokio runtime exists — avoids background-task leaks on LRU eviction.Test plan
cargo test -p lance-context-core(25 passed; includes 3 new tests)add_reuses_resident_writer_epoch_stable— epoch does not advance per append; every append immediately visible viaget_by_id.merge_reopens_writer_after_epoch_claim— merge-per-append never surfaces a fence error; rows stay visible.close_drains_resident_writer_and_add_reopens—close()drops the writer (idempotent);addreopens.cargo build -p lance-context-server(unchanged callers compile)cargo clippy -p lance-context-core --tests(clean)cargo fmt --allESTABLISHEDconnections persist instead ofTIME_WAITchurn.🤖 Generated with Claude Code