Skip to content

Harden Resque post-fork reconnect against saturated Redis (maxclients) [179D-310] - #874

Merged
brianlball merged 1 commit into
179D-310from
fix/resque-reconnect-retry-179D-310
Aug 18, 2026
Merged

Harden Resque post-fork reconnect against saturated Redis (maxclients) [179D-310]#874
brianlball merged 1 commit into
179D-310from
fix/resque-reconnect-retry-179D-310

Conversation

@brianlball

Copy link
Copy Markdown
Contributor

Cherry-pick of #873 onto the 179D-310 test branch (clean pick of f13e0c8c, no conflicts), so the fix rides the 3.10.0-179D-test images.

Problem

During the 2026-08-18 k8s deploy testing on the 179D-310 images, every analysis_wrappers job popped between 13:44 and 13:55 UTC vanished without a trace, stranding six analyses in queued forever (and losing three FinalizeAnalysis runs). The resque log showed:

(Job{analysis_wrappers} | ResqueJobs::InitializeAnalysis | [...]) failed: #<Redis::CommandError: ERR max number of clients reached>
Received exception when reporting failure: #<Redis::CommandError: ERR max number of clients reached>
Received exception when increasing failed jobs counter (redis issue) : #<Redis::CommandError: ERR max number of clients reached>

Root cause chain:

  1. A large spot-instance worker fleet (plus connections orphaned by unwarned node reclaims) pushed redis connected_clients past maxclients.
  2. Resque forks a child per job; the child must open a new redis connection (Resque::Worker#reconnect). Stock resque 2.6 only retries Redis::BaseConnectionError there — but a saturated server accepts the socket and replies -ERR max number of clients reached, which redis-rb raises as Redis::CommandError. No retry: the child dies ~4ms after got:.
  3. Reporting the failure needs redis too, so job.fail and the failed counter also throw — the job is never recorded anywhere, and the analysis sits in queued with no terminal state.

The capacity side is being fixed in the helm chart (redis.maxclients, keepalive/timeout). This PR is the app-side hardening so a brief saturation window degrades to a short wait instead of silent job loss.

Change

  • server/config/initializers/resque_reconnect_retry.rb — prepends an override of Resque::Worker#reconnect that also retries Redis::CommandError (covers maxclients saturation and -LOADING during AOF replay after a redis restart), 5 tries with increasing backoff (~30s total), then re-raises into the stock failure path. No-op in delayed_job deployments; retry count tunable via RESQUE_RECONNECT_RETRIES.
  • server/spec/lib/resque_reconnect_retry_spec.rb — unit specs, tagged depends_resque.
  • docker/server/run-server-tests.sh — runs the new spec in the docker (resque) CI job.

Testing

Verified in a local replica of the CI docker environment using the 3.10 image lineage of this branch (nrel/openstudio-server:3.10.0 + mongo:8.0.12 as db + redis:6.0.9 as queue, RAILS_ENV=docker): 6 examples, 0 failures. Same result on the develop image (see #873).

🤖 Generated with Claude Code

Stock resque 2.6 Worker#reconnect only retries Redis::BaseConnectionError.
A saturated Redis accepts the socket and replies "-ERR max number of
clients reached" (Redis::CommandError), so the forked child dies on its
first Redis use with no retry - and reporting the failure needs Redis
too, so the job vanishes unrecorded and InitializeAnalysis strands its
analysis in 'queued' forever (2026-08-18 k8s outage: large spot worker
fleet pushed connected_clients past maxclients).

- config/initializers/resque_reconnect_retry.rb: prepend override that
  also retries Redis::CommandError (covers maxclients and AOF LOADING),
  5 tries with backoff (~30s total), then re-raises into the stock
  failure path; no-op in delayed_job deployments (guarded on
  defined?(Resque::Worker)); retry count tunable via
  RESQUE_RECONNECT_RETRIES
- spec/lib/resque_reconnect_retry_spec.rb: unit specs, tagged
  depends_resque (resque only loads in resque envs); wired into the
  docker CI job via docker/server/run-server-tests.sh
- verified in a local replica of the CI docker env
  (nrel/openstudio-server:develop + mongo as db + redis as queue):
  6 examples, 0 failures

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@brianlball
brianlball merged commit 63bff78 into 179D-310 Aug 18, 2026
3 of 4 checks passed
@brianlball
brianlball deleted the fix/resque-reconnect-retry-179D-310 branch August 18, 2026 16:54
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.

1 participant