feat: complete cmpp platform phases 0-5

This commit is contained in:
hectorzhao
2026-07-01 13:22:04 +08:00
parent 824a8b334f
commit ee926fea04
86 changed files with 10490 additions and 14 deletions
+215
View File
@@ -0,0 +1,215 @@
import { Queue, Worker } from 'bullmq';
import IORedis from 'ioredis';
import { randomUUID } from 'node:crypto';
import { performance } from 'node:perf_hooks';
const connection = new IORedis({
host: process.env.REDIS_HOST ?? '127.0.0.1',
port: Number(process.env.REDIS_PORT ?? 6379),
maxRetriesPerRequest: null,
});
const submitQueueName = 'cmpp.submit.commands';
const submitResultQueueName = 'cmpp.submit.results';
const receiptQueueName = 'cmpp.receipt.events';
const submitQueue = new Queue(submitQueueName, { connection });
const submitResultQueue = new Queue(submitResultQueueName, { connection });
const receiptQueue = new Queue(receiptQueueName, { connection });
const messageCount = Number(process.env.SPIKE_MESSAGE_COUNT ?? 15000);
const concurrency = Number(process.env.SPIKE_CONCURRENCY ?? 500);
function createSubmitCommand(index) {
const padded = String(index).padStart(6, '0');
const now = new Date().toISOString();
return {
schemaVersion: 'v1',
messageType: 'SubmitCommand',
traceId: `trace-bullmq-${padded}`,
messageId: `msg-bullmq-${padded}`,
channelId: 'sms-channel-cmpp-spike',
createdAt: now,
tenantId: 'tenant-spike',
applicationId: 'app-spike',
taskId: 'task-bullmq-spike',
submitId: `submit-bullmq-${padded}`,
phoneNumber: '13800138000',
content: '您的验证码为 1234565 分钟内有效。',
signature: '测试平台',
templateId: 'tpl-spike',
billingUnits: 1,
route: {
channelCode: 'CMCC-CMPP-SPIKE',
cmppAccountCode: 'cmpp-account-spike',
priority: 10,
rateLimitPerSecond: 500,
},
cmpp: {
serviceId: 'CMPP',
srcId: '106900000000',
registeredDelivery: 1,
msgFmt: 15,
feeUserType: 2,
feeCode: '0',
feeType: '01',
},
retry: {
attempt: 0,
maxAttempts: 3,
},
};
}
async function cleanQueues() {
for (const queue of [submitQueue, submitResultQueue, receiptQueue]) {
await queue.drain(true);
await queue.obliterate({ force: true });
}
}
async function closeAll(workers) {
await Promise.all(workers.map((worker) => worker.close()));
await Promise.all([submitQueue.close(), submitResultQueue.close(), receiptQueue.close()]);
await connection.quit();
}
let submitResults = 0;
let receiptEvents = 0;
const gatewayWorker = new Worker(
submitQueueName,
async (job) => {
const cmd = job.data;
const sequenceId = Number(job.id.replace(/\D/g, '').slice(-9)) || job.attemptsMade + 1;
const gatewayMessageId = `gw-${cmd.messageId}`;
const now = new Date().toISOString();
await submitResultQueue.add(
'SubmitResult',
{
schemaVersion: 'v1',
messageType: 'SubmitResult',
traceId: cmd.traceId,
messageId: cmd.messageId,
channelId: cmd.channelId,
createdAt: now,
sequenceId,
gatewayMessageId,
submitStatus: 'accepted',
submittedAt: now,
},
{ jobId: `submit-result-${cmd.messageId}` },
);
await receiptQueue.add(
'ReceiptEvent',
{
schemaVersion: 'v1',
messageType: 'ReceiptEvent',
traceId: cmd.traceId,
messageId: cmd.messageId,
channelId: cmd.channelId,
createdAt: now,
sequenceId,
gatewayMessageId,
receiptStatus: 'delivered',
rawStatus: 'DELIVRD',
deliveredAt: now,
},
{ jobId: `receipt-${cmd.messageId}` },
);
},
{ connection, concurrency },
);
const submitResultWorker = new Worker(
submitResultQueueName,
async () => {
submitResults += 1;
},
{ connection, concurrency },
);
const receiptWorker = new Worker(
receiptQueueName,
async () => {
receiptEvents += 1;
},
{ connection, concurrency },
);
async function waitForCompletion() {
const deadline = Date.now() + 60_000;
while (Date.now() < deadline) {
if (submitResults >= messageCount && receiptEvents >= messageCount) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 10));
}
throw new Error(`Timed out waiting for BullMQ events: submitResults=${submitResults} receiptEvents=${receiptEvents}`);
}
async function main() {
await connection.ping();
await cleanQueues();
const workers = [gatewayWorker, submitResultWorker, receiptWorker];
const runId = randomUUID();
const startedAt = performance.now();
for (let offset = 0; offset < messageCount; offset += 1000) {
const jobs = [];
for (let index = offset; index < Math.min(offset + 1000, messageCount); index += 1) {
const cmd = createSubmitCommand(index);
jobs.push({
name: 'SubmitCommand',
data: cmd,
opts: {
jobId: `${runId}-${cmd.messageId}`,
attempts: 3,
},
});
}
await submitQueue.addBulk(jobs);
}
const enqueuedAt = performance.now();
await waitForCompletion();
const completedAt = performance.now();
const enqueueDurationMs = enqueuedAt - startedAt;
const totalDurationMs = completedAt - startedAt;
const enqueueTps = messageCount / (enqueueDurationMs / 1000);
const endToEndTps = messageCount / (totalDurationMs / 1000);
const meets500Tps = enqueueTps >= 500 && endToEndTps >= 500;
console.log(
JSON.stringify(
{
messageCount,
concurrency,
submitResults,
receiptEvents,
enqueueDurationMs: Number(enqueueDurationMs.toFixed(2)),
totalDurationMs: Number(totalDurationMs.toFixed(2)),
enqueueTps: Number(enqueueTps.toFixed(2)),
endToEndTps: Number(endToEndTps.toFixed(2)),
meets500Tps,
},
null,
2,
),
);
await closeAll(workers);
if (!meets500Tps) {
process.exitCode = 1;
}
}
main().catch(async (error) => {
console.error(error);
await closeAll([gatewayWorker, submitResultWorker, receiptWorker]);
process.exitCode = 1;
});