feat(master): retire experiments idle for a week from the stats table - #210
Merged
Merged
Conversation
beinan
added a commit
to beinan/lance-context
that referenced
this pull request
Jul 27, 2026
Stacked on lance-format#210. Retirement (lance-format#210) removes cold experiments from the stats table, which is what keeps a scan round proportional to active work. On its own it also removes them from `GET /experiments`, which is only acceptable once they remain findable another way. This closes that. default list the stats table, i.e. experiments written recently enough not to have been retired. A flat list of every experiment ever created is neither renderable nor interesting at tens of thousands. search the stats table, plus the registry for names it no longer holds. Cold matches are observed on demand. detail on a stats miss, resolve through the registry and observe on demand rather than returning 404. So retirement removes an experiment from the *default* view and never makes it unfindable. `observe_cold` deliberately does not write a stats row. Reading about a retired experiment must not make it hot again, or browsing the UI would silently undo retirement and the table would creep back toward holding everything -- the state retirement exists to prevent. It also takes no `MasterState`, so it structurally cannot write one; a refactor that hands it one has to justify itself. Search bounds its on-demand observations by the page size. A query matching thousands of cold experiments must not open thousands of datasets to render one page. Known gap: the two handlers changed here live in `routes.rs`, whose tests all require etcd and are `#[ignore]`d, so CI does not cover them. `observe_cold` is tested in `scanner.rs`, which CI does run. Worth fixing, but as its own change rather than a fourth concern in this one. Co-Authored-By: Claude <noreply@anthropic.com>
beinan
force-pushed
the
feat/stats-retire-cold
branch
2 times, most recently
from
July 27, 2026 17:30
2493ed3 to
5df8dc3
Compare
beinan
added a commit
to beinan/lance-context
that referenced
this pull request
Jul 27, 2026
Stacked on lance-format#210. Retirement (lance-format#210) removes cold experiments from the stats table, which is what keeps a scan round proportional to active work. On its own it also removes them from `GET /experiments`, which is only acceptable once they remain findable another way. This closes that. default list the stats table, i.e. experiments written recently enough not to have been retired. A flat list of every experiment ever created is neither renderable nor interesting at tens of thousands. search the stats table, plus the registry for names it no longer holds. Cold matches are observed on demand. detail on a stats miss, resolve through the registry and observe on demand rather than returning 404. So retirement removes an experiment from the *default* view and never makes it unfindable. `observe_cold` deliberately does not write a stats row. Reading about a retired experiment must not make it hot again, or browsing the UI would silently undo retirement and the table would creep back toward holding everything -- the state retirement exists to prevent. It also takes no `MasterState`, so it structurally cannot write one; a refactor that hands it one has to justify itself. Search bounds its on-demand observations by the page size. A query matching thousands of cold experiments must not open thousands of datasets to render one page. Known gap: the two handlers changed here live in `routes.rs`, whose tests all require etcd and are `#[ignore]`d, so CI does not cover them. `observe_cold` is tested in `scanner.rs`, which CI does run. Worth fixing, but as its own change rather than a fourth concern in this one. Co-Authored-By: Claude <noreply@anthropic.com>
Stacked on lance-format#209. The stats table currently holds a row for every experiment that has ever existed, and each one costs an open on every scan round. At the scale this deployment is heading for — tens of thousands, the overwhelming majority written once and never again — that is the remaining reason a round cannot stay proportional to actual work. An experiment with no writes for `STATS_COLD_RETIRE_SECS` (default 7 days) is now dropped from the table. It stays in the registry and can still be observed on demand; it simply stops costing a row and an open every round. Retirement order is a correctness property, not an optimisation. Both auto-sweeps read the stats table and nothing else, so an experiment absent from it is invisible to them permanently. Dropping one that still has un-merged MemWAL generations would strand them: the rows stay readable because the read path unions the generations, but they are never folded into the base table, read amplification never recovers, the `_mem_wal/{shard}/` directories are never reclaimed, and no process is left that would notice. So retirement is: merge the WAL -> compact -> verify pending_wal_generations == 0 -> drop the row `compact_files` deliberately leaves MemWAL generations alone, so compaction on its own would not have been sufficient. Compaction is still done, because a retired table is one nothing will come back to tidy. Any step failing leaves the experiment in the table for the next round. Refusing to retire is always safe; retiring early is not. `STATS_COLD_RETIRE_SECS=0` disables retirement entirely. Every existing test config sets it to 0, so they exercise the unchanged path. Four tests, the important one asserting the shard is genuinely drained after retirement rather than that the function returned success. Verified it is not vacuous: removing the WAL-merge step makes it fail with exactly the stranding it exists to prevent. New: `master_stats_hot_experiments` (table size, now bounded rather than tracking the registry), `master_stats_experiments_retired_total`, `master_stats_retire_failures_total`. Co-Authored-By: Claude <noreply@anthropic.com>
beinan
force-pushed
the
feat/stats-retire-cold
branch
from
July 27, 2026 17:43
5df8dc3 to
1ad7c3f
Compare
beinan
added a commit
to beinan/lance-context
that referenced
this pull request
Jul 27, 2026
Stacked on lance-format#210. Retirement (lance-format#210) removes cold experiments from the stats table, which is what keeps a scan round proportional to active work. On its own it also removes them from `GET /experiments`, which is only acceptable once they remain findable another way. This closes that. default list the stats table, i.e. experiments written recently enough not to have been retired. A flat list of every experiment ever created is neither renderable nor interesting at tens of thousands. search the stats table, plus the registry for names it no longer holds. Cold matches are observed on demand. detail on a stats miss, resolve through the registry and observe on demand rather than returning 404. So retirement removes an experiment from the *default* view and never makes it unfindable. `observe_cold` deliberately does not write a stats row. Reading about a retired experiment must not make it hot again, or browsing the UI would silently undo retirement and the table would creep back toward holding everything -- the state retirement exists to prevent. It also takes no `MasterState`, so it structurally cannot write one; a refactor that hands it one has to justify itself. Search bounds its on-demand observations by the page size. A query matching thousands of cold experiments must not open thousands of datasets to render one page. Known gap: the two handlers changed here live in `routes.rs`, whose tests all require etcd and are `#[ignore]`d, so CI does not cover them. `observe_cold` is tested in `scanner.rs`, which CI does run. Worth fixing, but as its own change rather than a fourth concern in this one. Co-Authored-By: Claude <noreply@anthropic.com>
beinan
added a commit
to beinan/lance-context
that referenced
this pull request
Jul 27, 2026
Stacked on lance-format#210. Retirement (lance-format#210) removes cold experiments from the stats table, which is what keeps a scan round proportional to active work. On its own it also removes them from `GET /experiments`, which is only acceptable once they remain findable another way. This closes that. default list the stats table, i.e. experiments written recently enough not to have been retired. A flat list of every experiment ever created is neither renderable nor interesting at tens of thousands. search the stats table, plus the registry for names it no longer holds. Cold matches are observed on demand. detail on a stats miss, resolve through the registry and observe on demand rather than returning 404. So retirement removes an experiment from the *default* view and never makes it unfindable. `observe_cold` deliberately does not write a stats row. Reading about a retired experiment must not make it hot again, or browsing the UI would silently undo retirement and the table would creep back toward holding everything -- the state retirement exists to prevent. It also takes no `MasterState`, so it structurally cannot write one; a refactor that hands it one has to justify itself. Search bounds its on-demand observations by the page size. A query matching thousands of cold experiments must not open thousands of datasets to render one page. Known gap: the two handlers changed here live in `routes.rs`, whose tests all require etcd and are `#[ignore]`d, so CI does not cover them. `observe_cold` is tested in `scanner.rs`, which CI does run. Worth fixing, but as its own change rather than a fourth concern in this one. Co-Authored-By: Claude <noreply@anthropic.com>
beinan
added a commit
to beinan/lance-context
that referenced
this pull request
Jul 27, 2026
Stacked on lance-format#210. Retirement (lance-format#210) removes cold experiments from the stats table, which is what keeps a scan round proportional to active work. On its own it also removes them from `GET /experiments`, which is only acceptable once they remain findable another way. This closes that. default list the stats table, i.e. experiments written recently enough not to have been retired. A flat list of every experiment ever created is neither renderable nor interesting at tens of thousands. search the stats table, plus the registry for names it no longer holds. Cold matches are observed on demand. detail on a stats miss, resolve through the registry and observe on demand rather than returning 404. So retirement removes an experiment from the *default* view and never makes it unfindable. `observe_cold` deliberately does not write a stats row. Reading about a retired experiment must not make it hot again, or browsing the UI would silently undo retirement and the table would creep back toward holding everything -- the state retirement exists to prevent. It also takes no `MasterState`, so it structurally cannot write one; a refactor that hands it one has to justify itself. Search bounds its on-demand observations by the page size. A query matching thousands of cold experiments must not open thousands of datasets to render one page. Known gap: the two handlers changed here live in `routes.rs`, whose tests all require etcd and are `#[ignore]`d, so CI does not cover them. `observe_cold` is tested in `scanner.rs`, which CI does run. Worth fixing, but as its own change rather than a fourth concern in this one. Co-Authored-By: Claude <noreply@anthropic.com>
beinan
added a commit
that referenced
this pull request
Jul 27, 2026
#211)⚠️ **Stacked on #209 → #210.** Both of those commits are included here. Merge order: **#209 → #210 → this**. Once they land, this diff reduces to the UI commit alone. ## Why this exists #210 removes cold experiments from the stats table — that's what keeps a scan round proportional to *active* work. But on its own it also removes them from `GET /experiments`, which is only acceptable once they stay findable another way. **This PR is what makes retirement safe to turn on.** | | behaviour | |---|---| | **default list** | the stats table — experiments written recently enough not to have been retired | | **search** | stats table **plus the registry** for names it no longer holds; cold matches observed on demand | | **detail** | on a stats miss, resolve via registry and observe on demand rather than returning 404 | Retirement therefore removes an experiment from the *default view* and **never makes it unfindable**. The default is also the right one at scale on its own merits: a flat list of every experiment ever created is neither renderable nor interesting once there are tens of thousands. ## Two deliberate decisions **1. `observe_cold` does not write a stats row.** Reading about a retired experiment must not make it hot again — otherwise **browsing the UI would silently undo retirement**, and the table would creep back toward holding everything, which is exactly the state retirement exists to prevent. It also takes no `MasterState`, so it *structurally* cannot write one. A refactor that hands it one has to justify itself. There's a test asserting the no-rehydration property. **2. Search bounds its on-demand observations by page size.** A query matching thousands of cold experiments must not open thousands of datasets to render one page. ## Known gap — stating it rather than hiding it The two handlers changed here live in `routes.rs`, whose tests **all require etcd and are `#[ignore]`d**, so CI does not cover them. `observe_cold` is tested in `scanner.rs`, which CI does run. So the registry-fallback logic in the handlers is verified locally but not by CI. That's a pre-existing gap in the test structure, worth fixing as its own change rather than as a fourth concern here — and it's the kind of blind spot that only became visible after #202 made CI actually run integration tests. ## Testing Full workspace: 178 core + 30 master + 51 server + 5 concurrency + 1 merge-cleanup, all pass. clippy `-D warnings` and fmt clean. ## The complete picture This finishes the line of work from the original report (`/experiments` at 30–43s): | | problem | fix | |---|---|---| | **#207** | write amplification — 100 versions/round | one snapshot commit; **84× fewer versions** | | **#209** | scan cost — full observe of everything, every round | skip unchanged; cold = one open | | **#210** | table size — held every experiment ever created | retire after 7 days idle, drained first | | **this** | retirement made cold experiments invisible | active-N default + registry-backed search | | **#208** | all of the above failing silently | maintenance failure metrics | 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
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.
Why
#209 made cold experiments cheap (one open instead of a full observation). They still cost something, and the table still holds a row for every experiment that has ever existed. At tens of thousands — the overwhelming majority written once and never again — that's the remaining reason a scan round can't stay proportional to actual work.
An experiment with no writes for
STATS_COLD_RETIRE_SECS(default 7 days) is dropped from the table. It stays in the registry and can still be observed on demand; it just stops costing a row and an open every round.Retirement order is a correctness property
This is the part worth reviewing carefully.
Both auto-sweeps read the stats table and nothing else (verified — the registry references in
scheduler.rsare all test-only). So an experiment absent from it is invisible to them permanently.Dropping one that still has un-merged MemWAL generations would strand them:
_mem_wal/{shard}/directories are never reclaimedSo the order is:
compact_filesdeliberately leaves MemWAL generations alone, so compaction alone would not have been sufficient. Compaction is still done, because a retired table is one nothing will come back to tidy.Any step failing leaves the experiment in the table for the next round. Refusing to retire is always safe; retiring early is not.
Testing
Four tests. The important one asserts the shard is genuinely drained after retirement — reopening the store and checking
pending_wal_generations == 0— rather than that the function returned success.Verified non-vacuous: removing the WAL-merge step makes it fail with exactly the stranding it exists to prevent:
Full workspace: 178 core + 28 master + 51 server, all pass. clippy
-D warningsand fmt clean.Config
STATS_COLD_RETIRE_SECS=0disables retirement entirely. Every existing test config sets it to0, so they exercise the unchanged path.Metrics
master_stats_hot_experiments(table size — now bounded, rather than tracking the registry),master_stats_experiments_retired_total,master_stats_retire_failures_total.Next
C3 — UI becomes "recently active N + search", with cold experiments resolved through the registry and observed on demand. That's what makes retirement invisible to users: today a retired experiment would simply vanish from
/experiments, which is only acceptable once search covers it.🤖 Generated with Claude Code