feat(ITL-508): indexed entry_id versioning for concurrency-safe miss-triggered recomputation#218
Conversation
Covers schema changes (base_entry_id + recomputation_index columns), entry ID computation redesign, Phase 1/2 flow updates, single shared index chain, asyncio atomicity guarantee, and test plan. Files ITL-515 as follow-up for multi-process / thread-safe insert_if_not_exists. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ne_entry_id Adds two new module-level constants (_PIPELINE_BASE_ENTRY_ID_COL, _PIPELINE_RECOMPUTATION_INDEX_COL) and two methods on FunctionJobNode: - compute_base_entry_id: stable hash without recomputation index (pre-ITL-508 semantics) - compute_pipeline_entry_id: now accepts recomputation_index (default 0) so each recomputation attempt gets a distinct DB primary key Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e helper Corrects the inaccurate constant comment and docstring for compute_base_entry_id that referenced future wiring not yet implemented, and eliminates code duplication between compute_base_entry_id and compute_pipeline_entry_id by extracting a shared _build_entry_id_preimage helper method. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… remove skip_cache_lookup Replaces the old skip_cache_lookup-based deduplication guard with an indexed versioning strategy. add_pipeline_record now computes the next recomputation index by querying existing rows sharing the same base_entry_id and writes at max_index+1 with skip_duplicates=True for concurrency safety. Removes skip_cache_lookup from the method signature and from PodNodeProtocol. Replaces TestAddPipelineRecordDeduplication with TestAddPipelineRecordIndexed (4 tests, all passing).
…ecord callers Remove the four call sites in _process_data_internal and _async_process_data_internal (ephemeral and persistent branches) that still passed skip_cache_lookup=True after Task 2 removed that parameter from add_pipeline_record, causing TypeError at runtime. Also update test_execute_data_second_call_same_input_deduplicates to reflect the ITL-508 recomputation-index design: two calls for the same logical input now write two versioned pipeline records (index 0 and 1) rather than deduplicating to one. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ocol stub with impl Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…data_internal Switches the in-memory cache key in _process_data_internal and _async_process_data_internal from compute_pipeline_entry_id (versioned) to compute_base_entry_id (stable). The versioned entry ID changes with each recomputation attempt; using it as a cache key would cause cache misses on subsequent recomputation cycles for the same logical input. base_entry_id is stable across all recomputation attempts, enabling proper cache hits during Phase 1 lookups even after miss-triggered Phase 2 rewrites. Also updates Phase 1 cache lookup in execute() and async_execute() to use base_entry_id for consistency. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nc_execute - Fix Fix 1: Update docstring for _process_data_internal to reference base_entry_id instead of stale entry_id - Fix Fix 2: Rename cached_by_entry_id dict and entry_id variable to cached_by_base_entry_id and base_entry_id respectively for consistency with the actual method names used Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…h/load/get_cached - Rename `get_cached_results` parameter from `entry_ids` to `base_entry_ids` - Rename `_fetch_joined_records` parameter from `entry_ids` to `base_entry_ids`; update anti-join and filter to use `_PIPELINE_BASE_ENTRY_ID_COL` - Rename `_load_cached_entries` parameter from `entry_ids` to `base_entry_ids`; update internal `_fetch_joined_records` call and dict key extraction - Update `get_all_records` to also drop `_PIPELINE_BASE_ENTRY_ID_COL` and `_PIPELINE_RECOMPUTATION_INDEX_COL` from user-facing output - Update `execute` caller of `get_cached_results` to use `base_entry_ids=` kwarg - Update test files: `test_function_node_fetch_joined.py` and `test_function_node_get_cached.py` to use `base_entry_id` and `compute_base_entry_id`
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…ion-indexed-entry_id-versioning
There was a problem hiding this comment.
Pull request overview
This PR implements ITL-508’s “base entry id + recomputation index” scheme to make miss-triggered recomputation concurrency-safe by separating a stable base_entry_id (used for Phase 1 filtering + in-memory cache keys) from a versioned entry_id (used as the pipeline DB primary key).
Changes:
- Add
compute_base_entry_id()and extendcompute_pipeline_entry_id(..., recomputation_index=0); re-key Phase 1/2 caching internals to usebase_entry_id. - Redesign
add_pipeline_record()to append versioned pipeline rows atmax(recomputation_index)+1, removingskip_cache_lookup. - Update protocols and tests to reflect the new ID model, filter semantics, and recomputation behavior (including new concurrency-focused tests/spec docs).
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/orcapod/core/nodes/function_node.py | Core implementation: new ID methods/columns, redesigned add_pipeline_record, Phase 1/2 re-keying to base_entry_id. |
| src/orcapod/protocols/pipeline_protocols.py | Update add_pipeline_record protocol signature (remove skip_cache_lookup, add computed / is_ephemeral, use uuid.UUID). |
| tests/test_core/nodes/test_function_node_get_cached.py | Update get_cached_results tests to use base_entry_id keys. |
| tests/test_core/nodes/test_function_node_fetch_joined.py | Update _fetch_joined_records tests to filter by base_entry_ids. |
| tests/test_core/function_pod/test_function_pod_node.py | Update expectations: repeated same input appends recomputation-indexed rows instead of deduplicating. |
| tests/test_core/function_pod/test_function_node_caching.py | Add tests for compute_base_entry_id and index-sensitive versioned IDs. |
| tests/test_core/function_pod/test_ephemeral_result.py | Replace dedup tests with indexed-write tests; add concurrent Phase 2 serialization tests. |
| superpowers/specs/2026-07-08-itl-508-indexed-entry-id-versioning-design.md | New design spec documenting the ITL-508 approach and testing plan. |
| superpowers/plans/2026-07-08-itl-508-indexed-entry-id-versioning.md | New implementation plan capturing the intended task breakdown and TDD steps. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Determine the next recomputation index by querying all existing rows | ||
| # for this base_entry_id. No await is used here, so within a single-threaded | ||
| # asyncio event loop this read-then-write sequence is uninterrupted. | ||
| existing = self._pipeline_database.get_records_with_column_value( | ||
| self.node_identity_path, | ||
| {_PIPELINE_BASE_ENTRY_ID_COL: base_entry_id}, | ||
| ) |
There was a problem hiding this comment.
Added a schema guard at the top of add_pipeline_record that calls get_all_records for the node path and checks whether the returned table (if non-empty) contains both _PIPELINE_BASE_ENTRY_ID_COL and _PIPELINE_RECOMPUTATION_INDEX_COL. If either column is absent, a ValueError is raised immediately with a clear message identifying the missing columns and instructing the user to clear or migrate the pipeline database. This fires before the get_records_with_column_value call, so the user gets an actionable error instead of a cryptic Arrow filter exception.
| ### Concurrent-miss serialisation (asyncio) | ||
| - Two asyncio tasks both observe a Phase 1 miss for the same (tag, data). | ||
| - Both enter Phase 2 concurrently. | ||
| - After both complete, pipeline DB contains exactly **one** row at | ||
| `recomputation_index=1` (no duplicate). | ||
| - `call_count` reflects two computations (both ran) but only one pipeline | ||
| record was written. |
There was a problem hiding this comment.
Updated the design spec's "Concurrent-miss serialisation (asyncio)" section to reflect actual semantics. The section now explains that add_pipeline_record is fully synchronous (no await), so asyncio cooperative scheduling serialises the two writes: each coroutine reads the current max_index before the other writes, so both rows land (indices 0 and 1). The assertion is documented as >= 1 — the correct lower bound — with a note that skip_duplicates=True only de-dupes when two coroutines accidentally compute the identical versioned entry ID (impossible when max_index advances between reads). The test code already used >= 1; only the spec was wrong.
| def test_entry_ids_filter_narrows_rows(self, executed_node): | ||
| """Passing a single entry_id returns only that row.""" | ||
| """Passing a single base_entry_id returns only that row.""" | ||
| node = executed_node |
There was a problem hiding this comment.
Renamed to test_no_base_entry_ids_returns_all_rows and updated the docstring to say base_entry_ids=None instead of entry_ids=None.
| def test_empty_entry_ids_returns_zero_rows(self, executed_node): | ||
| """Passing entry_ids=[] returns a 0-row table, not None.""" | ||
| result = executed_node._fetch_joined_records(entry_ids=[]) | ||
| result = executed_node._fetch_joined_records(base_entry_ids=[]) |
There was a problem hiding this comment.
Renamed to test_empty_base_entry_ids_returns_zero_rows and updated the docstring to say base_entry_ids=[] instead of entry_ids=[].
…, spec update - add_pipeline_record: fail fast with a clear ValueError when the pipeline DB contains pre-ITL-508 records missing _PIPELINE_BASE_ENTRY_ID_COL / _PIPELINE_RECOMPUTATION_INDEX_COL, instead of crashing with a cryptic Arrow error - test_function_node_fetch_joined: rename test_no_entry_ids_returns_all_rows → test_no_base_entry_ids_returns_all_rows and test_empty_entry_ids_returns_zero_rows → test_empty_base_entry_ids_returns_zero_rows; update docstrings to match - design spec: correct concurrent-miss testing plan — actual asyncio semantics yield >= 1 rows (not exactly one), explain why Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Review round addressedAll four Copilot comments resolved in commit f7bad25:
Full test suite: 4212 passed, 0 failures after all changes. |
Review round addressedAll four Copilot comments resolved in commit f7bad25:
Full test suite: 4212 passed, 0 failures after all changes. |
Summary
compute_base_entry_id(stable, index-free hash) as the in-memory cache key and Phase 1 filter column (_PIPELINE_BASE_ENTRY_ID_COL), separate from the versioned DB primary key produced bycompute_pipeline_entry_id(tag, data, recomputation_index).add_pipeline_recordto read the max existingrecomputation_indexfor a givenbase_entry_idand write atmax+1withskip_duplicates=True, making concurrent asyncio coroutines naturally serialise without duplicate pipeline records.get_cached_results,_fetch_joined_records,_load_cached_entries) and Phase 2 cache writes to usebase_entry_id, so stale versioned rows drop out of the inner join automatically while valid results are found via the stable key.Test plan
TestComputeBaseEntryId— stability and uniqueness of the index-free hashTestVersionedEntryIdDiffersByIndex— determinism and index sensitivity of versioned hashTestAddPipelineRecordIndexed— first call writes at index 0, second at index 1,_PIPELINE_BASE_ENTRY_ID_COLwritten,skip_cache_lookupremovedTestConcurrentMissSerialization— two asyncioTaskGrouptasks for the same input produce valid pipeline records; session 2 does not recomputeTestPersistentMissWarning::test_recompute_after_persistent_miss_appends_new_pipeline_record— miss-triggered Phase 2 write at index 1 resolves the stale index 0 entryTestEphemeralWritePath::test_recompute_after_ephemeral_miss_no_infinite_cycle— ephemeral miss does not loopLinear
Fixes ITL-508
ITL-507
ITL-515