Skip to content

perf(cascade): fix bulk-insert-with-TTL regression (v1.3.4) - #288

Merged
yedidyakfir merged 1 commit into
developfrom
gsd/ttl-codspeed-regression
Jul 24, 2026
Merged

perf(cascade): fix bulk-insert-with-TTL regression (v1.3.4)#288
yedidyakfir merged 1 commit into
developfrom
gsd/ttl-codspeed-regression

Conversation

@yedidyakfir

@yedidyakfir yedidyakfir commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Problem

CodSpeed flagged a regression in v1.3.4 (the cascade-TTL milestone): bulk ainsert with a TTL got ~13% slower vs v1.3.3, while single insert and ~80 other benchmarks improved.

Isolated via CodSpeed compare (v1.3.3 9b3c9af → v1.3.4 ab6ec37): exactly 3 regressions, all test_benchmark_with_ttl on the multi-insert path:

  • test_model.py::TestModelInsertMany (4.5 → 5.1 ms, −13.2%)
  • test_module_api.py::TestModuleAinsertMany (4.8 → 5.5 ms, −13.1%)
  • test_module_api.py::TestModuleAinsertMixedClasses (4.8 → 5.6 ms, −13.2%)

Single insert with TTL did not regress (+11% faster) → a per-model cost that scales with the number of models inserted.

Root cause

v1.3.4 rewrote refresh_ttl/aset_ttl to always issue a per-model cascade Redis-Function FCALL on real Redis instead of a plain pipe.expire. Since ainsert auto-refreshes TTL per result model, bulk-inserting N no-FK models pays N heavier FCALLs — pure overhead when the models have no cascade edges.

Fix

Gate the FCALL on a per-model _has_cascade_edges flag (set by init_rapyer from the built cascade plan; lazily resolved otherwise). Models with no cascade-enabled FK edges take the pre-v1.3.4 native pipe.expire(all_keys) fast path; the FCALL only fires when there is an actual cascade graph to walk. Genuine cascade roots are unchanged — still atomic, server-side, cycle/depth guards intact. Additive; no change to Meta.ttl/refresh_ttl semantics.

Verification

  • Local: import rapyer OK; affected unit tests 24/24; debugger self-verification reported unit 802 pass, cascade+refresh-ttl 86 pass, integration FK 40 pass, ttl+FK 23 pass, black + ruff clean.
  • Micro-bench on live Redis: EXPIRE 0.378 ms vs FCALL 0.575 ms (+52%); end-to-end no-FK bulk insert recovers ~18% (TCP).
  • This PR triggers CodSpeed to confirm the 3 test_benchmark_with_ttl regressions clear with no new test_cascade_ttl regressions.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved TTL refresh behavior for models without cascading relationships.
    • These models now refresh their own expiration directly, avoiding unnecessary cascade processing.
    • Preserved expected TTL updates for both synchronous and asynchronous refresh operations.
  • Performance

    • Reduced overhead during expiration refreshes when no related records require cascading updates.
  • Tests

    • Updated coverage to verify direct expiration handling and prevent unnecessary cascade execution.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: aae0654b-450c-49c4-9a49-8a1b6c1e518e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

AtomicRedisModel now detects whether cascade-enabled foreign-key edges exist and skips cascade Lua execution during TTL refresh for models without such edges. Initialization caches this capability, and tests now assert direct Redis expiration.

Changes

Cascade-aware TTL refresh

Layer / File(s) Summary
Cascade capability detection and initialization
rapyer/cascade/planner.py, rapyer/init.py, rapyer/base.py
Cascade-edge presence is detected, cached during initialization, and lazily resolved when needed.
Native expiration fast paths
rapyer/base.py, tests/unit/cascade/test_refresh_ttl_cascade_branch.py, tests/unit/test_refresh_ttl_if_needed.py
Synchronous and asynchronous TTL refresh use native expire for models without cascade edges; tests verify that cascade FCALL is skipped.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant init_rapyer
  participant AtomicRedisModel
  participant cascade_planner
  participant RedisPipeline
  init_rapyer->>cascade_planner: build_cascade_plan()
  init_rapyer->>AtomicRedisModel: cache _has_cascade_edges
  AtomicRedisModel->>cascade_planner: _cascade_edges_present()
  cascade_planner-->>AtomicRedisModel: cascade edge presence
  AtomicRedisModel->>RedisPipeline: expire model keys when no cascade edges
  AtomicRedisModel->>RedisPipeline: run cascade FCALL when cascade edges exist
