Skip to content

feat(session_store): supervise Redis store subtree to make restarts race-free - #242

Merged
zoedsoupe merged 7 commits into
zoedsoupe:mainfrom
faisalnazir7:fix/redis-store-idempotent-start
Jul 29, 2026
Merged

zoedsoupe merged 7 commits into
zoedsoupe:mainfrom
faisalnazir7:fix/redis-store-idempotent-start

Conversation

@faisalnazir7

@faisalnazir7 faisalnazir7 commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

Closes #241

Problem

Anubis.Server.Session.Store.Redis fails to restart under the server's
:one_for_all supervisor, crashing with {:error, {:already_started, pid}}.

The store registered two fixed names — the store GenServer (name: __MODULE__)
and a Redix pool supervisor started inside init/1 (:"anubis_#{conn_name}_ supervisor"). That inner supervisor was linked to the store but was not a
supervised child, so its teardown ran asynchronously relative to the parent's
restart. Since Anubis.Server.Supervisor runs :one_for_all and mounts the
store as a direct child, any sibling crash (transport / registry /
session-supervisor) restarted the store and re-ran the racy start against
still-registered names — either {:already_started, pid} on the store name or
the {:stop, error} "Failed to start Redis session store" path on the inner
supervisor name. Under sustained restarts the endpoint flapped or failed to
recover until the node restarted.

Solution

Make the store a proper supervised subtree instead of patching the race:

  • Anubis.Server.Session.Store.Redis is now a Supervisor (type: :supervisor
    child spec) whose children are the Redix connection pool plus an internal
    Redis.Server GenServer that answers the Session.Store behaviour calls.
  • The Supervisor.start_link-inside-init/1 orphan is gone; the pool lives in
    the supervision tree.
  • On a :one_for_all parent restart, OTP shuts the whole store subtree down
    synchronously (supervisor children wait :infinity), releasing the store name
    and every :"anubis_#{conn_name}_#{i}" connection name before the store
    is restarted. Restarts are race-free by construction — no {:already_started},
    no process adoption.

The Redis wire behaviour (SETEX/GET/DEL/EXPIRE/SCAN, JSON, namespace, TTL, key
format, monotonic pool selection) is unchanged, and the public API signatures are
unchanged, so no caller (session.ex, plug.ex) is touched.

Rationale

The first pass rescued {:already_started, pid} on both start paths (commit
68733c9). Review correctly flagged that returning that pid to the supervisor is
an OTP contract violation — GenServer.start_link does not link on
{:already_started}, so the parent would hold an unlinked child it cannot
monitor. Rather than paper over the race, commit cc2cabf removes its source: a
supervised subtree lets OTP coordinate start/stop and free names synchronously.
This resolves the ownership and pool-reuse concerns structurally rather than
defensively.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 611b0d35-7616-4e88-951a-5845c4274fa6

📥 Commits

Reviewing files that changed from the base of the PR and between 478635c and 4131375.

📒 Files selected for processing (1)
  • test/anubis/server/session/store/redis_test.exs

📝 Walkthrough

Problem

Redis session store restarts under the :one_for_all supervisor could fail because fixed process names (store/pool) could remain registered, causing racey teardown and replacement.

Solution

Reworked Anubis.Server.Session.Store.Redis into a proper supervised subtree: the store module now supervises the Redix connection pool and a nested Redis.Server GenServer, and the store API delegates calls to the nested server instead of the former top-level GenServer. Added tests to deterministically verify restart cleanup and delegation behavior.

Rationale

By making the pool and operational server children of the store subtree, OTP can synchronously terminate everything before restart, preventing {:already_started}/adoption failures while keeping Redis behavior and the public API unchanged.

Walkthrough

The Redis session store now runs as a supervisor containing Redix pool children and an internal Redis.Server GenServer. Public store operations forward calls to that server, while Redis persistence logic and state are encapsulated there. Tests cover supervision structure, restart replacement, API delegation, and session persistence across restart.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly names the Redis store subtree supervision fix and the restart race it addresses.
Description check ✅ Passed The description follows the template with Problem, Solution, and Rationale sections and includes the key fix details.
Linked Issues check ✅ Passed The PR resolves #241 by making the Redis store a supervised subtree and eliminating the :already_started restart race.
Out of Scope Changes check ✅ Passed The changes stay focused on the Redis store restart fix and its tests, with no unrelated feature work visible.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code

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.

@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: 5

Caution

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

⚠️ Outside diff range comments (1)
lib/anubis/server/session/store/redis.ex (1)

146-170: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

P3 — Hibernate after initialization.

Return {:ok, state, :hibernate} from both initialization-success paths.

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 26186f26-bdc2-49e9-b91e-96c038826125

📥 Commits

Reviewing files that changed from the base of the PR and between 2451bb7 and 68733c9.

📒 Files selected for processing (2)
  • lib/anubis/server/session/store/redis.ex
  • test/anubis/server/session/store/redis_test.exs

Comment thread lib/anubis/server/session/store/redis.ex Outdated
Comment thread lib/anubis/server/session/store/redis.ex Outdated
Comment thread test/anubis/server/session/store/redis_test.exs
Comment thread test/anubis/server/session/store/redis_test.exs Outdated
Comment thread test/anubis/server/session/store/redis_test.exs Outdated
@faisalnazir7 faisalnazir7 changed the title fix(session_store): make Redis store start idempotent under :one_for_all restart fix(session_store): supervise Redis store subtree to make restarts race-free Jul 23, 2026

@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: 3

Caution

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

⚠️ Outside diff range comments (1)
lib/anubis/server/session/store/redis.ex (1)

131-145: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Pool-name derivation is duplicated between init/1 and Server.get_connection/1.

The :"anubis_#{conn_name}_#{i}" pattern here is re-derived independently in get_connection/1 (line 350) as :"anubis_#{state.conn_name}_#{index}". They happen to agree today, but nothing enforces that — a future edit to one naming scheme without the other silently produces :noproc errors for every Redis call. Extract a single private helper (e.g. pool_worker_name(conn_name, index)) shared by both sites.

♻️ Proposed shared helper
+  defp pool_worker_name(conn_name, index), do: :"anubis_#{conn_name}_#{index}"
+
   def init(opts) do
     ...
     pool_children =
       for i <- 1..pool_size do
-        child_id = :"anubis_#{conn_name}_#{i}"
+        child_id = pool_worker_name(conn_name, i)
         ...
   defp get_connection(state) when is_struct(state, State) do
     index = rem(:erlang.unique_integer([:positive]), state.pool_size) + 1
-    :"anubis_#{state.conn_name}_#{index}"
+    Anubis.Server.Session.Store.Redis.pool_worker_name(state.conn_name, index)
   end

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1c6c1541-2cc4-42b7-9c60-4b8f00d7e722

📥 Commits

Reviewing files that changed from the base of the PR and between 68733c9 and cc2cabf.

📒 Files selected for processing (2)
  • lib/anubis/server/session/store/redis.ex
  • test/anubis/server/session/store/redis_test.exs

Comment thread lib/anubis/server/session/store/redis.ex
Comment thread lib/anubis/server/session/store/redis.ex Outdated
Comment thread test/anubis/server/session/store/redis_test.exs

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
lib/anubis/server/session/store/redis.ex (1)

131-145: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

P2 — Reject non-positive pool sizes before building children.

pool_size: 0 enumerates 1..0 but stores 0 in state; the first Redis operation then crashes at rem(_, 0). Require a positive integer via the initialization schema before constructing the pool. 😎

As per coding guidelines, “Use import Peri and defschema for all validation with schema definitions as module attributes.”

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bc04232b-5b15-4b5e-a161-72ba4afc8404

📥 Commits

Reviewing files that changed from the base of the PR and between cc2cabf and 88dc5be.

📒 Files selected for processing (2)
  • lib/anubis/server/session/store/redis.ex
  • test/anubis/server/session/store/redis_test.exs

Comment thread lib/anubis/server/session/store/redis.ex

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
lib/anubis/server/session/store/redis.ex (1)

211-256: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

P2 — Validate :pool_size before starting the subtree.

pool_size: 0 builds 1..0, then every Redis-backed call crashes in rem(_, 0) inside get_connection/1. Parse and validate initialization options with a Peri schema, requiring a positive pool size before constructing children. 😎

As per coding guidelines, use import Peri/defschema for validation and the parse_options!/1 initialization pattern.

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cc608a19-669a-47e8-8700-41ac485d9558

📥 Commits

Reviewing files that changed from the base of the PR and between 88dc5be and 478635c.

📒 Files selected for processing (2)
  • lib/anubis/server/session/store/redis.ex
  • test/anubis/server/session/store/redis_test.exs

Comment thread test/anubis/server/session/store/redis_test.exs
@zoedsoupe zoedsoupe changed the title fix(session_store): supervise Redis store subtree to make restarts race-free feat(session_store): supervise Redis store subtree to make restarts race-free Jul 29, 2026
@zoedsoupe
zoedsoupe merged commit 48c8c1a into zoedsoupe:main Jul 29, 2026
12 checks passed
@zoedsoupe zoedsoupe mentioned this pull request Jul 29, 2026
zoedsoupe added a commit that referenced this pull request Jul 29, 2026
🚀 Want to release this?
---


##
[1.11.0](v1.10.0...v1.11.0)
(2026-07-29)


### Features

* **session_store:** supervise Redis store subtree to make restarts
race-free ([#242](#242))
([48c8c1a](48c8c1a))


### Bug Fixes

* **prompts:** wrap prompt content objects and map system_message to
user role ([#234](#234))
([2451bb7](2451bb7))
* **server:** make title option compile in use Anubis.Server.Component
([b04feed](b04feed))
* **server:** use restart :temporary for session processes
([#240](#240))
([30bc4f5](30bc4f5))
* **sse:** buffer partial events across Finch chunks
([#245](#245))
([beea2f6](beea2f6))
* Stream the client SSE GET instead of buffering it (server push never
delivered) ([#231](#231))
([a722c1b](a722c1b))
* **telemetry:** expose tool call success/failure in tool_call span
metadata ([#246](#246))
([ace5ecb](ace5ecb))
* **telemetry:** include client_info in initialize response event
metadata ([#248](#248))
([74ed457](74ed457))
* **telemetry:** namespace tool_call span under :anubis_mcp
([#244](#244))
([561a96b](561a96b))


### Tests

* **server:** fix stale tool_call event name and function_exported?
loading races
([7247d2c](7247d2c))
* **transport:** synchronize held SSE plug with Bypass teardown
([93c5a34](93c5a34))


### Continuous Integration

* fix dialyzer plt caching
([8424439](8424439))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
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.

Redis session store crashes with {:already_started} on :one_for_all supervisor restart

2 participants