11require ( 'dotenv' ) . config ( ) ;
22
33const Arena = require ( 'bull-arena' ) ;
4+ const Redis = require ( 'ioredis' ) ;
45const express = require ( 'express' ) ;
56const basicAuth = require ( 'express-basic-auth' ) ;
67
@@ -9,6 +10,80 @@ const router = express.Router();
910
1011const DEFAULT_LISTEN_PORT = 4000 ;
1112
13+ // A transient Redis outage (Heroku failover, network blip) makes ioredis/bull
14+ // emit errors that, left unhandled, crash the process and put the container in
15+ // a restart loop. This is a read-only monitoring UI, so log and keep running —
16+ // ioredis will reconnect on its own (see retryStrategy below).
17+ process . on ( 'uncaughtException' , ( error ) => {
18+ console . error ( 'Uncaught exception (continuing):' , error ) ;
19+ } ) ;
20+ process . on ( 'unhandledRejection' , ( error ) => {
21+ console . error ( 'Unhandled rejection (continuing):' , error ) ;
22+ } ) ;
23+
24+ // Build ioredis connection options from a redis:// URL, adding resilience
25+ // settings. We parse the URL rather than passing the string through so we can
26+ // inject these options; the derived fields match what ioredis would parse from
27+ // the same URL, so AUTH/TLS behavior is unchanged.
28+ const buildRedisOptions = function buildRedisOptions ( redisUrl ) {
29+ const parsed = new URL ( redisUrl ) ;
30+
31+ const options = {
32+ host : parsed . hostname ,
33+ port : Number ( parsed . port ) || 6379 ,
34+ // Keep retrying commands across reconnects instead of throwing
35+ // MaxRetriesPerRequestError after ioredis' default of 20 retries.
36+ maxRetriesPerRequest : null ,
37+ // Reconnect with a capped backoff after the connection drops.
38+ retryStrategy : ( times ) => Math . min ( times * 200 , 5000 ) ,
39+ } ;
40+
41+ if ( parsed . username ) {
42+ options . username = parsed . username ;
43+ }
44+
45+ if ( parsed . password ) {
46+ options . password = parsed . password ;
47+ }
48+
49+ if ( parsed . protocol === 'rediss:' ) {
50+ options . tls = { } ;
51+ }
52+
53+ return options ;
54+ } ;
55+
56+ // Returns a bull `createClient(type)` factory so we own the ioredis clients and
57+ // can log their connection lifecycle. Bull creates a few client types per queue
58+ // ('client', 'subscriber', and 'bclient' for blocking ops); we label logs by
59+ // type so it's clear which connection is doing what.
60+ const createRedisClient = function createRedisClient ( redisUrl ) {
61+ const baseOptions = buildRedisOptions ( redisUrl ) ;
62+
63+ console . log ( `[redis] target ${ baseOptions . host } :${ baseOptions . port } ${ baseOptions . tls ? ' (tls)' : '' } ` ) ;
64+
65+ return ( type ) => {
66+ const options = Object . assign ( { } , baseOptions ) ;
67+
68+ // bull requires these for its blocking and subscriber clients.
69+ if ( type === 'bclient' || type === 'subscriber' ) {
70+ options . enableReadyCheck = false ;
71+ options . maxRetriesPerRequest = null ;
72+ }
73+
74+ const client = new Redis ( options ) ;
75+
76+ client . on ( 'connect' , ( ) => console . log ( `[redis:${ type } ] connecting` ) ) ;
77+ client . on ( 'ready' , ( ) => console . log ( `[redis:${ type } ] ready` ) ) ;
78+ client . on ( 'error' , ( error ) => console . error ( `[redis:${ type } ] error: ${ error . message } ` ) ) ;
79+ client . on ( 'close' , ( ) => console . warn ( `[redis:${ type } ] connection closed` ) ) ;
80+ client . on ( 'reconnecting' , ( delay ) => console . warn ( `[redis:${ type } ] reconnecting in ${ delay } ms` ) ) ;
81+ client . on ( 'end' , ( ) => console . warn ( `[redis:${ type } ] connection ended; no further reconnects` ) ) ;
82+
83+ return client ;
84+ } ;
85+ } ;
86+
1287if ( ! process . env . QUEUES ) {
1388 throw new Error ( 'Unable to load queues' ) ;
1489}
@@ -34,9 +109,12 @@ const getUnauthorizedResponse = function getUnauthorizedResponse ( request ) {
34109 return 'No credentials provided' ;
35110} ;
36111
37- if ( process . env . REDIS_URL ) {
38- for ( const queue of queues ) {
39- queue . url = process . env . REDIS_URL ;
112+ for ( const queue of queues ) {
113+ const redisUrl = process . env . REDIS_URL || queue . url ;
114+
115+ if ( redisUrl ) {
116+ queue . createClient = createRedisClient ( redisUrl ) ;
117+ delete queue . url ;
40118 }
41119}
42120
0 commit comments