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