Skip to content

Commit 87da877

Browse files
committed
fix(canonical): release the run store's freed pages through executescript so CPython 3.11 drains the freelist
1 parent 7b4104d commit 87da877

2 files changed

Lines changed: 55 additions & 8 deletions

File tree

‎codeclone/canonical/store.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4666,7 +4666,16 @@ def _release_freed_pages(store: RunStore) -> None:
46664666
connection = store._connection
46674667
if connection.in_transaction:
46684668
connection.commit()
4669-
connection.execute("PRAGMA incremental_vacuum").fetchall()
4669+
# ``executescript`` is the one form every supported CPython steps to
4670+
# completion. ``execute(...).fetchall()`` is not: CPython 3.11's
4671+
# ``sqlite3`` resets a statement that reports no result columns after its
4672+
# first step, so that form handed back exactly ONE page per call there
4673+
# (measured 2026-09-27 on 3.11.15 with SQLite 3.50.4: 375 free pages ->
4674+
# 374, while 3.10 and 3.12+ drained all 375 either way; the 3.11 CI job
4675+
# left 228 pages of this store on its freelist).
4676+
# ``tests/_sqlite_single_step.py`` reproduces that stepping on every
4677+
# interpreter, so the pin does not wait for a 3.11 runner.
4678+
connection.executescript("PRAGMA incremental_vacuum;")
46704679

46714680

46724681
def collect_garbage(store: RunStore, *, retain_history: int) -> GcJobReport:

‎tests/test_canonical_store_gc.py‎

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
GC_HOLD_STAGING,
5252
GcJobReport,
5353
)
54+
from tests._sqlite_single_step import SingleStepConnection
5455
from tests.test_canonical_roundtrip import fixture_model
5556

5657
_NS = "lineage-gc"
@@ -471,6 +472,49 @@ def _page_stats(store: RunStore) -> tuple[int, int, int]:
471472
)
472473

473474

475+
def _store_of_twelve_runs(tmp_path: Path) -> RunStore:
476+
"""Twelve published runs of growing width: a sweep that keeps one frees
477+
pages worth measuring, on a store opened with incremental auto-vacuum."""
478+
479+
store = _store(tmp_path)
480+
for index in range(12):
481+
_publish(
482+
store,
483+
_wider_model(*(f"pad-{index}-{n}" for n in range(400 + index))),
484+
expected_generation=index,
485+
)
486+
return store
487+
488+
489+
def test_the_release_drains_the_freelist_when_the_cursor_steps_the_pragma_once(
490+
tmp_path: Path,
491+
) -> None:
492+
"""The release must not depend on the cursor stepping the pragma to its end.
493+
494+
CPython 3.11's ``sqlite3`` resets a statement that reports no result
495+
columns after its first step, so ``execute("PRAGMA
496+
incremental_vacuum").fetchall()`` freed one page per call there: the
497+
3.11 job left 228 pages of this store on the freelist (CI, 2026-09-27)
498+
while 3.10 and 3.12+ were green. ``SingleStepConnection`` reproduces that
499+
stepping on every interpreter, and the sweep's release still has to leave
500+
the freelist empty through it.
501+
"""
502+
503+
store = _store_of_twelve_runs(tmp_path)
504+
store._connection = SingleStepConnection(store._connection) # type: ignore[assignment]
505+
_, pages_before, freelist_before = _page_stats(store)
506+
assert freelist_before == 0
507+
508+
collect_garbage(store, retain_history=1)
509+
510+
_, pages_after, freelist_after = _page_stats(store)
511+
assert pages_after < pages_before, "the sweep freed nothing, so nothing is proved"
512+
assert freelist_after == 0, (
513+
f"{freelist_after} freed pages were left on the freelist: the release "
514+
"stepped the pragma once instead of draining it"
515+
)
516+
517+
474518
def test_the_sweep_returns_the_pages_it_freed_to_the_file(tmp_path: Path) -> None:
475519
"""A collected run must leave the FILE, not only the tables.
476520
@@ -490,13 +534,7 @@ def test_the_sweep_returns_the_pages_it_freed_to_the_file(tmp_path: Path) -> Non
490534
sweep reports about itself.
491535
"""
492536

493-
store = _store(tmp_path)
494-
for index in range(12):
495-
_publish(
496-
store,
497-
_wider_model(*(f"pad-{index}-{n}" for n in range(400 + index))),
498-
expected_generation=index,
499-
)
537+
store = _store_of_twelve_runs(tmp_path)
500538

501539
auto_vacuum, pages_before, freelist_before = _page_stats(store)
502540
assert auto_vacuum == 2, (

0 commit comments

Comments
 (0)