Loading

Possibly related PRs

Suggested reviewers: yedidyahkfir

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the performance regression fix for bulk inserts with TTLs in the cascade path.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gsd/ttl-codspeed-regression

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

Coverage report

Total coverage: 99%

Full report
Name                                                     Stmts   Miss  Cover
----------------------------------------------------------------------------
rapyer/__init__.py                                           5      0   100%
rapyer/actions.py                                          213      0   100%
rapyer/base.py                                             869      2    99%
rapyer/cascade/__init__.py                                   3      0   100%
rapyer/cascade/planner.py                                  144      5    97%
rapyer/cascade/spec.py                                      13      0   100%
rapyer/cascade/ttl.py                                        5      0   100%
rapyer/config.py                                            45      0   100%
rapyer/context.py                                           40      0   100%
rapyer/errors/__init__.py                                   11      0   100%
rapyer/errors/base.py                                       23      0   100%
rapyer/errors/cascade.py                                     8      0   100%
rapyer/errors/delete.py                                      3      0   100%
rapyer/errors/find.py                                       15      0   100%
rapyer/fields/__init__.py                                    4      0   100%
rapyer/fields/expression.py                                108      0   100%
rapyer/fields/index.py                                      20      0   100%
rapyer/fields/key.py                                        19      0   100%
rapyer/fields/safe_load.py                                  14      0   100%
rapyer/init.py                                              67      0   100%
rapyer/links.py                                              2      0   100%
rapyer/result.py                                            31      0   100%
rapyer/scripts/__init__.py                                   5      0   100%
rapyer/scripts/constants.py                                 17      0   100%
rapyer/scripts/loader.py                                    38      0   100%
rapyer/scripts/lua/__init__.py                               0      0   100%
rapyer/scripts/lua/atomic/__init__.py                        0      0   100%
rapyer/scripts/lua/cascade/__init__.py                       0      0   100%
rapyer/scripts/lua/datetime/__init__.py                      0      0   100%
rapyer/scripts/lua/dict/__init__.py                          0      0   100%
rapyer/scripts/lua/list/__init__.py                          0      0   100%
rapyer/scripts/lua/numeric/__init__.py                       0      0   100%
rapyer/scripts/lua/sf/__init__.py                            0      0   100%
rapyer/scripts/lua/sf/redis_priority_queue/__init__.py       0      0   100%
rapyer/scripts/lua/sf/redis_set/__init__.py                  0      0   100%
rapyer/scripts/lua/string/__init__.py                        0      0   100%
rapyer/scripts/registry.py                                  63      0   100%
rapyer/types/__init__.py                                    13      0   100%
rapyer/types/base.py                                       103      0   100%
rapyer/types/byte.py                                        33      0   100%
rapyer/types/convert.py                                     53      0   100%
rapyer/types/datetime.py                                    77      0   100%
rapyer/types/dct.py                                        116      0   100%
rapyer/types/float.py                                       65      0   100%
rapyer/types/foreign_key.py                                 68      0   100%
rapyer/types/generic.py                                     83      0   100%
rapyer/types/init.py                                        10      0   100%
rapyer/types/integer.py                                     58      0   100%
rapyer/types/lst.py                                        129      0   100%
rapyer/types/priority_queue.py                             102      0   100%
rapyer/types/redis_set.py                                  196      0   100%
rapyer/types/relational.py                                  24      0   100%
rapyer/types/special.py                                     39      0   100%
rapyer/types/string.py                                      22      0   100%
rapyer/typing_support.py                                     3      0   100%
rapyer/utils/__init__.py                                     0      0   100%
rapyer/utils/annotation.py                                  62      1    98%
rapyer/utils/fields.py                                      43      0   100%
rapyer/utils/pythonic.py                                    21      0   100%
rapyer/utils/redis.py                                       77      0   100%
----------------------------------------------------------------------------
TOTAL                                                     3182      8    99%

@codspeed-hq

codspeed-hq Bot commented Jul 21, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 12.45%

