Repository navigation
Give each disk-backed holder storage its own directory - #558
Merged
Merged
Conversation
A holder created lazily in a branch made an OnDiskStorage that owned the family's shared <data_storage_dir>/<variable> directory, so garbage-collecting the branch deleted the parent's files. The shared directory also let a Simulation.clone() or a same-named branch under another parent overwrite the parent's .npy files, and let a parent's later write change what its branch read. Each storage now writes only into a directory of its own (created on first write inside the simulation's data_storage_dir), a clone keeps alive every directory it reads from, and a file a clone reads is never overwritten. Directories are removed by finalizers on reference-counted StorageDirectory handles, only in the process that created them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ression test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Simulation.derivative perturbs a clone's input; with the shared directory it overwrote the simulation's own input file and returned 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ypothesis The country-package smoke job installs no dev dependencies, so a module-level hypothesis import failed its collection. Shared helpers move to tests/fixtures/disk_storage.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 2, 2026
…branch-ownership # Conflicts: # policyengine_core/simulations/simulation.py
Another storage, possibly in another process, may have restored the same directory and read those files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 3.14 Python 3.14 removed pickle/copy support from itertools objects, so itertools.count made copy.deepcopy and pickle of an OnDiskStorage raise. Found by the independent review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…arents Round-3 review (REQUEST_CHANGES) found that a preserved storage inside the simulation's directory was removed with it, and that writing after restore() could overwrite an older clone's file or make a later restore read a stale value. The earlier attack script also showed permanent "shared" marks made every re-store after any clone (such as derivative's) add a file. - StorageDirectory keeps, per file, a WeakSet of the storages that may read it besides its creator (clones, copies, restorers); one handle per path per process. - A storage overwrites only the newest file it created itself, since the last fork, that no live reader may read (and, outside its own temporary directory, that is still the key's highest-numbered file). Otherwise it creates a new file with O_EXCL, numbered above every file for the key. - Preserving a directory preserves its parents; create_disk_storage(preserve= True) inside data_storage_dir takes the simulation's handle as parent. - copy/deepcopy return the same directory handle and pickles find the live one, so copies keep the directory; copies never overwrite the original's files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 2, 2026
Preserve the recovered #585-based storage repair and the current master release workflow without rewriting either history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep the #594 release version and changelog alongside the unchanged disk storage repair. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
US + core hub merge audit, core#558 at
|
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.
Problem and change
After adopting #585, independently recreated holders for the same variable still used the same temporary subdirectory. Collecting an old holder could remove files needed by its replacement or a clone. A missing caller-supplied storage directory also failed on first use, and overwriting an enum with a plain array retained stale enum metadata.
This PR's runtime changes relative to its merged master base are:
Holder.create_disk_storage()usesmkdtemp(prefix=f"{variable}_")for each independently created temporary storage. Explicit directory arguments andpreserve=Trueretain thedirectory/variabledump layout.Simulation.data_storage_dircreates missing caller-supplied directories and inherited parent directories on first use.OnDiskStorage.put()removes enum metadata when the replacement is a plain array.The implementation retains #585's
TemporaryStorageDirectoryownership and clone/copy lifetime model. Cloned storages share the source directory and copy their mappings. Writes to shared files usemkstempinreplaced/; later writes reuse the writer's own unshared replacement.restore()reads direct key files only, ignoresreplaced/, and does not select the latest write across independently constructed storages. Independent writers using the same explicit directory have no snapshot-isolation guarantee. Files restored after cloning are also outside that guarantee; clone or copy after restoring to protect the restored values.The API page describes these contracts and warns that exits skipping finalizers, such as
os._exitin multiprocessing workers, can leave uniquely named holder subdirectories accumulating in a caller-owned directory.Invariants and regression coverage
Existing property and differential coverage is preserved. No load-bearing runtime logic changed in this review follow-up.
test_disk_storage_ownership.pycontains 26 test functions and 33 parametrized cases. Most cover contracts inherited from #585; no claim is made that every case fails on the base. This review's untracked description, validation-provenance and cleanup-documentation probes failed on the saved original body/page and pass on the corrected versions. These are documentation/evidence regressions; the review identified no runtime P1/P2 defect.Validation
Local results below are from
c616364cd65d8a79ef10bbe275e94b91ef6167b4on macOS with Python 3.13.9. Runtime and test files are identical to reviewed head85c3e787be26e38966970dae2ba60362fbe26b30; the follow-up commit adds only the cleanup sentence. Dependency setup in a new environment was interrupted before tests ran; local runs use an existing read-only environment with Hypothesis 6.141.1 in a separate overlay, rather than claiming a fresh lockfile installation. Both property modules run without skipping.Recorded local dependency versions: NumPy 2.5.3, pytest 9.1.1, h5py 3.16.0 and numexpr 2.14.2.
Each file ran separately in the foreground using
uv run --python 3.13 --active --frozen --no-sync --with hypothesis==6.141.1 python -m pytest -q tests/core/<file> -W error::pytest.PytestUnraisableExceptionWarning. The environment selects the current checkout throughPYTHONPATHand disables bytecode writes to keep external dependencies read-only.test_disk_storage_ownership.pytest_disk_storage_differential.pytest_data_storage_dir.pytest_data_storage_dir_property.pytest_on_disk_storage_clones.pytest_branch_scoped_delete_arrays.pytest_dump_restore.pytest_holders.pyTotal: 131 passed, 2 expected xfails.
The two existing strict xfails in
test_data_storage_dir.pydocument a forked child reading after its parent removes the inherited directory. They remain unchanged.For the reviewed head, CI run 37847856790 has 19 successful checks, including core tests and country-package smoke checks on Linux and Windows with Python 3.11–3.14, lint, and changelog validation. CI checked out synthetic merge
501a2c27379612f5b123754b8a9b1470af312309, containing the reviewed head; it did not test the PR-only checkout. Its Ubuntu Python 3.13 core job reported 1791 passed, 4 skipped and 3 xfailed, plus 39 country-template cases; package and documentation builds also succeeded. These CI results precede the documentation follow-up and are recorded separately from the local results.Full suites, Windows tests, country-package tests, package builds and
make documentationwere not rerun locally.ruff format --checkandruff checkon the 10 Python files in the PR diff, plusgit diff --check, passed.Impact
Measured: focused synthetic storage/holder/dump tests, disk-versus-memory differential comparisons, directory-lifetime properties, and the existing CI checks described above. This is core disk-storage infrastructure used through
MemoryConfig, with no policy formula change.Microsimulations were not run. Country model-output differences, aggregate fiscal effects, partner baseline differences, performance and disk-space impact were not measured. CI's country smoke checks are initialization checks; microsimulation checks were skipped. The hub owns US/UK microsimulation and partner-impact measurement. No partner test files were edited.
Overlap, ordering and HOLD
OnDiskStorage.deleteand key parsing. Underscore branch names and contained-period deletion remain outside this PR's scope; the differential uses branch names without underscores and whole-variable deletion.:branch names differing between memory and disk, and deleted mappings leaving files until directory cleanup.Documentation review: durable storage contracts match the implementation; the review follow-up adds the caller-directory cleanup caveat. Impact is low and confidence is high for the documented storage behavior; cross-process lifetime limits and unmeasured country impact remain explicit.