feat: complete cmpp platform phases 0-5
This commit is contained in:
@@ -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: '您的验证码为 123456,5 分钟内有效。',
|
||||
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;
|
||||
});
|
||||
Reference in New Issue
Block a user