From f4af81ef02d9c7529f812e8701c2275aaa776d50 Mon Sep 17 00:00:00 2001 From: jakeaturner Date: Tue, 23 Jun 2026 17:40:10 +0000 Subject: [PATCH] fix: lazy-connect + retry for ioredis to avoid blocked startup --- admin/config/queue.ts | 19 +++++++++++++++++++ install/entrypoint.sh | 23 +++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/admin/config/queue.ts b/admin/config/queue.ts index 3c6a112..5b33d1e 100644 --- a/admin/config/queue.ts +++ b/admin/config/queue.ts @@ -1,4 +1,5 @@ import env from '#start/env' +import logger from '@adonisjs/core/services/logger' import { Redis } from 'ioredis' // BullMQ treats a plain `{host, port}` connection object as a recipe: every @@ -14,6 +15,24 @@ const sharedConnection = new Redis({ port: env.get('REDIS_PORT') ?? 6379, db: env.get('REDIS_DB') ?? 0, maxRetriesPerRequest: null, + // Don't open the socket at module import time. Importing this file (during + // `node ace migration:run`, `db:seed`, `queue:work`, or HTTP boot) otherwise + // races Docker's network/DNS lifecycle: on a fresh `up` the `redis` name is + // not yet resolvable (EAI_AGAIN), and on a recreate the embedded DNS briefly + // serves the previous container's IP (ECONNREFUSED to the stale address). + // Lazy-connecting defers the first dial until BullMQ actually needs Redis — + // after the entrypoint has confirmed it is reachable — so each (re)connect + // re-resolves the current IP instead of hammering a stale one. + lazyConnect: true, + // Bounded, backing-off retry so a transient outage doesn't busy-loop. + retryStrategy: (times) => Math.min(times * 200, 2000), +}) + +// Without an `error` listener ioredis logs the raw "[ioredis] Unhandled error +// event" lines and, on some Node versions, an EventEmitter `error` with no +// listener can crash the process. Route them through the app logger instead. +sharedConnection.on('error', (err) => { + logger.error({ err }, 'Shared Redis connection error') }) const queueConfig = { diff --git a/install/entrypoint.sh b/install/entrypoint.sh index 4361903..7375e71 100644 --- a/install/entrypoint.sh +++ b/install/entrypoint.sh @@ -7,6 +7,29 @@ echo "Starting entrypoint script..." # Ensure required storage directories exist (volume may be freshly mounted) mkdir -p /app/storage/logs /app/storage/kb_uploads +# Wait for Redis to be reachable before booting anything that opens a BullMQ +# connection. `depends_on: condition: service_healthy` only gates a clean +# `up --recreate`; it is NOT re-checked on `docker compose restart` or a +# `restart: unless-stopped` bounce, so without this the app can race Docker's +# DNS (EAI_AGAIN) or dial a restarted Redis container's stale IP (ECONNREFUSED). +# This isn't load-bearing since legacy installs used a mounted entrypoint script +# that may override this script, but it's a cost-nothing check for newer installs. +# The real check is done by the application itself, but this provides a safety net. +REDIS_HOST="${REDIS_HOST:-redis}" +REDIS_PORT="${REDIS_PORT:-6379}" +echo "Waiting for Redis at ${REDIS_HOST}:${REDIS_PORT}..." +for i in $(seq 1 60); do + if node -e "const net=require('net');const s=net.connect(Number(process.env.REDIS_PORT||6379),process.env.REDIS_HOST||'redis');s.on('connect',()=>{s.end();process.exit(0)});s.on('error',()=>process.exit(1));" 2>/dev/null; then + echo "Redis is up and running!" + break + fi + if [ "$i" -eq 60 ]; then + echo "Timed out waiting for Redis at ${REDIS_HOST}:${REDIS_PORT}" >&2 + exit 1 + fi + sleep 1 +done + # Run AdonisJS migrations echo "Running AdonisJS migrations..." node ace migration:run --force