Milestone: Configurable TTL Cascade - #283
Conversation
…ble TTL cascade across ForeignKey graphs (milestone, phases 1-4)" This reverts commit 48e4adc.
…): CR-01 replay EVALSHA-only on NOSCRIPT, converge recovery paths execute_pipeline_with_noscript_recovery replayed the FULL command stack on NOSCRIPT. In a transactional MULTI/EXEC a NOSCRIPT surfaces at EXEC time after the non-EVALSHA commands already committed (Redis does not roll back a transaction on a mid-execution command error), so a full replay double-applies non-idempotent native ops (JSON.NUMINCRBY, JSON.ARRAPPEND, SF ops). This affected every write routed through ensure_pipeline/pipeline_with_execution, not just cascade, and only on a script-cache flush/failover. - context.py: replay only the EVALSHA entries (matching the already-correct _apipeline pattern); add an ignore_redis_error param so the two write paths share one recovery implementation (WR-03, IN-01). - base.py: _apipeline now delegates to the shared helper; drop the duplicated backup/replay block. Also document the non-positive-ttl root-delete asymmetry in aset_ttl(cascade=True) (IN-04) and restore valid indentation on contains_sf_field (stray uncommitted corruption). - test_context.py: rewrite the full-stack-replay test to assert EVALSHA-only replay (it had locked in the buggy behavior). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…): make Meta.ttl/cascade_ttl freeze lifecycle robust - init.py (WR-01): wrap the unfreeze -> configure -> bake -> refreeze sequence in try/finally so a failed init (e.g. validate_cascade_ttl_targets on a mis-configured graph) still refreezes every model instead of leaving them unfrozen with silently-mutable, half-baked Meta.ttl/cascade_ttl. - init.py (WR-02): teardown_rapyer now clears _ttl_frozen so a torn-down model doesn't leak MetaTtlFrozenError into a later init-less path. - config.py (WR-04): freeze cascade_ttl too, not just ttl — both are baked into the per-class Lua plan / _has_cascade gate, so mutating cascade_ttl post-freeze would silently desync the runtime cascade from the baked plan. - cascade/conftest.py: autouse fixture resets _ttl_frozen around each cascade test so the process-global freeze can't make tests order-dependent (WR-02). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…): harden lua-literal escaping and correct dead-branch comment - registry.py (IN-03): _lua_literal now also escapes newlines/CR so a stray control char in an injected literal yields valid Lua at SCRIPT LOAD instead of a silently broken script body. - apply.lua (IN-02): correct the recurse=false comment — the branch is a not-yet-exercised seam (every emitted edge has recurse=true), and a non-recursing target can still be reached via its override edges since next_hop ignores budget for overrides. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…): freeze the whole Meta config after init, not just ttl Per review: the freeze should cover all config, not only ttl. Rename the _ttl_frozen flag to _frozen, block every public Meta field in __setattr__ once frozen (private attrs stay writable so init/teardown can toggle it), and rename MetaTtlFrozenError -> MetaFrozenError. A global autouse test fixture unfreezes models around each test so the process-global freeze can't leak across tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…): refresh_ttl always uses the cascade script Per review: drop the _has_cascade branch in refresh_ttl and always run the cascade EVALSHA. With no outgoing edges it simply re-arms the model's own keys, so every TTL refresh goes through one path. Tests updated to assert the script call instead of the per-key EXPIRE loop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…scade): planner dataclasses, contains_sf_field, shorter docstrings Per review, in rapyer/cascade/planner.py: - _classify_edge returns an EdgeClassification dataclass instead of a tuple. - _static_walk_special_suffixes uses contains_sf_field() instead of a manual hasattr(_special_field_names) check. - multi-line docstrings start on their own line and are trimmed to short summaries; drop -> None return annotations. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…de): remove internal workflow notation from comments Per review: drop the internal review/plan tags (D-0x, WR-0x, IN-0x, etc.) and RESEARCH.md/Pitfall references from comments and docstrings across the cascade code and tests; keep the actual explanations. Also reword multi-line docstrings to start on their own line. Renamed a few tests that embedded those tags. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📝 WalkthroughWalkthroughTTL cascade support is added across configuration, cascade planning, atomic Lua execution, model TTL APIs, pipeline recovery, public exports, documentation, and unit/integration tests. Cascade traversal supports depth limits, special fields, dangling counts, cycles, shared nodes, and per-child TTLs. ChangesTTL Cascade
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Model
participant CascadeScript
participant Redis
Model->>CascadeScript: Refresh root TTL with cascade
CascadeScript->>Redis: Read JSON foreign-key references
CascadeScript->>Redis: EXPIRE root, child, and special-field keys
Redis-->>Model: Return dangling child and special counts
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Coverage reportTotal coverage: 99% Full report |
Merging this PR will degrade performance by 11.26%
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing |
…on cascade script - cascade unit fixtures re-establish class-declared Meta.cascade_ttl (a prior init_rapyer() authoritatively resets it to None); restore on teardown - two integration tests save before clearing/flushing scripts, so the expected error fires inside pytest.raises rather than during asave() Arrange (asave now always runs the cascade Lua script via refresh_ttl) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (5)
tests/unit/test_context.py (1)
1-104: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winMissing coverage for the
ResponseErrorswallow/re-raise branch.
execute_pipeline_with_noscript_recoveryalso swallows non-NOSCRIPTResponseErrorwhenignore_redis_error=Trueand re-raises otherwise (per its docstring inrapyer/context.py), but no test here exercises either sub-path.♻️ Suggested additional tests
`@pytest.mark.asyncio` async def test_execute_pipeline_with_noscript_recovery_swallows_response_error_when_ignored(): pipe = _make_pipe( command_stack=[(("JSON.SET", "k", "$", "{}"), {})], execute_side_effect=ResponseError("boom"), ) result = await execute_pipeline_with_noscript_recovery( pipe, MagicMock(), ignore_redis_error=True ) assert result == [] `@pytest.mark.asyncio` async def test_execute_pipeline_with_noscript_recovery_reraises_response_error_by_default(): pipe = _make_pipe( command_stack=[(("JSON.SET", "k", "$", "{}"), {})], execute_side_effect=ResponseError("boom"), ) with pytest.raises(ResponseError): await execute_pipeline_with_noscript_recovery(pipe, MagicMock())As per path instructions, "Focus on test coverage completeness and edge cases" for
tests/**.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/test_context.py` around lines 1 - 104, Add coverage for the ResponseError handling in execute_pipeline_with_noscript_recovery: import ResponseError and add async tests verifying a non-NOSCRIPT error returns an empty list when ignore_redis_error=True and is re-raised with the default setting. Use _make_pipe with a JSON.SET command and ResponseError side effect, while preserving existing tests.Source: Path instructions
rapyer/utils/annotation.py (1)
116-123: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider reusing
extract_annotationto simplifyhas_annotation.To adhere to the DRY principle, you can update
has_annotation(lines 104-113) to simply check ifextract_annotationreturns a non-Nonevalue.♻️ Proposed refactor for `has_annotation`
def has_annotation(field: Any, annotation_type: Any) -> bool: return extract_annotation(field, annotation_type) is not None🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rapyer/utils/annotation.py` around lines 116 - 123, Update has_annotation to delegate to extract_annotation and return whether the result is non-None. Remove its duplicated annotation-origin and metadata traversal while preserving the existing boolean behavior.rapyer/config.py (1)
80-80: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueUse
getattrto safely access_frozenduring object initialization or deserialization.While Pydantic usually populates
__pydantic_private__(wherePrivateAttrs are stored) directly during__init__without triggering__setattr__, certain object lifecycle states (such as unpickling or deep-copying) might invoke__setattr__before the private attributes dictionary is fully initialized, which would cause anAttributeError.Consider using
getattrto ensure robust attribute access.💡 Proposed fix
- if self._frozen and not name.startswith("_"): + if getattr(self, "_frozen", False) and not name.startswith("_"):🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rapyer/config.py` at line 80, Update the __setattr__ logic in the configuration model around the _frozen check to access _frozen safely with getattr and a false default. Preserve the existing name.startswith("_") condition and assignment behavior while preventing initialization, deserialization, or deep-copy lifecycle states from raising AttributeError.rapyer/scripts/lua/cascade/apply.lua (1)
244-275: 🚀 Performance & Scalability | 🔵 TrivialOperational note: unbounded cascades run as one long, blocking Lua script.
The full read-walk (all
JSON.GETs) plus theEXPIREwrite phase execute inside a single atomic EVALSHA, which blocks Redis's single-threaded command loop for its entire duration. With an unboundedCascadeTTL(no depth) or a deepoverridechain, a large/wide reachable subtree turns oneaset_ttl/refresh_ttlcall into a long-running script that stalls every other client on that Redis instance. This is an inherent tradeoff of the atomic-Lua design, not a defect — worth documenting as guidance to boundCascadeTTL(depth=...)on graphs that could grow large, and to monitor Lua execution time in production.Also applies to: 287-299
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rapyer/scripts/lua/cascade/apply.lua` around lines 244 - 275, Document guidance near plan_refresh_keys and the related refresh flow that unbounded or deeply overridden cascades execute all JSON reads and expiration writes in one blocking atomic Lua script. Recommend bounded CascadeTTL(depth=...) for potentially large or wide graphs and monitoring Lua execution time in production; do not alter the traversal or atomic execution behavior.rapyer/context.py (1)
48-99: 🎯 Functional Correctness | 🔵 Trivial | 🏗️ Heavy liftNOSCRIPT recovery path silently drops non-EVALSHA results; also relies on an internal Pipeline attribute.
Two related concerns on this helper:
commands_backup = list(pipe.command_stack)(line 69) reads redis-py's internalPipeline.command_stack— not documented public API. It has changed shape/behavior across redis-py releases for other pipeline variants (e.g.redis/redis-py#3703forClusterPipeline), so a future redis-py upgrade could silently break this without a semver signal.- On recovery, the function replays and returns only the
EVALSHAentries (lines 85-92), dropping any other commands originally queued in the same pipe from the returned list. This contradicts the docstring's claim that the success path "returns pipe.execute()'s result unchanged" — the recovery path returns a different, filtered list. Today every caller that queues EVALSHA-plus-other-commands in one pipe (ensure_pipeline's/pipeline_with_execution's exits,_apipeline) discards the return value, and the one caller that inspects it (aset_ttlin base.py) only ever uses a dedicated single-EVALSHA pipe — so this isn't live today, but it's a footgun for the next caller that mixes EVALSHA with other pipe commands and trusts positional results after a NOSCRIPT event.Please confirm the target redis-py version continues to expose
command_stackin this shape, and whether any call path (e.g. viamark_actions/refresh_ttl(can_use_pipeline=True)) nests the cascade EVALSHA into a pipe alongside other writes while consuming this function's return value.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rapyer/context.py` around lines 48 - 99, Update execute_pipeline_with_noscript_recovery to avoid depending on redis-py’s internal command_stack shape: verify the supported redis-py version and capture queued command metadata through a stable, explicit mechanism. Audit callers such as mark_actions, refresh_ttl, ensure_pipeline, pipeline_with_execution, _apipeline, and aset_ttl, then make NOSCRIPT recovery preserve the original command-result positions instead of returning only replayed EVALSHA results. Keep dedicated EVALSHA callers’ behavior unchanged and document or enforce the resulting recovery contract for mixed pipelines.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@rapyer/cascade/planner.py`:
- Around line 178-184: Update the field-class resolution in the loop over
model_cls._contain_sf before calling safe_issubclass: unwrap Optional/Annotated
annotations using the existing strip_optional and get_origin logic, matching
_unwrap_nested_model_cls, then perform the AtomicRedisModel check on the
unwrapped nested model class.
In `@tests/integration/foreign_keys/test_cascade_ttl_apply.py`:
- Around line 22-25: Update the TTL assertion in the test using
SCRIPT_FLUSH_ROOT_TTL_SECONDS so it verifies the resulting parent TTL is bounded
by the explicit 120-second value, rather than merely being positive. Ensure the
test setup persists parent and child as needed before invoking the script flush,
while preserving the existing distinction from CASCADE_FIXTURE_TTL_SECONDS and
the current assertion flow.
---
Nitpick comments:
In `@rapyer/config.py`:
- Line 80: Update the __setattr__ logic in the configuration model around the
_frozen check to access _frozen safely with getattr and a false default.
Preserve the existing name.startswith("_") condition and assignment behavior
while preventing initialization, deserialization, or deep-copy lifecycle states
from raising AttributeError.
In `@rapyer/context.py`:
- Around line 48-99: Update execute_pipeline_with_noscript_recovery to avoid
depending on redis-py’s internal command_stack shape: verify the supported
redis-py version and capture queued command metadata through a stable, explicit
mechanism. Audit callers such as mark_actions, refresh_ttl, ensure_pipeline,
pipeline_with_execution, _apipeline, and aset_ttl, then make NOSCRIPT recovery
preserve the original command-result positions instead of returning only
replayed EVALSHA results. Keep dedicated EVALSHA callers’ behavior unchanged and
document or enforce the resulting recovery contract for mixed pipelines.
In `@rapyer/scripts/lua/cascade/apply.lua`:
- Around line 244-275: Document guidance near plan_refresh_keys and the related
refresh flow that unbounded or deeply overridden cascades execute all JSON reads
and expiration writes in one blocking atomic Lua script. Recommend bounded
CascadeTTL(depth=...) for potentially large or wide graphs and monitoring Lua
execution time in production; do not alter the traversal or atomic execution
behavior.
In `@rapyer/utils/annotation.py`:
- Around line 116-123: Update has_annotation to delegate to extract_annotation
and return whether the result is non-None. Remove its duplicated
annotation-origin and metadata traversal while preserving the existing boolean
behavior.
In `@tests/unit/test_context.py`:
- Around line 1-104: Add coverage for the ResponseError handling in
execute_pipeline_with_noscript_recovery: import ResponseError and add async
tests verifying a non-NOSCRIPT error returns an empty list when
ignore_redis_error=True and is re-raised with the default setting. Use
_make_pipe with a JSON.SET command and ResponseError side effect, while
preserving existing tests.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 8b870c71-cf05-45da-9f3c-905ff49267ba
📒 Files selected for processing (47)
docs/documentation/special-fields/foreign-keys.mddocs/documentation/special-fields/ttl-cascade.mdmkdocs.ymlrapyer/__init__.pyrapyer/base.pyrapyer/cascade/__init__.pyrapyer/cascade/planner.pyrapyer/cascade/spec.pyrapyer/cascade/ttl.pyrapyer/config.pyrapyer/context.pyrapyer/errors/__init__.pyrapyer/errors/cascade.pyrapyer/init.pyrapyer/result.pyrapyer/scripts/constants.pyrapyer/scripts/lua/cascade/__init__.pyrapyer/scripts/lua/cascade/apply.luarapyer/scripts/registry.pyrapyer/utils/annotation.pytests/conftest.pytests/integration/foreign_keys/conftest.pytests/integration/foreign_keys/test_cascade_action_boundary.pytests/integration/foreign_keys/test_cascade_concurrent_mutation.pytests/integration/foreign_keys/test_cascade_graph_shapes.pytests/integration/foreign_keys/test_cascade_ttl_apply.pytests/integration/lst/test_redis_list_remove_range.pytests/integration/pipeline/test_pipeline_noscript_recovery.pytests/models/cascade_types.pytests/unit/cascade/__init__.pytests/unit/cascade/conftest.pytests/unit/cascade/test_aset_ttl_cascade_flag.pytests/unit/cascade/test_cascade_action_boundary.pytests/unit/cascade/test_cascade_apply_lua.pytests/unit/cascade/test_cascade_apply_lua_syntax.pytests/unit/cascade/test_cascade_classification.pytests/unit/cascade/test_cascade_plan_injection.pytests/unit/cascade/test_cascade_plan_table.pytests/unit/cascade/test_cascade_ttl_config.pytests/unit/cascade/test_cascade_ttl_required_validation.pytests/unit/cascade/test_extract_annotation.pytests/unit/cascade/test_init_rapyer_cascade_ttl.pytests/unit/cascade/test_meta_ttl_freeze.pytests/unit/cascade/test_refresh_ttl_cascade_branch.pytests/unit/test_context.pytests/unit/test_init_rapyer.pytests/unit/test_refresh_ttl_if_needed.py
…est comments (PR #283 review) - Rename CascadeEdge fields (collection/recurse/ttl/special/override -> is_collection/recurse_into_target/refresh_target_ttl/ refresh_target_special_keys/resets_depth_budget) across planner.py, apply.lua, and every test that inspects them; document the always-True flags as forward-looking per-edge hooks. - Rename RedisConfig._frozen -> _meta_locked across config.py, init.py, and tests. - Fix multi-line docstrings that started text on the opening `"""` line. - Unwrap Optional/generic annotations before the subclass check in _static_walk_special_suffixes (CodeRabbit #14). - Tighten the SCRIPT_FLUSH_ROOT_TTL_SECONDS assertion in test_cascade_ttl_apply.py to prove the explicit root ttl was applied, not just that some positive ttl survived (CodeRabbit #15). - Reformat inline Arrange/Act/Assert comment markers onto their own header line across the new cascade tests (CodeRabbit #12/#13). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… TTL feature (PR #283 #10) - context.py: remove execute_pipeline_with_noscript_recovery and its now-unused imports; ensure_pipeline/pipeline_with_execution revert to a bare pipe.execute() (develop's behavior). - base.py: restore develop's self-contained inline NOSCRIPT recovery inside _apipeline (EVALSHA-only replay + PersistentNoScriptError on second failure); aset_ttl's standalone execute is now bare, matching the TTL-refresh paths' new (documented) lack of self-heal. - Update/remove tests that asserted the now-removed generic recovery seam; test_pipeline_noscript_recovery.py's two script-flush tests now flush after the establishing asave() so only the explicit apipeline() block (backed by _apipeline) needs to recover. - Track extending self-heal to the TTL-refresh paths as a follow-up (see .planning/quick/260714-l0p-fix-9-failing-tests-on-pr-283-cascade-tt/NOSCRIPT-ISSUE.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Remove the _has_cascade ClassVar from AtomicRedisModel and the marking loop in init_rapyer -- it was written but never read since aset_ttl/refresh_ttl unified onto the cascade Lua script, which is the sole source of cascade-traversal truth via the plan table baked in at register_scripts time. - build_cascade_plan/validate_cascade_ttl_targets still run in init_rapyer for fail-fast config validation. - Strip the matching _has_cascade stash/restore scaffolding from six test files; pure pass-through wrapper fixtures are removed in favor of the base fixture they wrapped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…iew) Condense refresh_ttl's and aset_ttl's over-long cascade-ARGV comment blocks down to their essential why -- comment text only, zero logic change (should_execute=False / manual pipe.execute() pattern, ARGV order, and CascadeResult construction are untouched). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ry.lua Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…disConfig - Add cascade_function_name field, init-baked (None on fakeredis) - Exempt it from the _meta_locked freeze guard so arun_fcall self-heal can rewrite it Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Delete _CASCADE_FUNCTION_NAME module global and get_cascade_function_name - register_cascade_function returns the plan-hashed name instead of writing a global - run_fcall takes function_name explicitly; arun_fcall reads/refreshes it from redis_config - handle_missing_function assigns the refreshed name onto redis_config - Drop get_cascade_function_name from scripts package exports Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ad it to FCALL - init_rapyer assigns register_cascade_function's return onto every Meta post-freeze - Both base.py run_fcall sites (refresh_ttl, aset_ttl) pass self.Meta.cascade_function_name Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sertions and conftests - Unit assertions insert Meta.cascade_function_name as run_fcall's new second positional arg - Integration conftests capture register_cascade_function's return onto Meta and restore in teardown Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Delete unused extract_annotation from rapyer/utils/annotation.py - Delete its sole test file tests/unit/cascade/test_extract_annotation.py - field_with_flag (production superseder) and has_annotation untouched Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ecute sites - Add shared aexecute_pipeline_with_cascade_self_heal + aretry_fcall_after_missing_function to registry.py - ensure_pipeline/pipeline_with_execution route execute through the self-heal wrapper (lazy import, documented cycle) - aset_ttl routes bare execute through the self-heal wrapper - _apipeline replays FCALL on function-not-found, reusing aretry_fcall_after_missing_function - Rewrite config.py freeze-exempt comment; drop resolved issue #284 notes - Retry rewrites only the function-name slot; single retry then PersistentCascadeFunctionError - fakeredis EXPIRE branch and single-FCALL atomicity preserved Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…call in test helpers - Delete arun_fcall from registry.py (import + __all__ in scripts/__init__.py) - handle_missing_function and PersistentCascadeFunctionError retained (used by self-heal) - 3 integration _apply_cascade helpers call real_redis_client.fcall directly - Production self-heal now covers what arun_fcall did (Task 2) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…284, RED) - After FUNCTION FLUSH, aset_ttl(cascade=True) and refresh_ttl must reload the cascade function and still refresh the reachable subtree - Currently fails: redis-py's async pipeline masks the function-not-found message, so the helper's string match never fires inside a pipeline Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…not error text (GREEN) - redis-py's async pipeline annotate_exception (client.py:1585) overwrites exception.args with a non-f-string literal, destroying the "Function not found" message; every production FCALL runs inside a pipeline, so string-matching never fired there (only the deleted direct-client arun_fcall saw the real text) - Add acascade_function_missing (FUNCTION LIST scan) + _pipeline_has_fcall guard - Self-heal only fires on real Redis when the pipeline enqueued an FCALL and the cascade function is genuinely absent; other ResponseErrors re-raise unchanged - Wire the registry-based detection into both the shared wrapper and _apipeline Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…drop cascade self-heal helpers - context.py: ensure_pipeline + pipeline_with_execution use bare pipe.execute(), remove lazy registry imports - base.py: aset_ttl uses pipe.execute(); _apipeline reverts to NOSCRIPT-only path (no FCALL-missing detection/replay) - registry.py: delete six self-heal helpers; imports drop ResponseError, PersistentCascadeFunctionError, cascade.planner Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…te self-heal test, fix stale comments - errors/cascade.py + __init__.py: remove PersistentCascadeFunctionError and its __all__ entry - delete tests/integration/foreign_keys/test_cascade_self_heal.py - config.py: cascade_function_name freeze-exemption comment now references init_rapyer(), not the removed self-heal path Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eased entry since v1.3.4 Only #288 had an entry; five merged items were missing. - [1.3.6] new: SF-held reference cascade (#289, Added) and multi-class union / polymorphic-base FK reach (#290, Fixed) - [1.3.5] backfilled: the configurable TTL cascade feature itself (#283), the init_rapyer client-rebind fix (#276), runtime CPU optimizations (#263) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The complete Configurable TTL Cascade milestone, re-applied on top of the reverted
develop(after #279) with every review comment from #277 addressed — plus the full evolution since. This is the PR to review the whole diff in and merge cascade back intodevelop.Summary
Milestone: Configurable TTL Cascade — the first slice of a configurable cascade framework for
rapyer. Setting a parent aggregate's TTL performs a cascading refresh across its cascade-enabledForeignKey-referenced children — each child re-armed to its ownMeta.ttl— applied atomically and server-side at set-time. Cascade is opt-in and disabled-by-default, so existing projects are byte-for-byte unaffected.Status: All 4 phases complete and verified; full CI matrix green.
Phases (all complete)
CascadeTTL; FK-graph field classification feeding the cascade plan;CascadeSpecEXT-01 seam for future delete/save cascade.Meta.ttlin a single atomic op, with a per-class plan baked in.Evolution since the initial milestone (quick-task refinements)
EVALSHAscript to aFUNCTION LOAD/FCALLlibrary (library.lua): the plan is baked into the library and decoded once (memoized upvalue;cjsonis unavailable at load scope), so per-call cost is ~0. Library + function names carry a plan-hash for server-global isolation.FUNCTIONsupport, so it falls back to a root-ownall_keysEXPIREloop — preservingMeta.ttl/refresh_ttlbehavior. Cascade traversal tests are gated to real Redis 7+.RedisConfig.cascade_function_name(init-baked, freeze-exempt).extract_annotationand the test-onlyarun_fcallwrapper.Verification
black --check+ruff checkclean;mypy(3.10–3.13) green.Note on the CodSpeed check
The only red check is CodSpeed Performance Analysis.
developcurrently has no cascade at all (reverted in #279), so any cascade feature registers as a "regression" against a cascade-free baseline — this is the feature's inherent cost, not a defect. Against the last cascade-bearing benchmark baseline, the Functions rewrite is 11–46% faster with 0 regressions.Housekeeping
Supersedes #278 (which showed only the fixes because its branch shared the milestone commit as a merge-base with develop). #278 can be closed.
🤖 Generated with Claude Code