Browse Source
Merge pull request #61 from vernu/job-queue
Merge pull request #61 from vernu/job-queue
Implement optional job queue for sending sms with delaypull/64/head
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 851 additions and 161 deletions
-
4api/.env.example
-
6api/package.json
-
536api/pnpm-lock.yaml
-
43api/src/app.module.ts
-
21api/src/gateway/gateway.module.ts
-
202api/src/gateway/gateway.service.ts
-
102api/src/gateway/queue/sms-queue.processor.ts
-
66api/src/gateway/queue/sms-queue.service.ts
-
15api/src/gateway/schemas/sms-batch.schema.ts
-
7api/src/gateway/schemas/sms.schema.ts
536
api/pnpm-lock.yaml
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,102 @@ |
|||||
|
import { Process, Processor } from '@nestjs/bull' |
||||
|
import { InjectModel } from '@nestjs/mongoose' |
||||
|
import { Job } from 'bull' |
||||
|
import { Model } from 'mongoose' |
||||
|
import * as firebaseAdmin from 'firebase-admin' |
||||
|
import { Device } from '../schemas/device.schema' |
||||
|
import { SMS } from '../schemas/sms.schema' |
||||
|
import { SMSBatch } from '../schemas/sms-batch.schema' |
||||
|
import { WebhookService } from 'src/webhook/webhook.service' |
||||
|
import { Logger } from '@nestjs/common' |
||||
|
|
||||
|
@Processor('sms') |
||||
|
export class SmsQueueProcessor { |
||||
|
private readonly logger = new Logger(SmsQueueProcessor.name) |
||||
|
|
||||
|
constructor( |
||||
|
@InjectModel(Device.name) private deviceModel: Model<Device>, |
||||
|
@InjectModel(SMS.name) private smsModel: Model<SMS>, |
||||
|
@InjectModel(SMSBatch.name) private smsBatchModel: Model<SMSBatch>, |
||||
|
private webhookService: WebhookService, |
||||
|
) {} |
||||
|
|
||||
|
@Process({ |
||||
|
name: 'send-sms', |
||||
|
concurrency: 10, |
||||
|
}) |
||||
|
async handleSendSms(job: Job<any>) { |
||||
|
this.logger.debug(`Processing send-sms job ${job.id}`) |
||||
|
const { deviceId, fcmMessages, smsBatchId } = job.data |
||||
|
|
||||
|
try { |
||||
|
this.smsBatchModel |
||||
|
.findByIdAndUpdate(smsBatchId, { |
||||
|
$set: { status: 'processing' }, |
||||
|
}) |
||||
|
.exec() |
||||
|
.catch((error) => { |
||||
|
this.logger.error( |
||||
|
`Failed to update sms batch status to processing ${smsBatchId}`, |
||||
|
error, |
||||
|
) |
||||
|
throw error |
||||
|
}) |
||||
|
|
||||
|
const response = await firebaseAdmin.messaging().sendEach(fcmMessages) |
||||
|
|
||||
|
this.logger.debug( |
||||
|
`SMS Job ${job.id} completed, success: ${response.successCount}, failures: ${response.failureCount}`, |
||||
|
) |
||||
|
|
||||
|
// Update device SMS count
|
||||
|
await this.deviceModel |
||||
|
.findByIdAndUpdate(deviceId, { |
||||
|
$inc: { sentSMSCount: response.successCount }, |
||||
|
}) |
||||
|
.exec() |
||||
|
|
||||
|
// Update batch status
|
||||
|
const smsBatch = await this.smsBatchModel.findByIdAndUpdate( |
||||
|
smsBatchId, |
||||
|
{ |
||||
|
$inc: { |
||||
|
successCount: response.successCount, |
||||
|
failureCount: response.failureCount, |
||||
|
}, |
||||
|
}, |
||||
|
{ returnDocument: 'after' }, |
||||
|
) |
||||
|
|
||||
|
if (smsBatch.successCount === smsBatch.recipientCount) { |
||||
|
await this.smsBatchModel.findByIdAndUpdate(smsBatchId, { |
||||
|
$set: { status: 'completed' }, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
return response |
||||
|
} catch (error) { |
||||
|
this.logger.error(`Failed to process SMS job ${job.id}`, error) |
||||
|
|
||||
|
const smsBatch = await this.smsBatchModel.findByIdAndUpdate( |
||||
|
smsBatchId, |
||||
|
{ |
||||
|
$inc: { |
||||
|
failureCount: fcmMessages.length, |
||||
|
}, |
||||
|
}, |
||||
|
{ returnDocument: 'after' }, |
||||
|
) |
||||
|
|
||||
|
const newStatus = |
||||
|
smsBatch.failureCount === smsBatch.recipientCount |
||||
|
? 'failed' |
||||
|
: 'partial_success' |
||||
|
|
||||
|
await this.smsBatchModel.findByIdAndUpdate(smsBatchId, { |
||||
|
$set: { status: newStatus }, |
||||
|
}) |
||||
|
|
||||
|
throw error |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
import { Injectable, Logger } from '@nestjs/common' |
||||
|
import { InjectQueue } from '@nestjs/bull' |
||||
|
import { Queue } from 'bull' |
||||
|
import { ConfigService } from '@nestjs/config' |
||||
|
import { Message } from 'firebase-admin/messaging' |
||||
|
|
||||
|
@Injectable() |
||||
|
export class SmsQueueService { |
||||
|
private readonly logger = new Logger(SmsQueueService.name) |
||||
|
private readonly useSmsQueue: boolean |
||||
|
private readonly maxSmsBatchSize: number |
||||
|
|
||||
|
constructor( |
||||
|
@InjectQueue('sms') private readonly smsQueue: Queue, |
||||
|
private readonly configService: ConfigService, |
||||
|
) { |
||||
|
this.useSmsQueue = this.configService.get<boolean>('USE_SMS_QUEUE', false) |
||||
|
this.maxSmsBatchSize = this.configService.get<number>( |
||||
|
'MAX_SMS_BATCH_SIZE', |
||||
|
5, |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if queue is enabled based on environment variable |
||||
|
*/ |
||||
|
isQueueEnabled(): boolean { |
||||
|
return this.useSmsQueue |
||||
|
} |
||||
|
|
||||
|
async addSendSmsJob( |
||||
|
deviceId: string, |
||||
|
fcmMessages: Message[], |
||||
|
smsBatchId: string, |
||||
|
) { |
||||
|
this.logger.debug(`Adding send-sms job for batch ${smsBatchId}`) |
||||
|
|
||||
|
// Split messages into batches of max smsBatchSize messages
|
||||
|
const batches = [] |
||||
|
for (let i = 0; i < fcmMessages.length; i += this.maxSmsBatchSize) { |
||||
|
batches.push(fcmMessages.slice(i, i + this.maxSmsBatchSize)) |
||||
|
} |
||||
|
|
||||
|
for (const batch of batches) { |
||||
|
await this.smsQueue.add( |
||||
|
'send-sms', |
||||
|
{ |
||||
|
deviceId, |
||||
|
fcmMessages: batch, |
||||
|
smsBatchId, |
||||
|
}, |
||||
|
{ |
||||
|
priority: 1, // TODO: Make this dynamic based on users subscription plan
|
||||
|
attempts: 1, |
||||
|
delay: 1000, // 1 second
|
||||
|
backoff: { |
||||
|
type: 'exponential', |
||||
|
delay: 5000, // 5 seconds
|
||||
|
}, |
||||
|
removeOnComplete: false, |
||||
|
removeOnFail: false, |
||||
|
}, |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue