Skip to content

feat(server): scale rollout stores via registry + LRU + global sweeper - #136

Merged
beinan merged 1 commit into
lance-format:mainfrom
beinan:per-experiment-lru-registry
Jul 14, 2026
Merged

beinan merged 1 commit into
lance-format:mainfrom
beinan:per-experiment-lru-registry

Conversation

@beinan

@beinan beinan commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Why

We want to partition rollouts by experiment_name — one physical Lance dataset per experiment. At the target scale (tens to hundreds of thousands of experiments) the current server runtime cannot cope:

  1. rollout_stores is an unbounded HashMap of open handles → never evicted → OOM.
  2. Each store spawns its own tokio cleanup timer (spawn_periodic_cleanup) → 100k+ background timers wedge the process.
  3. list_rollout_stores only enumerates the in-memory cache, so with eviction it would be incomplete, and there is no durable directory to enumerate.

This PR decouples resident memory / background tasks from the number of stores.

What

  • Durable registry (RolloutRegistry, new crates/lance-context-core/src/registry.rs): a single small Lance table (name, uri, created_at) that is the source of truth for which stores exist. create / list / delete / existence checks now go through it. Existence is no longer inferred from cache membership, so eviction cannot cause a false 404, and list returns every store.
  • Bounded LRU replaces the unbounded HashMap; evicted stores are transparently reopened from storage on the next request (reusing open_existing_with_options from fix(server): lazily open stores on cache miss (multi-replica 404) #135).
  • Single global sweeper (AppState::spawn_global_sweeper) replaces one-timer-per-store. It sweeps only the resident set each tick, with the same per-pass tokio::time::timeout hardening as fix(rollout): bound periodic WAL cleanup pass with a timeout #133. Cold (evicted) stores accrue no new generations and are swept again once re-opened.
  • RolloutStoreInfo.version → Option<u64> (omitted in list to avoid opening every dataset; populated for single get/create). Client updated (unwrap_or(0)).
  • New --rollout-cache-capacity / ROLLOUT_CACHE_CAPACITY (default 2000).

Tests

  • registry.rs: upsert idempotency, contains/remove, list, survive-reopen.
  • Server: evicted-store-transparently-reopened (capacity-1 LRU), list-enumerates-all-registered, delete-unregisters, sweeper gating. Existing multi-replica regression tests (fix(server): lazily open stores on cache miss (multi-replica 404) #135) still pass.
  • cargo test --workspace, cargo clippy --workspace --all-targets, cargo fmt --check all clean.

Follow-ups (out of scope)

  • Startup reconciliation (scan data_dir to backfill registry rows if a crash left a dataset without its row).
  • Optional btree index on registry.name if the narrow scan becomes hot.

🤖 Generated with Claude Code

…try + LRU + global sweeper

Per-experiment physical partitioning (one dataset per experiment) requires the
server runtime to decouple resident memory and background tasks from the number
of stores. Three changes make that safe at 100k+ experiments:

- Durable directory (`RolloutRegistry`, a single Lance table) is the source of
  truth for which stores exist. `create`/`list`/`delete`/existence checks go
  through it instead of the in-memory map, so eviction can no longer cause a
  false 404 and `list` enumerates every store, not just resident ones.
- Bounded LRU replaces the unbounded `HashMap` of store handles; evicted stores
  are transparently reopened on the next request.
- A single process-wide WAL-cleanup sweeper replaces the former
  one-tokio-timer-per-store model (which would spawn 100k+ timers). It sweeps
  the resident set with the same per-pass timeout as the periodic cleanup.

`RolloutStoreInfo.version` becomes `Option<u64>` (omitted in list responses to
avoid opening every dataset; populated for single-store get/create). Adds
`--rollout-cache-capacity` / `ROLLOUT_CACHE_CAPACITY`.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
@beinan
beinan merged commit 5784d3b into lance-format:main Jul 14, 2026
9 checks passed
beinan added a commit that referenced this pull request Jul 14, 2026
…I) (#137)

## Summary
First slice of the **`lance-context-master`** control-plane process for
the per-experiment rollout deployment (builds on #136's registry + LRU).
Delivers the read-only observability surface; CompactionScheduler and
the React UI follow in separate PRs.

- **core**: `RolloutStore::observe() -> RolloutObservation` + read-only
`pending_wal_generations()` — cheap metadata-only metrics (row/fragment
counts, manifest version + timestamp, pending MemWAL generations); no
full scan.
- **api**: `ExperimentSummary` / `ExperimentDetail` /
`ExperimentListResponse` / `CompactJobStatus` DTOs.
- **master**: new crate with
- `StatsStore` (`_stats.rollout.lance`, delete-then-append idempotent
upsert, `name LIKE` search + pagination),
- bounded-concurrency background `StatsScanner` that enumerates the
registry, observes each store read-only under a per-experiment timeout,
and reconciles away deleted experiments,
- `GET /api/v1/experiments`, `GET /api/v1/experiments/{name}`
(`?fresh=true` recompute), `POST /api/v1/rescan`,
- optional `--ui-dir` ServeDir wiring (SPA fallback) for the later UI
PR.

## Design
Master shares the data-plane's `data_dir`. It **reads** the registry
(data-plane-owned) and **owns** the stats table — no write conflict.
Pure direct connection: master opens datasets itself; it does not go
through the data-plane.

## Test plan
- [x] core: `observe_reports_cheap_metrics`
- [x] master: stats_store idempotency / get+remove / LIKE search +
pagination / reopen / sentinel mapping
- [x] master: scan populates + list/search; scan reconciles removed
experiments; detail 404
- [x] `cargo test` / `clippy` / `fmt` green for core/api/master

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
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.

1 participant