⚡ 17 improved benchmarks
✅ 154 untouched benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
WallTime test_benchmark_with_ttl 3.9 ms 3.4 ms +15.85%
WallTime test_benchmark_with_ttl 10.9 ms 9.5 ms +14.27%
WallTime test_benchmark_with_ttl 5.6 ms 4.9 ms +14.18%
WallTime test_benchmark_with_ttl 5.5 ms 4.9 ms +13.88%
WallTime test_benchmark_with_ttl 5.2 ms 4.6 ms +12.81%
WallTime test_benchmark_with_ttl 2.7 ms 2.4 ms +12.65%
WallTime test_benchmark_with_ttl 10.9 ms 9.7 ms +12.64%
WallTime test_benchmark_with_ttl 4 ms 3.6 ms +12.56%
WallTime test_benchmark_with_ttl 12 ms 10.7 ms +12.31%
WallTime test_benchmark_with_ttl 4.5 ms 4 ms +12.23%
WallTime test_benchmark_with_ttl 2.3 ms 2.1 ms +12.04%
WallTime test_benchmark_with_ttl 4.2 ms 3.8 ms +11.9%
WallTime test_benchmark_with_ttl 126 ms 112.9 ms +11.62%
WallTime test_benchmark_with_ttl 1.3 ms 1.2 ms +11.3%
WallTime test_benchmark_with_ttl 2.4 ms 2.2 ms +10.74%
WallTime test_benchmark_with_ttl 1.3 ms 1.1 ms +10.72%
WallTime test_benchmark_with_ttl 2.5 ms 2.3 ms +10.19%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing gsd/ttl-codspeed-regression (e2ac3e2) with develop (8d6ff3a)

Open in CodSpeed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
rapyer/base.py (1)

618-619: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add regression coverage for aset_ttl’s native fast path.

The supplied tests cover refresh_ttl, but aset_ttl has distinct execution and return behavior for cascade=False, cascade=True, and outer pipelines. Add no-edge cases verifying expiration of all_keys, no cascade FCALL, and the documented return values.

As per path instructions, tests/** should focus on test coverage completeness and edge cases.

🤖 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/base.py` around lines 618 - 619, Add regression tests for aset_ttl’s
native fast path when no cascade edges exist, covering cascade=False,
cascade=True, and execution inside an outer pipeline. Assert all_keys
expiration, confirm no cascade FCALL occurs, and verify each documented return
value while preserving the existing refresh_ttl coverage.

Source: Path instructions

🤖 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.

Nitpick comments:
In `@rapyer/base.py`:
- Around line 618-619: Add regression tests for aset_ttl’s native fast path when
no cascade edges exist, covering cascade=False, cascade=True, and execution
inside an outer pipeline. Assert all_keys expiration, confirm no cascade FCALL
occurs, and verify each documented return value while preserving the existing
refresh_ttl coverage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 949bb3fa-3b74-4cc5-8aa5-4e73e109308e

📥 Commits

Reviewing files that changed from the base of the PR and between 8d6ff3a and 9d75d30.

📒 Files selected for processing (5)
  • rapyer/base.py
  • rapyer/cascade/planner.py
  • rapyer/init.py
  • tests/unit/cascade/test_refresh_ttl_cascade_branch.py
  • tests/unit/test_refresh_ttl_if_needed.py

…egression

v1.3.4's cascade-TTL milestone made refresh_ttl/aset_ttl always issue a
per-model cascade FCALL on real Redis instead of a plain pipe.expire. Since
ainsert auto-refreshes TTL per result model, bulk-inserting N models with no
ForeignKey fields paid N heavier FCALLs -- pure overhead -- which regressed
the *Many/*MixedClasses _with_ttl benchmarks ~13% while single insert did not.

Gate the native-EXPIRE fast path on the _relational_field_names / _contain_fk
sets that __init_subclass__ already populates: a model with no FK fields takes
the plain pipe.expire path, and any model that has FK fields defers to the
cascade function (which no-ops when its plan carries no enabled edges). Genuine
cascade roots are unchanged -- still atomic, server-side, cycle/depth guards
intact. No new class variable, no cache, no cross-module import cycle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@yedidyakfir
yedidyakfir force-pushed the gsd/ttl-codspeed-regression branch from f99e293 to e2ac3e2 Compare July 24, 2026 13:07
@yedidyakfir
yedidyakfir merged commit 67943eb into develop Jul 24, 2026
57 checks passed
@yedidyakfir
yedidyakfir deleted the gsd/ttl-codspeed-regression branch July 24, 2026 13:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants