Skip to content

Configure ActionCable Redis reconnection retries - #8244

Open
donny-wong wants to merge 1 commit into
masterfrom
cable-redis-reconnect-attempts
Open

donny-wong wants to merge 1 commit into
masterfrom
cable-redis-reconnect-attempts

Conversation

@donny-wong

@donny-wong donny-wong commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Proposed Changes

  1. Adds reconnect_attempts: [1, 2, 5, 10, 30, 60, 120, 300, 300, 300] to the Redis adapter config in config/cable.yml (development and production).
  2. Adds config/initializers/action_cable.rb, which overrides the adapter's redis_connector so the connections ActionCable opens for publishing keep a single quick retry, while the subscription listener uses the long schedule above.
  3. Adds spec/initializers/action_cable_spec.rb guarding the initializer (no socket is opened, so it runs without Redis).

Motivation

After Redis was restarted on tiny, the running MarkUs processes never re-established their pub/sub subscriptions (redis-cli client list showed sub=0 for every client) and had to be restarted by hand. Realtime updates over ActionCable (submission collection progress, autotest results, PDF splitting, group creation) silently stop in that state while the site otherwise looks healthy.

Cause, from the ActionCable 8.1.4 adapter (lib/action_cable/subscription_adapter/redis.rb):

  • reconnect_attempts defaults to 1, which is converted to a single retry with a 0 s delay. That retry runs while Redis is still starting, fails, and the listener thread exits.
  • ensure_listener_running uses @thread ||= Thread.new, so once the thread has exited nothing recreates it. New browser subscriptions cannot revive it either.

With a list of delays the listener sleeps 1 s, 2 s, 5 s … between attempts (about 19 minutes total, enough to cover a host reboot or patching window) and, on success, re-issues SUBSCRIBE for every channel it previously had. The attempt counter resets after each successful reconnect, so the budget applies per outage. The listener only sleeps between attempts, so a long tail costs nothing.

Why the initializer

ActionCable passes the whole cable config, including reconnect_attempts, to Redis.new for every connection it opens, and PUBLISH is a retryable command in redis-client. Without the initializer, a broadcast issued while Redis is down blocks the calling thread until Redis returns or the window is exhausted. MarkUs broadcasts from Resque jobs (harmless) but also from three job-status controller actions (ExamTemplatesController, GroupsController, SubmissionsController), where that would pin a Puma thread for up to 19 minutes.

The listener wraps its subscribe in without_reconnect and runs its own retry loop driven by the cable config, so overriding redis_connector only affects the publishing side.

No gem upgrades are needed: redis 6.0.0 / redis-client 0.30.1 already accept an array for this option.

Verification (Docker Compose, development)

The reconnect behaviour itself cannot be covered by the test suite: the test environment uses adapter: test, so the Redis adapter is never loaded there, and the scenario needs a real Redis killed mid-run. To repeat the manual check: subscribe a process through ActionCable.server.pubsub.subscribe, docker compose restart redis, wait ~15 s, and confirm redis-cli client list shows sub ≥ 1 again and a broadcast is received; then docker compose pause redis and confirm a broadcast call raises within ~2 s instead of blocking.

  • Listener: a process subscribed through the adapter, Redis container restarted. 15 s later client list again showed the subscription, and a broadcast sent afterwards was received.
  • Publisher: with Redis paused, a broadcast call raised Redis::CannotConnectError after 2.0 s (one connect timeout plus one retry). Without the initializer the same call blocked for the whole 114 s outage and returned only when Redis resumed.
  • Inside the process: listener @reconnect_attempts = the 10-entry list, publisher connection @reconnect_attempts = [0].

Deploy note

MarkUs must be restarted once after this is deployed so the processes pick up the new config. After that, a Redis restart no longer requires a MarkUs restart.

Known limitations

  • Broadcasts published while Redis is down are lost (Redis pub/sub does not buffer), so a user waiting on a live update during the outage may need to refresh.
  • If Redis stays down longer than the retry window (~19 min), a MarkUs restart is still needed. Lengthen the tail of the list (more 300 entries) if longer outages are expected.

Type of Change

Type Applies?
🚨 Breaking change (fix or feature that would cause existing functionality to change)
✨ New feature (non-breaking change that adds functionality)
🐛 Bug fix (non-breaking change that fixes an issue) X
🎨 User interface change (change to user interface; provide screenshots)
♻️ Refactoring (internal change to codebase, without changing functionality)
🚦 Test update (change that only adds or modifies tests)
📦 Dependency update (change that updates a dependency)
📖 Documentation update (change that updates documentation)
🔧 Internal (change that only affects developers or continuous integration)

Checklist

Before opening your pull request:

  • I have performed a self-review of my changes.
  • I have added tests for my changes, if applicable.
  • I have updated the project documentation, if applicable.
  • If this is my first contribution, I have added myself to the list of contributors.

After opening your pull request:

  • I have updated the project Changelog (this is required for all changes).
  • I have verified that the pre-commit.ci checks have passed.
  • I have verified that the CI tests have passed.
  • I have reviewed the test coverage changes reported by Coveralls.
  • I have requested a review from a project maintainer.

🤖 Generated with Claude Code

When Redis restarted, every MarkUs web process lost its pub/sub
subscription and never got it back: ActionCable's Redis adapter
defaults to a single, immediate reconnect attempt, which fails while
Redis is still starting, after which the listener thread exits for the
lifetime of the process. Realtime updates (submission collection,
autotest results, PDF splitting, group creation) then silently stop
until MarkUs itself is restarted.

Set reconnect_attempts to a list of delays so the listener keeps
retrying for roughly 19 minutes (enough to cover a host reboot) and
resubscribes to all channels once Redis is back.

ActionCable passes the same option to the connection it publishes
broadcasts on, which would make a broadcast issued during an outage
block the calling thread for the whole window. Override the adapter's
redis_connector so publishing connections keep a single quick retry and
fail fast, while the listener keeps the long schedule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@donny-wong
donny-wong force-pushed the cable-redis-reconnect-attempts branch from 1cb969c to c1739fd Compare October 29, 2026 17:34
@donny-wong
donny-wong marked this pull request as ready for review October 29, 2026 17:43
@coveralls

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 40924034898

Coverage increased (+0.002%) to 91.065%

Details

  • Coverage increased (+0.002%) from the base build.
  • Patch coverage: 12 of 12 lines across 2 files are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 54151
Covered Lines: 50360
Line Coverage: 93.0%
Relevant Branches: 2569
Covered Branches: 1292
Branch Coverage: 50.29%
Branches in Coverage %: Yes
Coverage Strength: 127.24 hits per line

💛 - Coveralls

@donny-wong
donny-wong requested a review from Naragod October 29, 2026 18:39
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