Bound the RO reader pool's page caches - #177
Open
prismatic7 wants to merge 1 commit into
Open
prismatic7 wants to merge 1 commit into
prismatic7 wants to merge 1 commit into
Conversation
HippoDB.__init__ caps its writer connection's page cache with PRAGMA cache_size, but the read-only pool does not go through HippoDB: it opens its slots via get_lilli_raw_conn(), so every slot was constructed with the pager's max page count unset -- unbounded. That is a retention problem, not a budget one. The pager keeps every clean page it reads, so a full-store scan through a pooled reader leaves the whole working set resident for the life of the daemon, and the pool holds several slots. Measured on the reference pager (8 KiB pages, 4,000 pages written then scanned): unbounded resident 4,003 pages 32.8 MB retained 16 MiB resident 2,048 pages 16.8 MB retained with identical rows returned either way. Apply the bound where the slot is opened, through the engine's own PRAGMA cache_size handler so the conversion from the configured budget to a page count stays in one place. IAI_MCP_RO_POOL_CACHE_KIB overrides the budget (16384 default); 0 restores unbounded behaviour.
This branch has not been deployed
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
The read-only reader pool opens its slots via
get_lilli_raw_conn(), which bypassesHippoDB.__init__and therefore itsPRAGMA cache_sizewriter bound. Every slot was constructed with the pager's max-cached-pages unset, i.e. unbounded. This applies a per-slot page-cache bound at_open_slot().Type of change
Affected areas
Testing
pytestpasses locallyruff check src/ tests/cleantests/test_ro_pool_page_cache_bound.py— 7 tests:test_bound_issues_the_cache_size_pragmaPRAGMA cache_sizetest_bound_respects_an_overrideIAI_MCP_RO_POOL_CACHE_KIBis honouredtest_bound_is_a_no_op_when_disabled0restores upstream behaviour exactlytest_a_failed_bound_never_prevents_the_slot_openingtest_slot_cache_kib_default_override_and_malformedtest_pool_open_slot_applies_the_bound_end_to_end_open_slot, not just the helpertest_scan_retention_is_capped_at_the_budgetRegression surface — 46 passed, 0 failed:
test_recall_ro_pool.py,test_ro_pool_refresh.py,test_recall_ro_routing.py,test_ro_pool_checkpoint_invalidation.py,test_lilli_page_cache_bound.py,test_col_index_reader_pool_activation.pyThe end-to-end test was checked against a revert: removing only the
_bound_slot_page_cache(conn)call from_open_slotmakes it fail withsaw [], so it guards the wiring rather than restating the helper.Note on
ruff:src/iai_mcp/hippo/_ro_pool.pyhas 13 pre-existingruff checkerrors on pristinemain(mostlyUP037on quoted self-referential annotations). This change adds zero new ones — the count is 13 before and after. The new test file is clean. I did not opportunistically fix the pre-existing 13, since that would widen the diff; happy to do it as a separate PR if wanted.Benchmarks
No
bench.*re-run: this changes footprint, not retrieval.rows_scannedand returned rows are identical with and without the bound, so there is no retrieval-result delta to report.The measured effect is on retained pages, which is where the cost lives — the pager keeps every clean page it reads, and the pool holds several slots. Reference pager,
page_size=8192, 4,000 pages written then fully scanned:I want to be explicit about the limits of my evidence, because the fork this was distilled from claims "+368 MB → +6 MB" and I could not reproduce that. Process-level RSS cannot isolate this change: at 20,000 records I measured +443.5 MB unbounded vs +435.3 MB bounded, and a 16 MiB budget cannot produce an 8 MB delta. Page-cache retention is held engine-side in Rust and is invisible to both
tracemallocand whole-process RSS. So the quoted number is the pager's own resident-page count — the mechanism, measured directly — not a process-level figure I could not attribute. Effect scales with page count and withIAI_MCP_RO_POOL_SIZE, which is why the default is a parameter rather than a constant.Notes for reviewers
_refresh_or_reopen_slot()prefers the engine's in-place snapshot refresh, which does not recreate the connection, and falls back to_open_slot(). Bounding in_open_slot()survives both paths; bounding in_refresh_or_reopen_slot()would not.RO_POOL_SIZE * 16 MiB= 128 MiB at the default pool size of 8.IAI_MCP_RO_POOL_CACHE_KIB=0restores unbounded. If you'd prefer a different default or a pool-wide budget rather than per-slot, that's a one-line change and I'd rather take your preference than guess.maintenance.pyalready does exactly this shape with_MAINTENANCE_SCAN_CACHE_KIBand its_bounded_scan_cachecontext manager; this is the samePRAGMA cache_size=-{kib}, applied at slot open rather than around a scan. The negative-KiB → page-count conversion stays in the engine (crates/lilliengine/src/conn.rs, ~2104) so there is one place that knows the page size.PRAGMA cache_sizehas no read-back on the engine, so assertions are made against the issued PRAGMA and the pager's retention rather than by querying the pragma back.storage corruption: checksum mismatch on page 1— the two page formats differ, including a per-page checksum. The retention test therefore uses the reference pager on its own, and the slot test uses a recording stand-in.