1- import { Injectable , Logger } from '@nestjs/common' ;
1+ import { Injectable , Logger , OnModuleDestroy } from '@nestjs/common' ;
22import { Queue } from 'bullmq' ;
3+ import Redis from 'ioredis' ;
34import { AppConfigService } from '../config/app-config.service' ;
45import { NOTIFICATION_QUEUE , PAYMENT_WEBHOOKS_QUEUE , RECEIPT_PROCESSING_QUEUE } from './jobs.constants' ;
56
@@ -8,15 +9,115 @@ const RETRY_DELAY_MS = 250;
89const MAX_RETRY_DELAY_MS = 1000 ;
910
1011@Injectable ( )
11- export class JobsQueueService {
12+ export class JobsQueueService implements OnModuleDestroy {
1213 private readonly logger = new Logger ( JobsQueueService . name ) ;
13- private readonly notificationQueue : Queue ;
14- private readonly receiptQueue : Queue ;
15- private readonly paymentWebhookQueue : Queue ;
14+ private readonly config : AppConfigService ;
15+ private notificationQueue : Queue | null = null ;
16+ private receiptQueue : Queue | null = null ;
17+ private paymentWebhookQueue : Queue | null = null ;
18+ private redisAvailable : boolean | null = null ;
1619
1720 constructor ( config : AppConfigService ) {
21+ this . config = config ;
22+ }
23+
24+ async enqueueNotification ( payload : {
25+ userIds : string [ ] ;
26+ payload : {
27+ type : 'expense_created' | 'expense_deleted' | 'settlement_created' | 'group_invite' ;
28+ title : string ;
29+ body : string ;
30+ data ?: Record < string , unknown > ;
31+ } ;
32+ } ) : Promise < void > {
33+ const queue = await this . getNotificationQueue ( ) ;
34+ if ( ! queue ) {
35+ return ;
36+ }
37+
38+ try {
39+ await queue . add ( 'send' , payload , {
40+ attempts : 3 ,
41+ backoff : { type : 'exponential' , delay : 500 } ,
42+ removeOnComplete : true ,
43+ removeOnFail : 100 ,
44+ } ) ;
45+ } catch ( error ) {
46+ this . logger . warn ( `Notification queue unavailable: ${ error instanceof Error ? error . message : 'unknown error' } ` ) ;
47+ }
48+ }
49+
50+ async enqueueReceiptProcessing ( payload : { receiptId : string ; expenseId : string } ) : Promise < void > {
51+ const queue = await this . getReceiptQueue ( ) ;
52+ if ( ! queue ) {
53+ return ;
54+ }
55+
56+ try {
57+ await queue . add ( 'process' , payload , {
58+ attempts : 3 ,
59+ removeOnComplete : true ,
60+ removeOnFail : 100 ,
61+ } ) ;
62+ } catch ( error ) {
63+ this . logger . warn ( `Receipt queue unavailable: ${ error instanceof Error ? error . message : 'unknown error' } ` ) ;
64+ }
65+ }
66+
67+ async enqueuePaymentWebhook ( payload : { signature ?: string ; body : Record < string , unknown > } ) : Promise < void > {
68+ const queue = await this . getPaymentWebhookQueue ( ) ;
69+ if ( ! queue ) {
70+ return ;
71+ }
72+
73+ try {
74+ await queue . add ( 'handle' , payload , {
75+ attempts : 5 ,
76+ backoff : { type : 'exponential' , delay : 1000 } ,
77+ removeOnComplete : true ,
78+ removeOnFail : 100 ,
79+ } ) ;
80+ } catch ( error ) {
81+ this . logger . warn ( `Payment webhook queue unavailable: ${ error instanceof Error ? error . message : 'unknown error' } ` ) ;
82+ }
83+ }
84+
85+ async onModuleDestroy ( ) : Promise < void > {
86+ await Promise . allSettled (
87+ [ this . notificationQueue , this . receiptQueue , this . paymentWebhookQueue ]
88+ . filter ( ( queue ) : queue is Queue => Boolean ( queue ) )
89+ . map ( ( queue ) => queue . close ( ) ) ,
90+ ) ;
91+ }
92+
93+ private async getNotificationQueue ( ) : Promise < Queue | null > {
94+ const ready = await this . ensureQueues ( ) ;
95+ return ready ? this . notificationQueue : null ;
96+ }
97+
98+ private async getReceiptQueue ( ) : Promise < Queue | null > {
99+ const ready = await this . ensureQueues ( ) ;
100+ return ready ? this . receiptQueue : null ;
101+ }
102+
103+ private async getPaymentWebhookQueue ( ) : Promise < Queue | null > {
104+ const ready = await this . ensureQueues ( ) ;
105+ return ready ? this . paymentWebhookQueue : null ;
106+ }
107+
108+ private async ensureQueues ( ) : Promise < boolean > {
109+ if ( this . redisAvailable !== null ) {
110+ return this . redisAvailable ;
111+ }
112+
113+ this . redisAvailable = await this . canReachRedis ( ) ;
114+ if ( ! this . redisAvailable ) {
115+ this . logger . warn ( 'Redis unavailable, BullMQ queue producers disabled for local startup' ) ;
116+ return false ;
117+ }
118+
18119 const connection = {
19- url : config . redisUrl ,
120+ url : this . config . redisUrl ,
20121 lazyConnect : true ,
21122 maxRetriesPerRequest : null ,
22123 enableOfflineQueue : false ,
@@ -34,39 +135,27 @@ export class JobsQueueService {
34135 this . notificationQueue = new Queue ( NOTIFICATION_QUEUE , { connection } ) ;
35136 this . receiptQueue = new Queue ( RECEIPT_PROCESSING_QUEUE , { connection } ) ;
36137 this . paymentWebhookQueue = new Queue ( PAYMENT_WEBHOOKS_QUEUE , { connection } ) ;
138+ return true ;
37139 }
38140
39- async enqueueNotification ( payload : {
40- userIds : string [ ] ;
41- payload : {
42- type : 'expense_created' | 'expense_deleted' | 'settlement_created' | 'group_invite' ;
43- title : string ;
44- body : string ;
45- data ?: Record < string , unknown > ;
46- } ;
47- } ) : Promise < void > {
48- await this . notificationQueue . add ( 'send' , payload , {
49- attempts : 3 ,
50- backoff : { type : 'exponential' , delay : 500 } ,
51- removeOnComplete : true ,
52- removeOnFail : 100 ,
53- } ) ;
54- }
55-
56- async enqueueReceiptProcessing ( payload : { receiptId : string ; expenseId : string } ) : Promise < void > {
57- await this . receiptQueue . add ( 'process' , payload , {
58- attempts : 3 ,
59- removeOnComplete : true ,
60- removeOnFail : 100 ,
141+ private async canReachRedis ( ) : Promise < boolean > {
142+ const probe = new Redis ( this . config . redisUrl , {
143+ lazyConnect : true ,
144+ maxRetriesPerRequest : 1 ,
145+ enableOfflineQueue : false ,
146+ connectTimeout : 750 ,
147+ retryStrategy : ( ) => null ,
61148 } ) ;
62- }
63149
64- async enqueuePaymentWebhook ( payload : { signature ?: string ; body : Record < string , unknown > } ) : Promise < void > {
65- await this . paymentWebhookQueue . add ( 'handle' , payload , {
66- attempts : 5 ,
67- backoff : { type : 'exponential' , delay : 1000 } ,
68- removeOnComplete : true ,
69- removeOnFail : 100 ,
70- } ) ;
150+ try {
151+ await probe . connect ( ) ;
152+ await probe . ping ( ) ;
153+ await probe . quit ( ) ;
154+ return true ;
155+ } catch ( error ) {
156+ this . logger . warn ( `Redis queue probe failed: ${ error instanceof Error ? error . message : 'unknown error' } ` ) ;
157+ probe . disconnect ( ) ;
158+ return false ;
159+ }
71160 }
72161}
0 commit comments