perf: batch gateway submits and isolate callbacks

This commit is contained in:
hectorzhao
2026-08-25 11:39:59 +08:00
parent c6f11014d6
commit 761c123b65
29 changed files with 1912 additions and 193 deletions
@@ -0,0 +1,102 @@
import { BadRequestException, Body, Controller, Get, Post } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { ProtocolLogsService, type ProtocolLogInput } from '../protocol-logs/protocol-logs.service';
import type {
GatewayReceiptEventDto,
GatewaySubmitDeadLetterDto,
GatewaySubmitResultDto,
GatewaySubmitSegmentResultDto,
GatewayUplinkEventDto,
} from './send-chain.contracts';
import { SendChainService } from './send-chain.service';
@Controller()
export class GatewayCallbackController {
constructor(
private readonly sendChain: SendChainService,
private readonly protocolLogs: ProtocolLogsService,
private readonly prisma: PrismaService,
) {}
@Get('health')
async health() {
await this.prisma.$queryRaw`SELECT 1`;
return { status: 'ok', role: 'gateway-callback', databasePool: this.prisma.getPoolState() };
}
@Post('gateway/events/submit-result')
submitResult(@Body() body: GatewaySubmitResultDto) {
return this.sendChain.handleSubmitResult(body);
}
@Post('gateway/events/submit-segment-result')
submitSegmentResult(@Body() body: GatewaySubmitSegmentResultDto) {
return this.sendChain.handleSubmitSegmentResult(body);
}
@Post('gateway/events/receipt/intake')
receiptIntake(@Body() body: GatewayReceiptEventDto) {
return this.track('deliver_receipt', body, () => this.sendChain.intakeReceipt(body));
}
@Post('gateway/events/receipt')
receipt(@Body() body: GatewayReceiptEventDto) {
return this.track('deliver_receipt', body, () => this.sendChain.handleReceipt(body));
}
@Post('gateway/events/uplink')
uplink(@Body() body: GatewayUplinkEventDto) {
return this.track('deliver_uplink', body, () => this.sendChain.handleUplink(body));
}
@Post('gateway/events/protocol-log')
protocolLog(@Body() body: ProtocolLogInput) {
const allowedPacket = (
body.direction === 'platform_to_channel' && ['submit', 'deliver_resp'].includes(body.eventType)
) || (
body.direction === 'channel_to_platform' && body.eventType === 'submit_resp'
);
if (body.protocol !== 'cmpp' || !allowedPacket || !['success', 'failed'].includes(body.status)) {
throw new BadRequestException('Unsupported Gateway callback protocol log event');
}
this.protocolLogs.record(body);
return { accepted: true };
}
@Post('gateway/events/dead-letter')
deadLetter(@Body() body: GatewaySubmitDeadLetterDto) {
return this.sendChain.recordGatewaySubmitDeadLetter(body);
}
private async track<T>(eventType: string, body: object, action: () => Promise<T> | T) {
const startedAt = Date.now();
const value = body as Record<string, unknown>;
const common: Omit<ProtocolLogInput, 'status'> = {
protocol: 'cmpp', direction: 'channel_to_platform', eventType,
tenantId: value.tenantId as string, applicationId: value.applicationId as string,
channelId: value.channelId as string,
messageId: (value.messageId ?? value.platformMessageId) as string,
gatewayMessageId: (value.gatewayMessageId ?? value.msgId ?? value.upstreamMessageId) as string,
phone: (value.phoneNumber ?? value.srcTerminalId) as string,
resultCode: (value.rawStatus ?? value.status ?? value.stat) as string,
};
try {
const result = await action();
const resolved = result && typeof result === 'object' ? result as Record<string, unknown> : {};
this.protocolLogs.record({
...common,
tenantId: (resolved.tenantId ?? common.tenantId) as string,
applicationId: (resolved.applicationId ?? common.applicationId) as string,
messageId: (resolved.messageId ?? common.messageId) as string,
status: 'success', durationMs: Date.now() - startedAt,
});
return result;
} catch (error) {
this.protocolLogs.record({
...common, status: 'failed', durationMs: Date.now() - startedAt,
detail: { error: error instanceof Error ? error.message : String(error) },
});
throw error;
}
}
}