perf(cmpp): add durable inbound fast path
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { BadRequestException, HttpException, HttpStatus, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { BadRequestException, ConflictException, HttpException, HttpStatus, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { Queue, Worker } from 'bullmq';
|
||||
import IORedis from 'ioredis';
|
||||
@@ -525,15 +525,16 @@ async reserveDailySendQuota(applicationId: string, requestedCount: number) {
|
||||
return result;
|
||||
}
|
||||
|
||||
async tryReserveDailySendQuota(applicationId: string, requestedCount: number) {
|
||||
async tryReserveDailySendQuota(applicationId: string, requestedCount: number, reservationKey?: string) {
|
||||
if (!Number.isInteger(requestedCount) || requestedCount <= 0) {
|
||||
throw new BadRequestException('发送号码数量必须为正整数');
|
||||
}
|
||||
const usageDate = shanghaiDateKey();
|
||||
const reservationId = randomUUID();
|
||||
const rows = await this.prisma.$queryRaw<Array<{ dailyLimit: number; usedCount: number | null }>>(Prisma.sql`
|
||||
const reserve = (client: Pick<Prisma.TransactionClient, '$queryRaw'>) => {
|
||||
const reservationId = randomUUID();
|
||||
return client.$queryRaw<Array<{ tenantId: string; dailyLimit: number; usedCount: number | null }>>(Prisma.sql`
|
||||
WITH application_limit AS (
|
||||
SELECT id, COALESCE("dailyLimit", 100000)::integer AS "dailyLimit"
|
||||
SELECT id, "tenantId", COALESCE("dailyLimit", 100000)::integer AS "dailyLimit"
|
||||
FROM "SmsApplication"
|
||||
WHERE id = ${applicationId}
|
||||
), reservation AS (
|
||||
@@ -550,10 +551,49 @@ async tryReserveDailySendQuota(applicationId: string, requestedCount: number) {
|
||||
<= (SELECT "dailyLimit" FROM application_limit)
|
||||
RETURNING "usedCount"
|
||||
)
|
||||
SELECT application_limit."dailyLimit", reservation."usedCount"
|
||||
SELECT application_limit."tenantId", application_limit."dailyLimit", reservation."usedCount"
|
||||
FROM application_limit
|
||||
LEFT JOIN reservation ON TRUE
|
||||
`);
|
||||
`);
|
||||
};
|
||||
const normalizedReservationKey = reservationKey?.trim();
|
||||
const rows = normalizedReservationKey
|
||||
? await this.prisma.$transaction(async (tx) => {
|
||||
// The quota increment and its idempotency record share one short transaction. A worker
|
||||
// crash can therefore neither lose a successful reservation nor increment it twice.
|
||||
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${'daily-quota:' + normalizedReservationKey}, 0))`;
|
||||
const existing = await tx.smsApplicationDailyReservation.findUnique({
|
||||
where: { reservationKey: normalizedReservationKey },
|
||||
});
|
||||
if (existing) {
|
||||
if (existing.applicationId !== applicationId || existing.requestedCount !== requestedCount) {
|
||||
throw new ConflictException('日发送配额幂等键已用于另一笔预留');
|
||||
}
|
||||
return [{
|
||||
tenantId: existing.tenantId,
|
||||
dailyLimit: existing.dailyLimit,
|
||||
usedCount: existing.usedCount,
|
||||
}];
|
||||
}
|
||||
const reservedRows = await reserve(tx);
|
||||
if (reservedRows.length > 0) {
|
||||
const row = reservedRows[0];
|
||||
await tx.smsApplicationDailyReservation.create({
|
||||
data: {
|
||||
reservationKey: normalizedReservationKey,
|
||||
tenantId: row.tenantId,
|
||||
applicationId,
|
||||
usageDate,
|
||||
requestedCount,
|
||||
dailyLimit: Number(row.dailyLimit),
|
||||
usedCount: row.usedCount == null ? null : Number(row.usedCount),
|
||||
reserved: row.usedCount != null,
|
||||
},
|
||||
});
|
||||
}
|
||||
return reservedRows;
|
||||
})
|
||||
: await reserve(this.prisma);
|
||||
if (rows.length === 0) {
|
||||
throw new NotFoundException('短信应用不存在');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user