Skip to content

Remove Redis keyspace SCANs in preparation for cluster mode - #2139

Merged
cyberantonz merged 1 commit into
constructorfabric:mainfrom
cyberantonz:preparation-to-redis-cluster-mode
Aug 3, 2026
Merged

Remove Redis keyspace SCANs in preparation for cluster mode#2139
cyberantonz merged 1 commit into
constructorfabric:mainfrom
cyberantonz:preparation-to-redis-cluster-mode

Conversation

@cyberantonz

@cyberantonz cyberantonz commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Redis Cluster has no cluster-wide SCAN cursor, so the two keyspace scans in the backend must go before cluster mode can be supported. This PR removes both; after it, no keyspace SCAN remains anywhere in the backend.

Authenticator

  • create_session trims already-expired members from the per-user session index (ZREMRANGEBYSCORE) inside the existing pipeline — same key, so the transaction stays single-slot on a cluster.
  • The per-user index gets an EXPIREAT at the longest member's absolute expiry, applied NX then GT (GT treats a TTL-less key as infinite, so NX seeds it; GT then only ever extends). Abandoned indexes self-expire instead of waiting for a sweeper.
  • The sid/sub back-channel indexes switch from a plain EXPIREAT to the same NX/GT guard — this also fixes a latent issue where a later, shorter-lived session could cut the shared index's TTL under a longer-lived one.
  • The janitor keeps its single-key duties (login-state index trim, refresh-schedule orphan cleanup) and no longer scans asm:user_sessions:*. Metric names are unchanged; removed/backlog now cover only those duties — alert thresholds calibrated on index backlog should be reviewed.
  • SessionManager::connect fails fast on Redis < 7.0 instead of surfacing EXPIREAT NX/GT rejections as a 500 on every login.
  • Upgrade note: indexes written by earlier builds have no TTL. Legacy sessions are dropped rather than repaired: flush asm:* at upgrade (users re-login), or accept that pre-upgrade sessions live out their TTL with a possible listing/revocation blind spot.

Analytics

  • flush_all walks a registry set (cat:v1:tenants, SADD-maintained on every put) instead of SCAN cat:v1:*. UNLINKs stay single-key (a batch could span cluster slots) but run concurrently on the multiplexed connection, and the enumerated members are removed with one SREM — never the whole set, so a concurrent put's registration can't be lost.
  • Upgrade note: hashes written by builds before this change aren't in the registry, so a flush_all during the first rolling deploy can miss them — the 5-minute per-hash TTL clears them shortly after.

Docs

  • Authenticator DESIGN.md updated to match (janitor scope, §4.3, key-cleanup notes).
  • DESIGN.md/PRD.md cleanup: references to the deleted API Gateway spec tree removed; inline decision references are now bare IDs resolved by §5 "Inherited decisions".
  • cfs validate passes on both.

Requirements

Redis ≥ 7.0 (EXPIREAT with NX/GT options) — enforced at boot.

Validation

  • cargo clippy --all-targets clean; unit tests 589 passed
  • Analytics live Redis suite (incl. the rewritten flush_all isolation test) against Redis 7: passed
  • Authenticator e2e (tests/run-e2e.sh, live Redis + binaries): 9/9 suites passed on the final revision
  • Independent adversarial review pass performed; findings addressed (version gate, flush/put race) or resolved by dropping legacy sessions at upgrade

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements

    • Improved cache clearing to remove tenant data more reliably while preserving newly written entries.
    • Enhanced session maintenance by automatically removing stale session references and preserving session expiration behavior.
    • Added startup validation requiring Redis 7 or newer.
    • Improved cleanup of expired login and refresh-related data.
  • Documentation

    • Updated authentication architecture, session lifecycle, cache behavior, and performance requirements to reflect the current system design.

@cyberantonz
cyberantonz requested a review from a team as a code owner August 3, 2026 08:12
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cyberantonz, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 26 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8f761aaf-2367-4d91-9335-c9d0c012f6bb

📥 Commits

Reviewing files that changed from the base of the PR and between 8edb00de40200ef9121739161fb3b77c2aaca80f and 80fdf4c.

📒 Files selected for processing (7)
  • .github/workflows/ci.yml
  • docs/components/backend/authenticator/DESIGN.md
  • docs/components/backend/authenticator/PRD.md
  • src/backend/services/analytics/src/domain/catalog/reader.rs
  • src/backend/services/analytics/src/infra/cache/catalog_cache.rs
  • src/backend/services/authenticator/src/janitor.rs
  • src/backend/services/authenticator/src/session.rs
📝 Walkthrough

Walkthrough

The PR updates authenticator architecture documentation, adds Redis 7 startup validation and writer-side session index cleanup, and changes catalog cache invalidation from keyspace scanning to registry-based flushing.

