feat(config): add missing_cache_policy to NodeConfig (ITL-604) - #248
feat(config): add missing_cache_policy to NodeConfig (ITL-604)#248kurodo3[bot] wants to merge 18 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a new NodeConfig.missing_cache_policy setting to unify and control how FunctionJobNode handles “pipeline record exists, but result-store entry is missing” scenarios across persistent and ephemeral stores, including sync and async execution paths.
Changes:
- Adds
NodeConfig.missing_cache_policy: Literal["recompute", "as_empty", "strict"] | None(plus merge semantics and docstring guidance). - Adds
CacheMissErrorand updatesFunctionJobNodeto enforce the policy in_fetch_joined_records(),execute(), andasync_execute()(including INFO logging for ephemeral misses and aligned async routing behavior). - Adds a comprehensive new test suite for policy × store-type × mode combinations, plus design spec and implementation plan artifacts under
superpowers/.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/orcapod/types.py |
Adds missing_cache_policy to NodeConfig, documents semantics, and updates merge(). |
src/orcapod/errors.py |
Introduces CacheMissError for strict persistent-miss handling. |
src/orcapod/core/nodes/function_node.py |
Implements policy branching for persistent misses, emits/handles EmptyData per policy, and aligns async routing with sync behavior. |
tests/test_core/function_pod/test_missing_cache_policy.py |
Adds end-to-end and matrix tests for the new policy behaviors (sync, async, and cache-only). |
superpowers/specs/2026-08-06-itl-604-missing-cache-policy-design.md |
Documents intended policy semantics and behavior matrix for ITL-604. |
superpowers/plans/2026-08-06-itl-604-missing-cache-policy.md |
Provides an implementation plan and verification checklist for ITL-604. |
Suppressed comments (2)
tests/test_core/function_pod/test_missing_cache_policy.py:174
- This test duplicates the DB-wipe logic inline. Using the shared
_wipe_result_db()helper here keeps the wipe semantics consistent across tests (including_pending_record_ids) and reduces brittleness ifInMemoryArrowDatabaseinternals change.
ephemeral_db._tables.clear()
ephemeral_db._pending_batches.clear()
tests/test_core/function_pod/test_missing_cache_policy.py:185
- This test currently only asserts the length and includes a misleading “# recomputed” comment. Under the new
execute()logic, anEmptyDatacache entry is only treated as a recompute sentinel whenmissing_cache_policyis"recompute"; for"strict"(ephemeral miss) the expected behavior is to emitEmptyData(no recompute). Adding an explicit assertion here will prevent regressions and clarify intent.
assert len(results) == 1 # recomputed
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Review round responseSingle commit
The All 24 tests in |
| @@ -1260,39 +1260,45 @@ def execute( | |||
| self.get_cached_results(base_entry_ids=base_entry_ids) | |||
There was a problem hiding this comment.
the method name is very confusion/misleading -- we should call it something like load_cached_results
There was a problem hiding this comment.
Done — renamed get_cached_results to load_cached_results throughout function_node.py (method definition, call site, and docstring references). The new name makes the intent clear: the method loads DB records into the in-memory cache as a side effect and returns those results. The test file test_function_node_get_cached.py was updated accordingly (class and method names, module docstring).
There was a problem hiding this comment.
consider making buffer size configurable as an argument into the async execute?
There was a problem hiding this comment.
Done — added a channel_buffer_size: int = 16 keyword argument to async_execute(). The value is forwarded to both the compute_channel and result_channel constructors, replacing the previous hardcoded 16. The default is preserved so existing callers are unaffected. The docstring documents the parameter with guidance on when to tune it (higher for throughput, lower to tighten backpressure).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/orcapod/types.py:388
NodeConfig.missing_cache_policyis typed as aLiteral[...], but there’s no runtime validation. Because the code branches only on"strict"/"as_empty"and otherwise treats the value as a fallback, an invalid non-empty string can lead to inconsistent behavior (e.g.,_fetch_joined_records()falls back to recompute semantics, whileexecute()/async_execute()would treatEmptyDataas an opportunistic cache hit). Add a__post_init__guard to fail fast on invalid values.
is_result_ephemeral: bool | None = None
ignore_schema: tuple[str, ...] | None = None
missing_cache_policy: Literal["recompute", "as_empty", "strict"] | None = None
src/orcapod/core/nodes/function_node.py:2000
_fetch_joined_records(base_entry_ids=...)computes miss counts / raisesCacheMissError/ populatesEmptyDatatokens using the fullpersistent_taginfo_df/ephemeral_taginfo_dfsets, and only later applies thebase_entry_idsfilter tomerged_df. Withmissing_cache_policy="strict", this can raise due to missing entries that are outside the requestedbase_entry_ids, and in"as_empty"it can populateEmptyDatatokens for unrelated entries (cache pollution). The miss handling should be scoped tobase_entry_idsby filteringtaginfo_df(or the persistent/ephemeral taginfo DFs) up front before joins/token creation whenbase_entry_idsis provided.
policy = self._node_config.missing_cache_policy or "recompute"
results_schema = None
persistent_df = pl.DataFrame()
empty_data_tokens: dict[bytes, EmptyData] = {}
empty_taginfo_rows: dict[bytes, dict] = {}
if persistent_taginfo_df.height > 0:
Review round 2 — changes addressedTwo comments from @eywalker resolved in commit
All 4 646 tests pass. |
Design document for the unified missing_cache_policy NodeConfig field, CacheMissError exception, and per-policy behaviour for ephemeral and non-ephemeral result-store misses. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…or (ITL-604) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r (ITL-604) Add policy-aware handling of non-ephemeral cache misses in _fetch_joined_records(): when missing_cache_policy="strict", log ERROR and raise CacheMissError instead of warning and recomputing. Extract shared _populate_empty_data_tokens() helper used by both the ephemeral miss path and the as_empty persistent miss path.
…nt, complete wipe helper (ITL-604) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…oined_records (ITL-604) - Append TestAsEmptyPolicy and TestEphemeralInfoLog test classes to test_missing_cache_policy.py; as_empty tests are expected to fail until Task 4 wires EmptyData through execute(). - Add INFO-level log for ephemeral miss path in _fetch_joined_records() so cross-session ephemeral misses are visible at INFO (not WARNING). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…che_policy (ITL-604) In recompute mode, route_inputs now sends EmptyData cache hits to the compute channel for recomputation instead of forwarding them directly to output, matching the behaviour of the sync execute() path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add TestAsEmptyEndToEnd with two tests: - ITL-605 boundary: downstream node raises EphemeralResultMissingError when it receives EmptyData but has no cached result for that hash. - Opportunistic propagation: downstream node B serves its cached result (60) when upstream node A emits EmptyData carrying A's output hash, which equals B's input hash — verifying the _process_data_internal -> lookup_cached_data flow-through works end-to-end. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…TL-604) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… assertion (ITL-604)
…s, configurable async buffer - Rename `get_cached_results` to `load_cached_results` for clarity; the name now reflects that the method loads DB results into the in-memory cache rather than returning a generic "get" result. - Add `channel_buffer_size: int = 16` keyword argument to `async_execute()` so callers can tune the backpressure buffer capacity of the internal compute/result channels. - Update all call sites, docstrings, and test file accordingly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…meral, channel_buffer_size Close the gaps identified in post-review audit: - TestAsEmptyPolicy: add Branch B (partial gap) test — result DB has some rows but one entry is absent; assert EmptyData for the missing row and real data for the present row, with no recomputation. - TestAsyncExecutePolicy: add three new tests * as_empty + non-ephemeral Branch A (complete DB wipe) via async_execute * as_empty + non-ephemeral Branch B (partial gap) via async_execute * strict + ephemeral miss via async_execute — must emit EmptyData, not raise - TestChannelBufferSize: new class with two tests (buffer_size=4 and =1) confirming the new channel_buffer_size kwarg is accepted and produces correct results under tighter backpressure. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
b758f5e to
a09aa9e
Compare
…opt-in-permissive-mode-for
…opt-in-permissive-mode-for
| # Use the shared helper — same EmptyData creation logic for | ||
| # ephemeral misses and permissive persistent misses. | ||
| self._populate_empty_data_tokens( | ||
| unmatched_df, empty_data_tokens, empty_taginfo_rows | ||
| ) |
There was a problem hiding this comment.
Good catch — confirmed the bug. The root cause was that empty_data_tokens / empty_taginfo_rows were populated for all unmatched rows before the base_entry_ids filter ran, and the early-return else branch (no rows in either store) exited before that filter was reached.
Fix (commit 4a01dc3b): Moved the token-dict filter to immediately before the merge block, so every return path — including the early-return no-rows case — scopes the dicts identically to the merged_df filter applied after merging.
Regression test added (TestAsEmptyPolicy.test_as_empty_load_cached_results_scoped_to_requested_ids): calls load_cached_results(base_entry_ids=[eid0]) on a node with two pipeline entries both missing from the result store (as_empty policy), and asserts that only eid0's EmptyData appears in _cached_output_datas — the test failed before the fix and passes after.
…merge ``_fetch_joined_records`` populated ``empty_data_tokens`` / ``empty_taginfo_rows`` for all unmatched rows before applying the ``base_entry_ids`` filter. The filter was only applied to ``merged_df`` at the end of the function, and the early-return ``else`` branch (no matched rows in either store) exited before that filter ran. This caused ``load_cached_results(base_entry_ids=[x])`` to cache EmptyData entries for IDs outside [x], polluting ``_cached_output_datas``. Fix: filter the token dicts immediately after populating them, before the merge block, so every return path — including the early-return no-rows case — scopes the dicts identically to the ``merged_df`` filter. Add regression test in ``TestAsEmptyPolicy`` that calls ``load_cached_results(base_entry_ids=[eid0])`` when two pipeline entries both have no result (as_empty policy) and asserts that only eid0's EmptyData is cached afterwards. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Review round 3 — changes addressedOne comment from Copilot resolved in commit
31/31 policy tests pass. |
Summary
Adds
missing_cache_policy: Literal["recompute", "as_empty", "strict"] | NonetoNodeConfig, giving callers unified control over howFunctionJobNodehandles a cache miss — where the pipeline table has a record for an input but the result store does not."recompute"(default) — WARNING logged; existing behaviour preserved exactly.EmptyDatatokens act as recompute sentinels in bothexecute()andasync_execute()."as_empty"— WARNING logged (non-ephemeral) or INFO logged (ephemeral);EmptyDatais emitted directly to the output, allowing downstream nodes to serve from their own cache opportunistically."strict"— ERROR logged andCacheMissErrorraised for non-ephemeral misses. Ephemeral misses always degrade gracefully toEmptyDataregardless of policy.Ephemeral misses now always log at INFO level (previously silent).
async_execute().route_inputsis aligned withexecute()so EmptyData sentinels in"recompute"mode trigger recomputation rather than being silently forwarded.Closes ITL-604
Changes
src/orcapod/errors.py— newCacheMissErrorexceptionsrc/orcapod/types.py—NodeConfig.missing_cache_policyfield +merge()update + docstringsrc/orcapod/core/nodes/function_node.py:_populate_empty_data_tokens()shared helper (eliminates duplication between ephemeral and permissive persistent miss paths)_fetch_joined_records()— three-way policy branch for both Branch A (result DB entirely absent) and Branch B (partial gap); ephemeral INFO logexecute()— EmptyData sentinel check respects policyasync_execute().route_inputs— aligned withexecute()EmptyData handlingtests/test_core/function_pod/test_missing_cache_policy.py— 24 new tests covering all policy × store-type × mode combinationssuperpowers/specs/andsuperpowers/plans/— design spec and implementation planTest plan
TestNodeConfigMissingCachePolicy— field, merge semantics, importabilityTestStrictPolicy— raisesCacheMissErrorfor non-ephemeral Branch A & B; no-op when all present; ephemeral miss does not raiseTestAsEmptyPolicy— EmptyData emitted, function not called; WARNING logged; policy honoured across multiple callsTestEphemeralInfoLog— INFO log for ephemeral miss; no WARNINGTestAsyncExecutePolicy— recompute mode recomputes; as_empty mode forwards EmptyData; strict raises via async pathTestCacheOnlyPolicy— strict raises, as_empty forwards EmptyData, recompute silently omitsTestAsEmptyEndToEnd— downstream serves from cache;EphemeralResultMissingErrorraised when downstream has no cache (ITL-605 boundary)TestRecomputeRegression— default behaviour unchangedFull suite: 4655 passed, 93 skipped, 6 xfailed.
🤖 Generated with Claude Code