perf: batch inbound workflows by tenant

This commit is contained in:
hectorzhao
2026-08-25 17:11:55 +08:00
parent d3ceeb1e16
commit 76e7c8c401
9 changed files with 184 additions and 25 deletions
+70 -1
View File
@@ -2449,11 +2449,80 @@ describe('SendChainService', () => {
const { service, prisma } = createService();
prisma.$queryRaw.mockResolvedValueOnce([]);
await (service as any).submission.inboundEntry.claimInboundWorkflows(5);
await (service as any).submission.inboundEntry.claimInboundWorkflows(5, ['tenant-busy']);
const sql = prisma.$queryRaw.mock.calls[0][0].strings.join(' ');
expect(sql).toContain("NOW() AT TIME ZONE 'UTC'");
expect(sql).toContain('"tenantId" NOT IN');
expect(prisma.$queryRaw.mock.calls[0][0].values).toContain('tenant-busy');
expect(sql).toContain('FOR UPDATE SKIP LOCKED');
expect(sql).toContain('inbox."tenantId"');
});
it('groups Inbox work by tenant and keeps chunks for one tenant serial', async () => {
const { service } = createService();
const inboundEntry = (service as any).submission.inboundEntry;
const items = [
{ id: 'a1', tenantId: 'tenant-a', applicationId: 'app-1' },
{ id: 'b1', tenantId: 'tenant-b', applicationId: 'app-2' },
{ id: 'a2', tenantId: 'tenant-a', applicationId: 'app-1' },
{ id: 'a3', tenantId: 'tenant-a', applicationId: 'app-1' },
];
const grouped = inboundEntry.groupInboundWorkflowsByTenant(items);
expect([...grouped.keys()]).toEqual(['tenant-a', 'tenant-b']);
expect(grouped.get('tenant-a').map((item: { id: string }) => item.id)).toEqual(['a1', 'a2', 'a3']);
inboundEntry.processClaimedInboundWorkflowBatch = jest.fn().mockResolvedValue(undefined);
await inboundEntry.processTenantInboundWorkflowBatches(grouped.get('tenant-a'), 2, new Map());
expect(inboundEntry.processClaimedInboundWorkflowBatch.mock.calls.map((call: unknown[]) => (
(call[0] as Array<{ id: string }>).map((item) => item.id)
))).toEqual([['a1', 'a2'], ['a3']]);
});
it('coalesces a small ready Inbox set before claiming the next tenant batch', async () => {
const previousWait = process.env.API_INBOUND_WORKFLOW_BATCH_WAIT_MS;
const previousTarget = process.env.API_INBOUND_WORKFLOW_TARGET_BATCH_SIZE;
process.env.API_INBOUND_WORKFLOW_BATCH_WAIT_MS = '5';
process.env.API_INBOUND_WORKFLOW_TARGET_BATCH_SIZE = '32';
try {
const { service, prisma } = createService();
prisma.$queryRaw.mockResolvedValueOnce([{ count: 3n }]);
const startedAt = Date.now();
await (service as any).submission.inboundEntry.waitForInboundWorkflowMicroBatch(96, ['tenant-busy']);
expect(Date.now() - startedAt).toBeGreaterThanOrEqual(4);
const query = prisma.$queryRaw.mock.calls[0][0];
expect(query.strings.join(' ')).toContain('COUNT(*)::bigint');
expect(query.values).toContain('tenant-busy');
} finally {
if (previousWait == null) delete process.env.API_INBOUND_WORKFLOW_BATCH_WAIT_MS;
else process.env.API_INBOUND_WORKFLOW_BATCH_WAIT_MS = previousWait;
if (previousTarget == null) delete process.env.API_INBOUND_WORKFLOW_TARGET_BATCH_SIZE;
else process.env.API_INBOUND_WORKFLOW_TARGET_BATCH_SIZE = previousTarget;
}
});
it('uses UTC for Submit Outbox claim, lease, publish, and retry timestamps', async () => {
const previousShadow = process.env.SEND_SUBMIT_OUTBOX_SHADOW_ENABLED;
const previousPublish = process.env.SEND_SUBMIT_OUTBOX_PUBLISH_ENABLED;
process.env.SEND_SUBMIT_OUTBOX_SHADOW_ENABLED = 'true';
delete process.env.SEND_SUBMIT_OUTBOX_PUBLISH_ENABLED;
try {
const { service, prisma } = createService();
prisma.$queryRaw.mockResolvedValueOnce([{ id: 'outbox-1', submitId: 'SUB-1', payload: { messageId: 'MSG-1' } }]);
const gatewaySubmit = (service as any).submission.gatewaySubmit;
await gatewaySubmit.publishSubmitOutboxBatch();
const claimSql = prisma.$queryRaw.mock.calls[0][0].strings.join(' ');
const publishSql = prisma.$executeRaw.mock.calls.at(-1)[0].strings.join(' ');
expect(claimSql).toContain("NOW() AT TIME ZONE 'UTC'");
expect(publishSql).toContain("NOW() AT TIME ZONE 'UTC'");
expect(`${claimSql} ${publishSql}`).not.toContain('CURRENT_TIMESTAMP');
} finally {
if (previousShadow == null) delete process.env.SEND_SUBMIT_OUTBOX_SHADOW_ENABLED;
else process.env.SEND_SUBMIT_OUTBOX_SHADOW_ENABLED = previousShadow;
if (previousPublish == null) delete process.env.SEND_SUBMIT_OUTBOX_PUBLISH_ENABLED;
else process.env.SEND_SUBMIT_OUTBOX_PUBLISH_ENABLED = previousPublish;
}
});
it('enqueues a freshly persisted inbound message without querying the task and message again', async () => {