Changes

Authenticator architecture and session cleanup

Layer / File(s) Summary
Current authenticator architecture and decisions
docs/components/backend/authenticator/DESIGN.md, docs/components/backend/authenticator/PRD.md
The documentation now describes the standalone authenticator, current gateway boundaries, session flows, Redis namespace, ADR decisions, and latency targets without deleted-specification references.
Session index lifecycle and janitor cleanup
src/backend/services/authenticator/src/session.rs, src/backend/services/authenticator/src/janitor.rs, docs/components/backend/authenticator/DESIGN.md
Redis startup requires version 7 or newer. Writers trim stale index members and apply guarded EXPIREAT NX/GT updates. Janitor cleanup no longer scans per-user indexes.

Catalog cache registry flushing

Layer / File(s) Summary
Registry-based tenant cache flushing
src/backend/services/analytics/src/infra/cache/catalog_cache.rs, src/backend/services/analytics/src/domain/catalog/reader.rs
Catalog cache writes register tenant hash keys. flush_all() enumerates the registry, concurrently unlinks the captured keys, and preserves keys registered during the flush. Comments describe the updated invalidation behavior.

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

Possibly related PRs

Suggested reviewers: ktursunov

🚥 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 clearly and concisely describes the main change: removing Redis keyspace SCAN operations to support future cluster mode.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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

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.

@cyberantonz
cyberantonz enabled auto-merge August 3, 2026 08:57

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/backend/services/authenticator/src/session.rs (1)

401-472: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Don’t rely on this pipeline for Redis Cluster.

create_session runs pipe.atomic() across asm:session:*, asm:token:*, asm:jwt:*, asm:user_sessions:*, asm:sid_index:*, asm:sub_index:*, and literal asm:idp_refresh_due. Those keys are not in one slot, and Redis Cluster rejects multi-key commands such as MULTI/EXEC that span slots unless the keys satisfy a shared {...} tag. If Cluster is supported, use a redis+cluster:// client and a single-namespace pattern with hash tags for all joined keys, or split this write into isolated-slot pipelines.

🤖 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 `@src/backend/services/authenticator/src/session.rs` around lines 401 - 472,
The create_session pipeline must not execute atomic MULTI/EXEC across keys in
different Redis Cluster slots. Update the key-generation symbols used by
create_session, including user_sessions_key, sid_index_key, sub_index_key,
session, token, JWT, and idp_refresh_due keys, to use a shared hash-tag
namespace, or split the writes into slot-isolated pipelines while preserving
atomicity within each slot. Ensure the Redis client configuration supports the
selected Cluster-safe approach.
🧹 Nitpick comments (2)
src/backend/services/analytics/src/infra/cache/catalog_cache.rs (1)

34-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove documentation comments from service and private implementation items.

  • src/backend/services/analytics/src/infra/cache/catalog_cache.rs#L34-L36: remove the service module documentation.
  • src/backend/services/analytics/src/infra/cache/catalog_cache.rs#L77-L79: remove the private constant documentation and enforce required behavior with tests.

As per coding guidelines, “Use /// documentation comments only on exported items in shared library crates” and “Do not add documentation comments to binaries or services.”

🤖 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 `@src/backend/services/analytics/src/infra/cache/catalog_cache.rs` around lines
34 - 36, Remove the service module documentation at
src/backend/services/analytics/src/infra/cache/catalog_cache.rs lines 34-36 and
the private constant documentation at lines 77-79. Add tests covering the
required behavior currently described by the private constant documentation; no
direct documentation changes are needed elsewhere.

Source: Coding guidelines

src/backend/services/authenticator/src/session.rs (1)

217-241: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document and test the Redis version gate.

SessionManager::connect now fails when INFO fails or Redis reports a major version below 7, but connect’s # Errors still only mentions malformed URLs and initial connection failures. Re-check the module for the exact test location and add a named test: reject Redis 6 or malformed redis_version: metadata; accept Redis 7.

🤖 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 `@src/backend/services/authenticator/src/session.rs` around lines 217 - 241,
Update the documentation for SessionManager::connect so its Errors section
includes Redis INFO failures and rejection of Redis versions below 7. Add a
named test at the module’s existing test location covering Redis 6 and malformed
redis_version metadata as rejected cases, plus Redis 7 as an accepted case,
reusing the version-gating logic rather than requiring live Redis connections.

Source: Coding guidelines

🤖 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 `@docs/components/backend/authenticator/DESIGN.md`:
- Line 39: Update the inherited decisions TOC entry in DESIGN.md to use the
fragment generated from its matching heading, or add an explicit anchor that
matches the existing `#inherited-decisions-dd-bff--dd-router` target, so the link
resolves and MD051 passes.

