fix: ack protocol logs after complete flush

This commit is contained in:
hectorzhao
2026-08-30 21:54:33 +08:00
parent 67774509d9
commit 1a5063a5d9
3 changed files with 74 additions and 23 deletions
@@ -81,13 +81,53 @@ describe('ProtocolLogsService', () => {
const service = new ProtocolLogsService(prisma as never);
await service.list({ createdAtFrom: '2026-08-21', createdAtTo: '2026-08-27' });
expect(prisma.protocolInteractionLog.findMany).toHaveBeenCalledWith(expect.objectContaining({
where: expect.objectContaining({
createdAt: {
gte: new Date('2026-08-20T16:00:00.000Z'),
lte: new Date('2026-08-27T15:59:59.999Z'),
},
expect(prisma.protocolInteractionLog.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
createdAt: {
gte: new Date('2026-08-20T16:00:00.000Z'),
lte: new Date('2026-08-27T15:59:59.999Z'),
},
}),
}),
}));
);
});
it('waits for an active flush and persists every buffered batch before reporting success', async () => {
const previousBatchSize = process.env.PROTOCOL_LOG_BATCH_SIZE;
process.env.PROTOCOL_LOG_BATCH_SIZE = '2';
let releaseFirstBatch!: (value: { count: number }) => void;
prisma.protocolInteractionLog.createMany
.mockImplementationOnce(
() =>
new Promise((resolve) => {
releaseFirstBatch = resolve;
}),
)
.mockResolvedValue({ count: 1 });
try {
const service = new ProtocolLogsService(prisma as never);
service.recordMany(
[1, 2, 3].map((sequenceId) => ({
protocol: 'cmpp' as const,
direction: 'channel_to_platform' as const,
eventType: 'deliver_receipt',
status: 'success' as const,
detail: { sequenceId },
})),
);
const completed = service.flushNow();
expect(prisma.protocolInteractionLog.createMany).toHaveBeenCalledTimes(1);
releaseFirstBatch({ count: 2 });
await expect(completed).resolves.toBe(true);
expect(prisma.protocolInteractionLog.createMany).toHaveBeenCalledTimes(2);
expect(prisma.protocolInteractionLog.createMany.mock.calls.flatMap(([input]) => input.data)).toHaveLength(3);
} finally {
if (previousBatchSize === undefined) delete process.env.PROTOCOL_LOG_BATCH_SIZE;
else process.env.PROTOCOL_LOG_BATCH_SIZE = previousBatchSize;
}
});
});
+26 -16
View File
@@ -47,7 +47,7 @@ export class ProtocolLogsService implements OnModuleInit, OnModuleDestroy {
private readonly buffer: Prisma.ProtocolInteractionLogCreateManyInput[] = [];
private flushTimer?: ReturnType<typeof setInterval>;
private retentionTimer?: ReturnType<typeof setInterval>;
private flushing = false;
private flushPromise?: Promise<boolean>;
constructor(private readonly prisma: PrismaService) {}
@@ -142,22 +142,32 @@ export class ProtocolLogsService implements OnModuleInit, OnModuleDestroy {
return { items, total, page, pageSize, eventTypes: eventTypes.map((item) => item.eventType) };
}
private async flush(): Promise<boolean> {
if (this.flushing) return false;
if (this.buffer.length === 0) return true;
this.flushing = true;
const batch = this.buffer.splice(0, positiveEnv('PROTOCOL_LOG_BATCH_SIZE', 100));
try {
await this.prisma.protocolInteractionLog.createMany({ data: batch });
} catch (error) {
this.buffer.unshift(...batch);
this.logger.error(`Protocol log batch write failed (${batch.length} events)`, error instanceof Error ? error.stack : String(error));
return false;
} finally {
this.flushing = false;
if (this.buffer.length > 0) setImmediate(() => void this.flush());
private flush(): Promise<boolean> {
if (this.flushPromise) return this.flushPromise;
if (this.buffer.length === 0) return Promise.resolve(true);
const flushPromise = this.flushBufferedBatches().finally(() => {
if (this.flushPromise === flushPromise) this.flushPromise = undefined;
});
this.flushPromise = flushPromise;
return flushPromise;
}
private async flushBufferedBatches(): Promise<boolean> {
while (this.buffer.length > 0) {
const batch = this.buffer.splice(0, positiveEnv('PROTOCOL_LOG_BATCH_SIZE', 100));
try {
await this.prisma.protocolInteractionLog.createMany({ data: batch });
} catch (error) {
this.buffer.unshift(...batch);
this.logger.error(
`Protocol log batch write failed (${batch.length} events)`,
error instanceof Error ? error.stack : String(error),
);
return false;
}
}
return true;
return true;
}
private async purgeExpired() {