perf: expand gateway capacity and prevent receipt replay

This commit is contained in:
hectorzhao
2026-08-25 16:08:30 +08:00
parent 761c123b65
commit 9292352be1
48 changed files with 2001 additions and 144 deletions
@@ -68,6 +68,51 @@ export class GatewayCallbackController {
return this.sendChain.recordGatewaySubmitDeadLetter(body);
}
@Post('gateway/events/batch')
async batch(@Body() body: {
batchId?: string;
gatewayInstanceId?: string;
createdAt?: string;
events?: Array<{ eventId?: string; type?: string; payload?: Record<string, unknown> }>;
}) {
if (!body.batchId || !body.gatewayInstanceId || !Array.isArray(body.events) || body.events.length < 1 || body.events.length > 100) {
throw new BadRequestException('batchId, gatewayInstanceId and 1 to 100 events are required');
}
if (Buffer.byteLength(JSON.stringify(body), 'utf8') > 1024 * 1024) {
throw new BadRequestException('Gateway callback batch exceeds 1MB');
}
const seen = new Set<string>();
const results: Array<{ eventId: string; accepted: boolean; retryable?: boolean; errorCode?: string }> = [];
for (let offset = 0; offset < body.events.length; offset += 25) {
results.push(...await Promise.all(body.events.slice(offset, offset + 25).map(async (event) => {
const eventId = String(event.eventId ?? '').trim();
if (!eventId || seen.has(eventId) || !event.payload || typeof event.payload !== 'object') {
return { eventId, accepted: false, retryable: false, errorCode: seen.has(eventId) ? 'DUPLICATE_EVENT_ID' : 'INVALID_EVENT' };
}
seen.add(eventId);
try {
await this.dispatchBatchEvent(String(event.type ?? ''), { ...event.payload, eventId });
return { eventId, accepted: true };
} catch (error) {
const invalid = error instanceof BadRequestException;
return { eventId, accepted: false, retryable: !invalid, errorCode: invalid ? 'INVALID_EVENT' : 'PROCESSING_FAILED' };
}
})));
}
return { batchId: body.batchId, results };
}
private dispatchBatchEvent(type: string, payload: Record<string, unknown>) {
switch (type) {
case 'submit_result': return this.sendChain.handleSubmitResult(payload as unknown as GatewaySubmitResultDto);
case 'submit_segment_result': return this.sendChain.handleSubmitSegmentResult(payload as unknown as GatewaySubmitSegmentResultDto);
case 'receipt_intake': return this.track('deliver_receipt', payload, () => this.sendChain.intakeReceipt(payload as unknown as GatewayReceiptEventDto));
case 'uplink': return this.track('deliver_uplink', payload, () => this.sendChain.handleUplink(payload as unknown as GatewayUplinkEventDto));
case 'dead_letter': return this.sendChain.recordGatewaySubmitDeadLetter(payload as unknown as GatewaySubmitDeadLetterDto);
default: throw new BadRequestException(`Unsupported batch event type ${type}`);
}
}
private async track<T>(eventType: string, body: object, action: () => Promise<T> | T) {
const startedAt = Date.now();
const value = body as Record<string, unknown>;
@@ -83,7 +128,7 @@ export class GatewayCallbackController {
try {
const result = await action();
const resolved = result && typeof result === 'object' ? result as Record<string, unknown> : {};
this.protocolLogs.record({
if (process.env.PROTOCOL_LOG_CALLBACK_TRACKING_ENABLED === 'true') this.protocolLogs.record({
...common,
tenantId: (resolved.tenantId ?? common.tenantId) as string,
applicationId: (resolved.applicationId ?? common.applicationId) as string,
@@ -92,7 +137,7 @@ export class GatewayCallbackController {
});
return result;
} catch (error) {
this.protocolLogs.record({
if (process.env.PROTOCOL_LOG_CALLBACK_TRACKING_ENABLED === 'true') this.protocolLogs.record({
...common, status: 'failed', durationMs: Date.now() - startedAt,
detail: { error: error instanceof Error ? error.message : String(error) },
});