In `@src/backend/services/analytics/src/infra/cache/catalog_cache.rs`:
- Around line 390-393: Update the registry handling around the `sadd` call to
use expiry-aware, bounded-retention membership and remove entries when tenant
caches are invalidated or expire. In `flush_all`, process `TENANT_REGISTRY_KEY`
members in bounded batches or via streaming rather than buffering the full set,
and cap concurrent `UNLINK` operations; apply these changes at both
catalog_cache.rs:390-393 and catalog_cache.rs:427-438.
- Around line 440-442: Update flush_all and the corresponding put path so
concurrent rewrites cannot remove a registry membership after a newer payload
write; use a versioned registry entry or an equivalent write/flush barrier, and
ensure SREM only removes entries whose version still matches the enumerated
snapshot. Add an integration test that forces put to occur between UNLINK and
SREM and verifies the key remains registered.

In `@src/backend/services/authenticator/src/janitor.rs`:
- Around line 3-9: Update the janitor metrics and documentation around
janitor_pass so refresh backlog is reported independently from janitor pass
liveness. Add or use a dedicated pass-heartbeat metric emitted on each
successful janitor pass, and stop treating a rising backlog metric as evidence
that no pod is running.

In `@src/backend/services/authenticator/src/session.rs`:
- Around line 825-829: Split the responsibilities currently concentrated in
session.rs into focused Rust modules, keeping one primary noun per file and
limiting each module to roughly 400 lines. Extract Redis connection,
login-state/index handling, session lifecycle, IdP refresh, janitor, and
back-channel logout workers into appropriately named modules; update module
declarations, imports, visibility, and call sites so behavior and public
interfaces remain unchanged.
- Around line 401-419: Document the legacy authenticator-index migration/reset
and the limitation that writer-only EXPIREAT NX/GT cleanup cannot bound
untouched no-TTL indexes or validate existing members. Update the index
write/cleanup paths in src/backend/services/authenticator/src/session.rs:401-419
and 430-472, the related expiry/lifetime note at
src/backend/services/authenticator/src/session.rs:825-829, and the janitor
context at src/backend/services/authenticator/src/janitor.rs:3-9; update the
corresponding design documentation at
docs/components/backend/authenticator/DESIGN.md:447, 770-780, and 955-971.
Preserve the current EXPIREAT behavior while clearly requiring a one-time legacy
asm:* reset or migration.

---

Outside diff comments:
In `@src/backend/services/authenticator/src/session.rs`:
- Around line 401-472: The create_session pipeline must not execute atomic
MULTI/EXEC across keys in different Redis Cluster slots. Update the
key-generation symbols used by create_session, including user_sessions_key,
sid_index_key, sub_index_key, session, token, JWT, and idp_refresh_due keys, to
use a shared hash-tag namespace, or split the writes into slot-isolated
pipelines while preserving atomicity within each slot. Ensure the Redis client
configuration supports the selected Cluster-safe approach.

---

Nitpick comments:
In `@src/backend/services/analytics/src/infra/cache/catalog_cache.rs`:
- Around line 34-36: Remove the service module documentation at
src/backend/services/analytics/src/infra/cache/catalog_cache.rs lines 34-36 and
the private constant documentation at lines 77-79. Add tests covering the
required behavior currently described by the private constant documentation; no
direct documentation changes are needed elsewhere.

In `@src/backend/services/authenticator/src/session.rs`:
- Around line 217-241: Update the documentation for SessionManager::connect so
its Errors section includes Redis INFO failures and rejection of Redis versions
below 7. Add a named test at the module’s existing test location covering Redis
6 and malformed redis_version metadata as rejected cases, plus Redis 7 as an
accepted case, reusing the version-gating logic rather than requiring live Redis
connections.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 173f41d3-6e8e-49d5-a0ff-7c0db27b94da

📥 Commits

Reviewing files that changed from the base of the PR and between 153a278 and 8edb00de40200ef9121739161fb3b77c2aaca80f.

📒 Files selected for processing (6)
  • docs/components/backend/authenticator/DESIGN.md
  • docs/components/backend/authenticator/PRD.md
  • src/backend/services/analytics/src/domain/catalog/reader.rs
  • src/backend/services/analytics/src/infra/cache/catalog_cache.rs
  • src/backend/services/authenticator/src/janitor.rs
  • src/backend/services/authenticator/src/session.rs

Comment thread docs/components/backend/authenticator/DESIGN.md Outdated
Comment thread src/backend/services/analytics/src/infra/cache/catalog_cache.rs
Comment thread src/backend/services/analytics/src/infra/cache/catalog_cache.rs
Comment on lines +3 to +9
//! Per-key Redis TTLs remove session records and token mappings; the
//! login-state live index and refresh-schedule orphans linger until trimmed.
//! Per-user session indexes are trimmed inline by writers and TTL-bounded,
//! so the pass issues no keyspace SCAN. One leader (Redis lock, DD-BFF-09 —
//! same election as the refresher) runs a pass every
//! `janitor_interval_seconds` (default 30 s) and emits removed/backlog
//! metrics; a rising backlog means no pod is running passes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Separate backlog from pass liveness.

