fix(cmpp): honor UTC inbox retry leases
This commit is contained in:
@@ -530,6 +530,7 @@ async tryReserveDailySendQuota(applicationId: string, requestedCount: number, re
|
||||
throw new BadRequestException('发送号码数量必须为正整数');
|
||||
}
|
||||
const usageDate = shanghaiDateKey();
|
||||
const usageDateValue = new Date(`${usageDate}T00:00:00.000Z`);
|
||||
const reserve = (client: Pick<Prisma.TransactionClient, '$queryRaw'>) => {
|
||||
const reservationId = randomUUID();
|
||||
return client.$queryRaw<Array<{ tenantId: string; dailyLimit: number; usedCount: number | null }>>(Prisma.sql`
|
||||
@@ -583,7 +584,7 @@ async tryReserveDailySendQuota(applicationId: string, requestedCount: number, re
|
||||
reservationKey: normalizedReservationKey,
|
||||
tenantId: row.tenantId,
|
||||
applicationId,
|
||||
usageDate,
|
||||
usageDate: usageDateValue,
|
||||
requestedCount,
|
||||
dailyLimit: Number(row.dailyLimit),
|
||||
usedCount: row.usedCount == null ? null : Number(row.usedCount),
|
||||
|
||||
@@ -336,6 +336,14 @@ function createPrismaMock() {
|
||||
count: jest.fn().mockResolvedValue(0),
|
||||
findFirst: jest.fn().mockResolvedValue(null),
|
||||
},
|
||||
smsApplicationDailyReservation: {
|
||||
findUnique: jest.fn().mockResolvedValue(null),
|
||||
create: jest.fn().mockResolvedValue({ id: 'daily-reservation-1' }),
|
||||
},
|
||||
phoneFrequencyReservation: {
|
||||
findUnique: jest.fn().mockResolvedValue(null),
|
||||
create: jest.fn().mockResolvedValue({ id: 'frequency-reservation-1' }),
|
||||
},
|
||||
smsBillingRecord: {
|
||||
findFirst: jest.fn().mockResolvedValue(null),
|
||||
create: jest.fn().mockResolvedValue({ id: 'bill-1' }),
|
||||
@@ -2194,6 +2202,34 @@ describe('SendChainService', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('persists an idempotent daily quota reservation with a Prisma Date value', async () => {
|
||||
const { service, prisma } = createService();
|
||||
prisma.$queryRaw.mockResolvedValueOnce([{ tenantId: 'tenant-1', dailyLimit: 100000, usedCount: 1 }]);
|
||||
|
||||
await expect((service as any).tryReserveDailySendQuota('app-1', 1, 'workflow-1:daily-quota'))
|
||||
.resolves.toEqual({ dailyLimit: 100000, usedCount: 1, reserved: true });
|
||||
|
||||
expect(prisma.smsApplicationDailyReservation.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
reservationKey: 'workflow-1:daily-quota',
|
||||
usageDate: expect.any(Date),
|
||||
}),
|
||||
});
|
||||
expect(prisma.smsApplicationDailyReservation.create.mock.calls[0][0].data.usageDate.toISOString())
|
||||
.toMatch(/^\d{4}-\d{2}-\d{2}T00:00:00\.000Z$/);
|
||||
});
|
||||
|
||||
it('claims Inbox leases against UTC for timestamp-without-time-zone columns', async () => {
|
||||
const { service, prisma } = createService();
|
||||
prisma.$queryRaw.mockResolvedValueOnce([]);
|
||||
|
||||
await (service as any).submission.inboundEntry.claimInboundWorkflows(5);
|
||||
|
||||
const sql = prisma.$queryRaw.mock.calls[0][0].strings.join(' ');
|
||||
expect(sql).toContain("NOW() AT TIME ZONE 'UTC'");
|
||||
expect(sql).toContain('FOR UPDATE SKIP LOCKED');
|
||||
});
|
||||
|
||||
it('enqueues a freshly persisted inbound message without querying the task and message again', async () => {
|
||||
const { service, prisma } = createService();
|
||||
const add = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
@@ -1086,10 +1086,10 @@ startInboundWorkflowWorker() {
|
||||
FROM "CmppInboundSubmissionInbox"
|
||||
WHERE (
|
||||
status = 'pending'
|
||||
AND "nextAttemptAt" <= NOW()
|
||||
AND "nextAttemptAt" <= (NOW() AT TIME ZONE 'UTC')
|
||||
) OR (
|
||||
status = 'processing'
|
||||
AND "lockedAt" <= NOW() - make_interval(secs => ${staleSeconds})
|
||||
AND "lockedAt" <= (NOW() AT TIME ZONE 'UTC') - make_interval(secs => ${staleSeconds})
|
||||
)
|
||||
-- Priority applications enter the same durable Inbox, but are claimed first while
|
||||
-- preserving FIFO within each class. This keeps the V5 priority contract effective
|
||||
@@ -1101,9 +1101,9 @@ startInboundWorkflowWorker() {
|
||||
UPDATE "CmppInboundSubmissionInbox" AS inbox
|
||||
SET status = 'processing',
|
||||
attempts = inbox.attempts + 1,
|
||||
"lockedAt" = NOW(),
|
||||
"lockedAt" = (NOW() AT TIME ZONE 'UTC'),
|
||||
"lockedBy" = ${this.inboundWorkflowWorkerId},
|
||||
"updatedAt" = NOW()
|
||||
"updatedAt" = (NOW() AT TIME ZONE 'UTC')
|
||||
FROM candidates
|
||||
WHERE inbox.id = candidates.id
|
||||
RETURNING inbox.id, inbox."requestKey", inbox."applicationId", inbox.attempts, inbox.payload
|
||||
|
||||
Reference in New Issue
Block a user