Repository navigation
feat: record cache entries in a per-notebook manifest - #10838
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
All contributors have signed the CLA ✍️ ✅ |
There was a problem hiding this comment.
3 issues found across 18 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="marimo/_save/cache.py">
<violation number="1" location="marimo/_save/cache.py:93">
P2: When a custom `Store` is unhashable, manifest recording fails before any entry is recorded because `CacheState` keys records by the store object. Key manifest state by a guaranteed-hashable store identity or otherwise support unhashable `Store` implementations.</violation>
</file>
<file name="marimo/_runtime/runner/hooks_on_finish.py">
<violation number="1" location="marimo/_runtime/runner/hooks_on_finish.py:97">
P3: This hook duplicates `CacheCallbacks._flush_cache_manifest`, so future changes to manifest-flush guards or error handling can diverge between run completion and teardown. Extract the shared flush helper and call it from both lifecycle paths.</violation>
</file>
<file name="marimo/_save/manifest.py">
<violation number="1" location="marimo/_save/manifest.py:270">
P2: When two sessions flush the same notebook concurrently, the last whole-manifest write discards records from the other session. Serialize the read/merge/write operation or retry after detecting a changed manifest.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| # Cache keys this session recorded, grouped by the store they were | ||
| # written through, then by path hash and block. A manifest lists only | ||
| # the entries stored beside it. | ||
| manifest_records: dict[Store, dict[str, dict[str, set[str]]]] = field( |
There was a problem hiding this comment.
P2: When a custom Store is unhashable, manifest recording fails before any entry is recorded because CacheState keys records by the store object. Key manifest state by a guaranteed-hashable store identity or otherwise support unhashable Store implementations.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At marimo/_save/cache.py, line 93:
<comment>When a custom `Store` is unhashable, manifest recording fails before any entry is recorded because `CacheState` keys records by the store object. Key manifest state by a guaranteed-hashable store identity or otherwise support unhashable `Store` implementations.</comment>
<file context>
@@ -86,6 +87,16 @@ class CacheState:
+ # Cache keys this session recorded, grouped by the store they were
+ # written through, then by path hash and block. A manifest lists only
+ # the entries stored beside it.
+ manifest_records: dict[Store, dict[str, dict[str, set[str]]]] = field(
+ default_factory=dict
+ )
</file context>
|
|
||
| key = manifest_name(ctx.filename) | ||
| try: | ||
| manifest = load_manifest(store, key) or CacheManifest() |
There was a problem hiding this comment.
P2: When two sessions flush the same notebook concurrently, the last whole-manifest write discards records from the other session. Serialize the read/merge/write operation or retry after detecting a changed manifest.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At marimo/_save/manifest.py, line 270:
<comment>When two sessions flush the same notebook concurrently, the last whole-manifest write discards records from the other session. Serialize the read/merge/write operation or retry after detecting a changed manifest.</comment>
<file context>
@@ -0,0 +1,339 @@
+
+ key = manifest_name(ctx.filename)
+ try:
+ manifest = load_manifest(store, key) or CacheManifest()
+ except UnsupportedManifestVersionError as e:
+ # A rewrite under the older schema drops whatever the newer one
</file context>
| notebook produced, and is rewritten whole. | ||
| """ | ||
| del ctx | ||
| from marimo._runtime.context import safe_get_context |
There was a problem hiding this comment.
P3: This hook duplicates CacheCallbacks._flush_cache_manifest, so future changes to manifest-flush guards or error handling can diverge between run completion and teardown. Extract the shared flush helper and call it from both lifecycle paths.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At marimo/_runtime/runner/hooks_on_finish.py, line 97:
<comment>This hook duplicates `CacheCallbacks._flush_cache_manifest`, so future changes to manifest-flush guards or error handling can diverge between run completion and teardown. Extract the shared flush helper and call it from both lifecycle paths.</comment>
<file context>
@@ -86,7 +86,25 @@ def _send_cancellation_errors(ctx: OnFinishHookContext) -> None:
+ notebook produced, and is rewritten whole.
+ """
+ del ctx
+ from marimo._runtime.context import safe_get_context
+ from marimo._save.manifest import flush_dirty_manifests
+
</file context>
dc38c19 to
0fc5c45
Compare
0fc5c45 to
9807129
Compare
9807129 to
2c7f43e
Compare
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Unresolved critical and moderate findings affect manifest durability, consistency, pruning, and cleanup.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 4
Open (5)
What changed in this PR
Adds per-notebook cache manifests, static closure hashing, atomic file writes, interrupted-write cleanup, and hyphenated cell-ID handling.
Changes:
- Records cache hits and saves in Store-backed manifests.
- Adds manifest flushing across cache lifecycles and storage tiers.
- Adds atomic writes, partial-write handling, and regression tests.
| File | Changes and final review findings |
|---|---|
tests/_save/test_manifest.py |
Manifest tests. Nit (1 vote): Update the contradictory docstring to describe the no-rewrite behavior. |
tests/_save/test_hash.py |
Path-hash tests. No final findings. |
tests/_save/test_cache_dirs.py |
Interrupted-write stats tests. No final findings. |
tests/_ast/test_demangle_locals.py |
Hyphenated-ID demangling tests. No final findings. |
marimo/_save/stores/store.py |
Store interface documentation. No final findings. |
marimo/_save/stores/file.py |
Atomic file writes. No final findings. |
marimo/_save/save.py |
Critical (1 vote): Cache entries can be recorded before asynchronous writes complete, allowing manifests to attest missing entries. |
marimo/_save/manifest.py |
Moderate (1 vote): evict_dead can remove entries still attested by live nodes.Moderate (1 vote): Embedded-app records may not reach a flushed manifest. Critical (2 votes): Remote read failures can overwrite existing records. Critical (2 votes): Concurrent read/merge/write operations can lose manifest updates. |
marimo/_save/loaders/loader.py |
Moderate (1 vote): Zero-byte temporary files are not swept. Moderate (1 vote): Nested temporary files are missed during cleanup and counting. |
marimo/_save/hash.py |
Static closure hashing. No final findings. |
marimo/_save/cache.py |
Moderate (1 vote): Cache clearing does not invalidate session records, allowing deleted entries to be re-attested. |
marimo/_save/cache_dirs.py |
Partial-write detection and statistics. No final findings. |
marimo/_runtime/runner/hooks_on_finish.py |
Critical (2 votes): Manifest flushing can occur before background cache writes finish, preventing teardown retry from correcting the manifest. |
marimo/_runtime/executor/lifecycles/cached.py |
Moderate (1 vote): Successful ui_live cache hits are not recorded.Moderate (3 votes): Cache hits are marked as saved and unnecessarily dirty the manifest. |
marimo/_runtime/callbacks/cache.py |
Teardown flushing. No final findings. |
marimo/_ast/variables.py |
Hyphenated local-name unmangling. No final findings. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| context = safe_get_context() | ||
| if context is not None: | ||
| flush_dirty_manifests(context) |
|
|
||
| key = manifest_name(ctx.filename) | ||
| try: | ||
| manifest = load_manifest(store, key) or CacheManifest() |
| manifest.notebook = _notebook_ref(ctx.filename, store) | ||
| manifest.written_at = _utc_now() | ||
| try: | ||
| written = store.put(key, manifest.to_bytes()) |
| if self.loader.save_cache(attempt): | ||
| self._record(attempt) |
| from marimo._save.manifest import record_cache_event | ||
|
|
||
| record_cache_event( | ||
| self._loader, attempt.key, cell_id, self._graph, saved=True |
6ff05dc to
cef795f
Compare
## 📝 Summary Adds the `marimo cache` command group. This is the base of a seven-PR stack; each PR targets the one below it. 1. #10836 `marimo cache dir` (this PR): the command group and the shared PATH → cache-directory resolution. 2. #10837 `marimo cache size`: disk usage and entry count per cache directory; fills the `-1` stubs behind the cache panel. 3. #10838 Per-notebook manifest: every persistent cache hit or save is recorded under a static hash of the cell and its ancestors. 4. #10839 `marimo cache clean`: deletes a notebook's recorded entries, or wipes a directory, after confirmation. 5. #10840 `marimo cache prune`: deletes only entries that current code can no longer produce; never deletes an entry current code still records. Adds the marimo cache command group with its first subcommand, dir, which prints the cache directories a PATH resolves to. A notebook file resolves to the cache directory beside it. A directory resolves its own __marimo__/cache, as if a notebook ran in that directory; with -r/--recursive it is instead searched recursively, skipping dot-folders and logging unreadable subtrees.
cef795f to
2d7a509
Compare
Every persistent cache hit or save now records its entry in a manifest beside the cache, grouped under a path hash: a static digest of the defining cell and every cell it depends on, recomputable from source alone (exposed as BlockHasher.path_hash and hash_cell_closure). Records accumulate across runs and travel through the Store interface under a reserved key, `<notebook-stem>-<path-digest>.json`, so remote and tiered stores carry manifests too, each listing only the entries stored beside it. Flushes coalesce to one write per store per run (ON_FINISH hook, teardown retry); a store that declines the write counts as a failed flush; a save records its entry again even when the session saw the key before, since a clean can have removed it in between; recording never fails the user's cell. FileStore.put is now atomic (tmp sibling + os.replace) and interrupted-write leftovers are excluded from entry counts and swept by clear(). Also fixes unmangle_local for hyphenated cell ids (a UUID, or a nested app's prefix): a mangled _private name was previously left mangled in co_names, so kernel and load_app digests disagreed for such sessions.
Signed-off-by: Dylan Madisetti <dylan@madisetti.me>
2d7a509 to
b07a6f7
Compare


Stacked on #10837; review against
dm/cache-size.📝 Summary
Every persistent cache hit or save now attests its entry in a manifest
beside the cache, grouped under a path hash: a static digest of the
defining cell and every cell it depends on, recomputable from source
alone (exposed as BlockHasher.path_hash and hash_cell_closure). Records
accumulate across runs and travel through the Store interface under a
reserved dot key, so remote and tiered stores carry manifests too, each
attesting only the entries stored beside it. Flushes coalesce to one
write per run (ON_FINISH hook, teardown retry); recording never fails
the user's cell. FileStore.put is now atomic (tmp sibling + os.replace)
and interrupted-write leftovers are excluded from stats and swept by
clear().
Also fixes unmangle_local for hyphenated cell ids (a UUID, or a nested
app's prefix): a mangled _private name was previously left mangled in
co_names, so kernel and load_app digests disagreed for such sessions.