janitor_pass counts overdue refresh entries before orphan trimming. A rising backlog can also result from a delayed refresher or repeated IdP failures while the janitor continues to run. Use a separate pass-heartbeat metric for “no pod is running.”

🤖 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 `@src/backend/services/authenticator/src/janitor.rs` around lines 3 - 9, Update
the janitor metrics and documentation around janitor_pass so refresh backlog is
reported independently from janitor pass liveness. Add or use a dedicated
pass-heartbeat metric emitted on each successful janitor pass, and stop treating
a rising backlog metric as evidence that no pod is running.

Comment thread src/backend/services/authenticator/src/session.rs
Comment on lines +825 to +829
/// One janitor pass (DESIGN §4.3): trim expired members from the
/// login-state index and drop long-overdue orphans from the refresh
/// schedule (live sessions are re-scheduled by the refresher; an entry
/// still due after `orphan_grace` has no owner). Per-user session
/// indexes are trimmed inline by writers and TTL-bounded — no SCAN.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Split session.rs into focused modules.

session.rs is over 1,000 lines and mixes Redis connection, login state, session lifecycle, IdP refresh, janitor, and back-channel logout. Move worker and index responsibilities into focused modules.

As per coding guidelines: keep one noun per Rust file and split modules when a module exceeds approximately 400 lines.

🤖 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 `@src/backend/services/authenticator/src/session.rs` around lines 825 - 829,
Split the responsibilities currently concentrated in session.rs into focused
Rust modules, keeping one primary noun per file and limiting each module to
roughly 400 lines. Extract Redis connection, login-state/index handling, session
lifecycle, IdP refresh, janitor, and back-channel logout workers into
appropriately named modules; update module declarations, imports, visibility,
and call sites so behavior and public interfaces remain unchanged.

Source: Coding guidelines

@cyberantonz
cyberantonz force-pushed the preparation-to-redis-cluster-mode branch from 8edb00d to 3706d1a Compare August 3, 2026 11:20
@cyberantonz
cyberantonz disabled auto-merge August 3, 2026 11:24
@cyberantonz
cyberantonz force-pushed the preparation-to-redis-cluster-mode branch from f1a4b22 to 855ef20 Compare August 3, 2026 11:24
@cyberantonz
cyberantonz enabled auto-merge August 3, 2026 11:25
Redis Cluster has no cluster-wide SCAN cursor, so both backend
keyspace scans go away; no SCAN remains anywhere in the backend.

Authenticator: create_session trims already-expired members from the
per-user session index in its own (single-key, single-slot) pipeline,
and the user/sid/sub indexes carry EXPIREAT NX/GT TTLs so abandoned
keys self-expire — NX seeds a TTL, GT only ever extends, which also
stops a shorter-lived session from cutting a shared index's TTL under
a live one. The janitor keeps only its single-key duties (login-state
trim, refresh-schedule orphans). SessionManager::connect fails fast
on Redis < 7.0 (EXPIREAT NX/GT). Legacy pre-TTL indexes are dropped
at upgrade rather than repaired: flush asm:* (users re-login) or
accept the TTL-bounded listing/revocation blind spot.

Analytics: flush_all walks a cat:v1:tenants registry set instead of
SCAN cat:v1:*. UNLINKs stay single-key (a batch could span cluster
slots), run concurrently with a bounded in-flight cap, and only the
enumerated members are SREMed so a concurrent put's registration is
never lost.

CI: the coverage job starts a Redis 7 container and runs the cache
live tests in a second, serial llvm-cov invocation (flush_all wipes
the shared keyspace, so parallel cache tests race each other).

Docs: authenticator DESIGN.md updated (janitor scope, key-cleanup and
pre-TTL-upgrade notes); DESIGN/PRD references to the deleted API
Gateway spec tree removed — inline decision references are bare IDs
resolved by the section 5 'Inherited decisions' list.

Requires Redis >= 7.0, enforced at boot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Anton Zelenov <antonz@constructor.tech>
@cyberantonz
cyberantonz force-pushed the preparation-to-redis-cluster-mode branch from 855ef20 to 80fdf4c Compare August 3, 2026 11:27
@cyberantonz
cyberantonz added this pull request to the merge queue Aug 3, 2026
Merged via the queue into constructorfabric:main with commit e821321 Aug 3, 2026
48 checks passed
@cyberantonz
cyberantonz deleted the preparation-to-redis-cluster-mode branch August 3, 2026 12:16
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