perf: merge CMPP inbox validation and persistence

This commit is contained in:
hectorzhao
2026-08-20 18:55:09 +08:00
parent 633e7a7055
commit 99fb346566
6 changed files with 303 additions and 32 deletions
+82 -22
View File
@@ -2119,6 +2119,24 @@ describe('SendChainService', () => {
try {
const { service, prisma, billing, riskReview, phoneFrequency } = createService();
service.enqueueBatchTask = jest.fn();
prisma.$queryRaw.mockImplementationOnce((query) => {
const payloadHash = query.values.find((value: unknown) => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value));
return Promise.resolve([{
validationError: null,
payloadHash,
response: {
accepted: true,
tenantId: 'tenant-1',
applicationId: 'app-1',
taskId: '',
messageId: 'MSG-fast',
messageRecordId: '',
status: 'accepted_pending',
phoneCount: 2,
messages: [],
},
}]);
});
const result = await service.submitInboundMessage({
requestId: 'cmpp-inbound:test-fast-path',
@@ -2134,13 +2152,12 @@ describe('SendChainService', () => {
status: 'accepted_pending',
phoneCount: 2,
}));
expect(prisma.cmppInboundSubmissionInbox.create).toHaveBeenCalledWith({
data: expect.objectContaining({
requestKey: 'cmpp-inbound:test-fast-path',
tenantId: 'tenant-1',
applicationId: 'app-1',
}),
});
expect(prisma.smsApplication.findFirst).not.toHaveBeenCalled();
expect(prisma.$queryRaw).toHaveBeenCalledTimes(1);
const sql = prisma.$queryRaw.mock.calls[0][0].strings.join(' ');
expect(sql).toContain('INSERT INTO "CmppInboundSubmissionInbox"');
expect(sql).toContain('JOIN "Tenant"');
expect(sql).toContain('ON CONFLICT ("requestKey") DO NOTHING');
expect(prisma.smsBatchTask.create).not.toHaveBeenCalled();
expect(prisma.smsMessageRecord.create).not.toHaveBeenCalled();
expect(riskReview.evaluateTask).not.toHaveBeenCalled();
@@ -2158,7 +2175,6 @@ describe('SendChainService', () => {
process.env.CMPP_INBOUND_FAST_PATH_ENABLED = 'true';
try {
const { service, prisma } = createService();
let originalHash = '';
const storedResponse = {
accepted: true,
tenantId: 'tenant-1',
@@ -2170,19 +2186,10 @@ describe('SendChainService', () => {
phoneCount: 1,
messages: [{ phoneNumber: '13800000001', messageId: 'MSG-stable', messageRecordId: '', taskId: '', status: 'accepted_pending' }],
};
prisma.cmppInboundSubmissionInbox.create
.mockImplementationOnce(({ data }) => {
originalHash = data.payloadHash;
return Promise.resolve({ id: 'inbox-1' });
})
.mockRejectedValueOnce(new Prisma.PrismaClientKnownRequestError('duplicate request key', {
code: 'P2002',
clientVersion: '7.9.0',
}));
prisma.cmppInboundSubmissionInbox.findUnique.mockImplementation(() => Promise.resolve({
payloadHash: originalHash,
response: storedResponse,
}));
prisma.$queryRaw.mockImplementation((query) => {
const payloadHash = query.values.find((value: unknown) => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value));
return Promise.resolve([{ validationError: null, payloadHash, response: storedResponse }]);
});
const request = {
requestId: 'cmpp-inbound:test-retry',
account: '100001',
@@ -2194,7 +2201,8 @@ describe('SendChainService', () => {
await service.submitInboundMessage(request);
await expect(service.submitInboundMessage(request)).resolves.toEqual(storedResponse);
expect(prisma.cmppInboundSubmissionInbox.create).toHaveBeenCalledTimes(2);
expect(prisma.$queryRaw).toHaveBeenCalledTimes(2);
expect(prisma.cmppInboundSubmissionInbox.findUnique).not.toHaveBeenCalled();
expect(prisma.smsMessageRecord.create).not.toHaveBeenCalled();
} finally {
if (previous == null) delete process.env.CMPP_INBOUND_FAST_PATH_ENABLED;
@@ -2202,6 +2210,58 @@ describe('SendChainService', () => {
}
});
it('rejects a disabled application before the merged Inbox statement can insert', async () => {
const previous = process.env.CMPP_INBOUND_FAST_PATH_ENABLED;
process.env.CMPP_INBOUND_FAST_PATH_ENABLED = 'true';
try {
const { service, prisma } = createService();
prisma.$queryRaw.mockResolvedValueOnce([{
validationError: 'CMPP account is disabled for new submissions',
payloadHash: null,
response: null,
}]);
await expect(service.submitInboundMessage({
requestId: 'cmpp-inbound:disabled',
account: '100001',
phoneNumber: '13800000001',
content: 'hello',
})).rejects.toThrow('CMPP account is disabled for new submissions');
expect(prisma.cmppInboundSubmissionInbox.findUnique).not.toHaveBeenCalled();
} finally {
if (previous == null) delete process.env.CMPP_INBOUND_FAST_PATH_ENABLED;
else process.env.CMPP_INBOUND_FAST_PATH_ENABLED = previous;
}
});
it('uses a read-only recovery only for a concurrent Inbox insert outside the CTE snapshot', async () => {
const previous = process.env.CMPP_INBOUND_FAST_PATH_ENABLED;
process.env.CMPP_INBOUND_FAST_PATH_ENABLED = 'true';
try {
const { service, prisma } = createService();
let payloadHash = '';
prisma.$queryRaw.mockImplementationOnce((query) => {
payloadHash = query.values.find((value: unknown) => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value));
return Promise.resolve([{ validationError: null, payloadHash: null, response: null }]);
});
prisma.cmppInboundSubmissionInbox.findUnique.mockImplementationOnce(() => Promise.resolve({
payloadHash,
response: { accepted: true, messageId: 'MSG-concurrent', status: 'accepted_pending' },
}));
await expect(service.submitInboundMessage({
requestId: 'cmpp-inbound:concurrent',
account: '100001',
phoneNumber: '13800000001',
content: 'hello',
})).resolves.toEqual(expect.objectContaining({ messageId: 'MSG-concurrent' }));
expect(prisma.cmppInboundSubmissionInbox.findUnique).toHaveBeenCalledTimes(1);
} finally {
if (previous == null) delete process.env.CMPP_INBOUND_FAST_PATH_ENABLED;
else process.env.CMPP_INBOUND_FAST_PATH_ENABLED = previous;
}
});
it('persists an idempotent daily quota reservation with a Prisma Date value', async () => {
const { service, prisma } = createService();
prisma.$queryRaw.mockResolvedValueOnce([{ tenantId: 'tenant-1', dailyLimit: 100000, usedCount: 1 }]);
+193 -10
View File
@@ -33,6 +33,26 @@ type ClaimedInboundWorkflow = {
payload: Prisma.JsonValue;
};
type PersistedInboundWorkflowRow = {
validationError: string | null;
payloadHash: string | null;
response: Prisma.JsonValue | null;
};
type InboundApplication = NonNullable<Awaited<ReturnType<SendSubmissionService['findInboundApplication']>>>;
type InboundWorkflowResponse = {
accepted: boolean;
tenantId: string;
applicationId: string;
taskId: string;
messageId: string;
messageRecordId: string;
status: string;
phoneCount: number;
messages: Array<{ phoneNumber: string; messageId: string; messageRecordId: string; taskId: string; status: string }>;
};
/**
* R9 inboundEntry implementation. Cross-method calls return through the stable SendChainService seam.
*/
@@ -183,6 +203,13 @@ async submitInboundMessage(data: GatewayInboundSubmitDto) {
throw new BadRequestException('CMPP submit phone number is invalid');
}
if (this.inboundFastPathEnabled() && !data.longMessage) {
return this.measureInboundStage(
'inbox_persist',
() => this.persistValidatedInboundWorkflow(data, phoneNumbers),
);
}
const application = await this.measureInboundStage(
'application_lookup',
() => this.facade.findInboundApplication(data.account),
@@ -276,12 +303,6 @@ async submitInboundMessage(data: GatewayInboundSubmitDto) {
throw error;
}
}
if (this.inboundFastPathEnabled()) {
return this.measureInboundStage(
'inbox_persist',
() => this.persistInboundWorkflow(data, phoneNumbers, application),
);
}
return this.measureInboundStage(
'complete_submit',
() => this.facade.submitCompleteInboundMessage(data, phoneNumbers, application),
@@ -292,6 +313,162 @@ async submitInboundMessage(data: GatewayInboundSubmitDto) {
return process.env.CMPP_INBOUND_FAST_PATH_ENABLED === 'true';
}
private async persistValidatedInboundWorkflow(
data: GatewayInboundSubmitDto,
phoneNumbers: string[],
) {
const requestKey = data.requestId?.trim();
if (!requestKey || requestKey.length > 160) {
throw new BadRequestException('CMPP inbound requestId is required for fast-path idempotency');
}
const submitGroupMessageId = `MSG-${randomUUID()}`;
const messageIds = phoneNumbers.map((_, index) => index === 0 ? submitGroupMessageId : `MSG-${randomUUID()}`);
const payload: InboundWorkflowPayload = {
data: JSON.parse(JSON.stringify(data)) as GatewayInboundSubmitDto,
phoneNumbers,
submitGroupMessageId,
messageIds,
};
const payloadHash = createHash('sha256').update(JSON.stringify({
data: payload.data,
phoneNumbers,
requestedGroupMessageId: null,
})).digest('hex');
const responseMessages = phoneNumbers.map((phoneNumber, index) => ({
phoneNumber,
messageId: messageIds[index],
messageRecordId: '',
taskId: '',
status: 'accepted_pending',
}));
const remoteIp = data.remoteIp?.replace(/^::ffff:/, '').trim() || null;
const submittedSrcId = data.srcId?.trim() ?? '';
// One indexed statement must both validate the current application state and create the
// durable Inbox row. Keeping those operations in one database snapshot prevents a disable
// racing between a separate SELECT and INSERT, while the unique request key remains the
// authoritative idempotency boundary.
const rows = await this.prisma.$queryRaw<PersistedInboundWorkflowRow[]>(Prisma.sql`
WITH application AS (
SELECT app.id,
app."tenantId",
app.status,
app."interfaceEnabled",
app."queuePriority",
app."cmppApplicationExtension",
app."cmppAccessNumberFillEnabled",
app."cmppAccessNumberFillPrefix",
app."cmppClientSrcId",
tenant.status AS "tenantStatus"
FROM "SmsApplication" AS app
JOIN "Tenant" AS tenant ON tenant.id = app."tenantId"
WHERE app."cmppAccount" = ${data.account}
LIMIT 1
), validation AS (
SELECT application.*,
CASE
WHEN application.status <> 'active'
OR application."tenantStatus" <> 'active'
OR NOT application."interfaceEnabled"
THEN 'CMPP account is disabled for new submissions'
WHEN ${remoteIp}::text IS NOT NULL
AND EXISTS (
SELECT 1 FROM "SmsApplicationIpAllowlist" allowlist
WHERE allowlist."applicationId" = application.id
)
AND NOT EXISTS (
SELECT 1
FROM "SmsApplicationIpAllowlist" allowlist
WHERE allowlist."applicationId" = application.id
AND (
(position('/' IN trim(allowlist."ipCidr")) = 0
AND regexp_replace(trim(allowlist."ipCidr"), '^::ffff:', '') = ${remoteIp})
OR (
trim(allowlist."ipCidr") ~ '^[0-9]{1,3}(\\.[0-9]{1,3}){3}/([0-9]|[12][0-9]|3[0-2])$'
AND ${remoteIp} ~ '^[0-9]{1,3}(\\.[0-9]{1,3}){3}$'
AND ${remoteIp}::inet <<= trim(allowlist."ipCidr")::cidr
)
)
)
THEN 'CMPP source IP is not in application allowlist'
WHEN coalesce(trim(application."cmppApplicationExtension"), '') <> ''
AND ${submittedSrcId} <> coalesce(
nullif(trim(application."cmppClientSrcId"), ''),
CASE WHEN application."cmppAccessNumberFillEnabled"
THEN coalesce(trim(application."cmppAccessNumberFillPrefix"), '')
ELSE ''
END || trim(application."cmppApplicationExtension")
)
THEN 'CMPP Src_Id must equal the access number assigned to this application: ' || coalesce(
nullif(trim(application."cmppClientSrcId"), ''),
CASE WHEN application."cmppAccessNumberFillEnabled"
THEN coalesce(trim(application."cmppAccessNumberFillPrefix"), '')
ELSE ''
END || trim(application."cmppApplicationExtension")
)
ELSE NULL
END AS "validationError"
FROM application
), inserted AS (
INSERT INTO "CmppInboundSubmissionInbox" (
id, "requestKey", "payloadHash", "tenantId", "applicationId", "queuePriority",
payload, response, status, attempts, "nextAttemptAt", "createdAt", "updatedAt"
)
SELECT ${randomUUID()}, ${requestKey}, ${payloadHash}, validation."tenantId", validation.id,
CASE WHEN validation."queuePriority" = 'priority' THEN 'priority' ELSE 'normal' END,
${JSON.stringify(payload)}::jsonb,
jsonb_build_object(
'accepted', true,
'tenantId', validation."tenantId",
'applicationId', validation.id,
'taskId', '',
'messageId', ${submitGroupMessageId},
'messageRecordId', '',
'status', 'accepted_pending',
'phoneCount', ${phoneNumbers.length},
'messages', ${JSON.stringify(responseMessages)}::jsonb
),
'pending', 0,
(NOW() AT TIME ZONE 'UTC'),
(NOW() AT TIME ZONE 'UTC'),
(NOW() AT TIME ZONE 'UTC')
FROM validation
WHERE validation."validationError" IS NULL
ON CONFLICT ("requestKey") DO NOTHING
RETURNING "payloadHash", response
)
SELECT validation."validationError",
coalesce(inserted."payloadHash", existing."payloadHash") AS "payloadHash",
coalesce(inserted.response, existing.response) AS response
FROM validation
LEFT JOIN inserted ON true
LEFT JOIN "CmppInboundSubmissionInbox" existing
ON existing."requestKey" = ${requestKey}
LIMIT 1
`);
const row = rows[0];
if (!row) {
throw new BadRequestException('CMPP account is invalid');
}
if (row.validationError) {
throw new BadRequestException(row.validationError);
}
if (row.response == null) {
// A concurrent insert that wins after this statement's MVCC snapshot is not visible to
// the CTE. The unique key has already prevented duplication, so only that rare retry path
// needs a second read; normal submissions still use exactly one database round trip.
const concurrent = await this.prisma.cmppInboundSubmissionInbox.findUnique({ where: { requestKey } });
if (!concurrent || concurrent.payloadHash !== payloadHash) {
throw new BadRequestException('CMPP inbound requestId conflicts with another payload');
}
return concurrent.response as InboundWorkflowResponse;
}
if (row.payloadHash !== payloadHash) {
throw new BadRequestException('CMPP inbound requestId conflicts with another payload');
}
return row.response as InboundWorkflowResponse;
}
private async persistInboundWorkflow(
data: GatewayInboundSubmitDto,
phoneNumbers: string[],
@@ -1051,10 +1228,17 @@ startInboundWorkflowWorker() {
try {
await this.refreshInboundWorkflowMetrics();
const claimed = await this.claimInboundWorkflows(available);
const applications = claimed.length === 0
? []
: await this.prisma.smsApplication.findMany({
where: { id: { in: [...new Set(claimed.map((item) => item.applicationId))] } },
include: { tenant: true, ipAllowlist: true },
});
const applicationById = new Map(applications.map((application) => [application.id, application]));
for (const item of claimed) {
this.inboundWorkflowInFlight += 1;
this.metrics?.setInboundWorkflowSlots(concurrency, this.inboundWorkflowInFlight);
const task = this.processClaimedInboundWorkflow(item)
const task = this.processClaimedInboundWorkflow(item, applicationById.get(item.applicationId))
.catch((error) => this.logger.error(`CMPP inbound workflow ${item.id} failed to settle: ${String(error)}`))
.finally(() => {
this.inboundWorkflowInFlight = Math.max(0, this.inboundWorkflowInFlight - 1);
@@ -1110,11 +1294,10 @@ startInboundWorkflowWorker() {
`);
}
private async processClaimedInboundWorkflow(item: ClaimedInboundWorkflow) {
private async processClaimedInboundWorkflow(item: ClaimedInboundWorkflow, application?: InboundApplication) {
try {
const payload = parseInboundWorkflowPayload(item.payload);
const application = await this.facade.findInboundApplication(payload.data.account);
if (!application || application.id !== item.applicationId) {
if (!application || application.cmppAccount !== payload.data.account) {
throw new Error('CMPP inbound application no longer matches persisted workflow');
}
const result = await this.facade.submitCompleteInboundMessage(
+2
View File
@@ -1160,3 +1160,5 @@ global,不能错误归入client。`client-signature-*`、发送页、企业认
- 进程隔离同时包含容量隔离:`PrismaService`按进程角色选择独立、有界的连接池上限;Gateway inbound分别持有与受限Submit窗口匹配的Submit API Transport和小型后台Transport,协议日志/回执流量不得占用Submit连接。连接复用、池上限和超时只属于传输/基础设施边界,不得渗入风控、计费、路由或消息状态机。
- `api/src/infrastructure-monitoring/`是运营端监控聚合与固定阈值应用边界:只消费代码白名单 PromQL,并通过版本化 PostgreSQL 单例、promtool 校验和原子规则热加载管理数值阈值;Exporter安装、端口隔离、固定规则模板和权限仍归`tools/monitoring/`治理。
- 活动告警已读也归该边界:Prometheus保留告警事实,Prisma仅持久化逐管理员、逐触发周期的阅读状态;全局布局只消费轻量未读汇总,不复制指纹、activeAt或用户隔离逻辑。
- 500条/秒第二阶段仍限制在`send-inbound-entry`和既有进程容量边界:短短信用一个PostgreSQL CTE完成当前应用校验和Inbox写入;长短信继续走既有分片域。Worker可按一次领取批次预取应用快照,但不得引入跨批应用状态缓存,也不得把模板、风控、计费、路由状态塞回Gateway同步入口。
- 容量参数继续归进程组装层:Inbox业务槽、BullMQ发送槽、Gateway供应商槽和结果Outbox槽分别有界、分别观测。代码模块不得假定测试环境参数就是生产默认值;生产调优必须重新基于PostgreSQL连接预算、六通道TPS/窗口和回调承载证据。
@@ -2114,3 +2114,11 @@
- 右上角预警中心增加“系统监控告警”,通过独立轻量接口统计 Prometheus 当前 firing/pending 告警及严重数,跳转系统监控活动告警区;任一预警域失败不得清空其他域。
- 活动告警列表必须提供逐条“标记已读”。已读状态按管理员和“告警指纹 + 本次 activeAt”持久化到 PostgreSQL;仅从当前管理员的预警中心数量中扣减,不改变 Prometheus firing/pending 状态,也不减少页面活动告警总数。同标签告警恢复后再次触发时必须重新成为未读。
- 服务端只能确认 Prometheus 当前仍存在且 activeAt 一致的告警,过期、已恢复或已重新触发的请求必须拒绝;重复点击同一次告警应幂等,并写操作日志。阈值设置弹窗只保留通用 Modal 外层滚动,不得嵌套第二个独立滚动区域。
## 完整处理500条/秒第二阶段:合并入口SQL与有界全链扩容(2026-08-20
- 普通短短信快路径必须把应用、企业当前启停状态、接口开关、IP白名单、Src_Id校验与Inbox幂等写入合并为同一个PostgreSQL语句和同一MVCC快照;正常新请求只允许一次数据库往返,不得先查应用再写Inbox。应用账号唯一索引和Inbox请求键唯一索引继续作为查询与幂等边界,不增加无证据索引。
- 合并语句不得缓存应用启停或白名单;应用不存在、已停用、接口关闭、IP或Src_Id不匹配时不得产生Inbox。相同请求键同载荷返回原响应,不同载荷拒绝;并发唯一键竞争因快照不可见时只允许一次只读恢复,不得通过无意义`ON CONFLICT DO UPDATE`制造写放大。
- 长短信仍在完整重组并完成既有校验后写Inbox,不为追求吞吐改写分片状态机。Inbox Worker每次领取后应按本批应用ID一次读取当前应用/企业/白名单快照,不能对同一批每条消息重复查应用;领取、实际业务处理和逐条完成仍保持原短事务与租约边界。
- 测试环境可在PostgreSQL连接总预算内分别提高Worker业务槽、BullMQ发送槽、Gateway供应商槽及结果Outbox槽,但每个池必须显式有界,且总数据库连接须为运维保留余量。供应商真实TPS、连接数和窗口仍是硬上限,禁止用并发参数绕过单通道限速。
- 第二阶段仍以“完整处理”验收:除500条/秒SubmitResp零拒绝、零缺失和延迟停止线外,还必须等待Inbox、BullMQ、命令Stream、结果Outbox与回执链排空,并按数据库业务记录、唯一MessageId、供应商尝试/接受和最终状态对账。入口达到500而全链排空速率不足500条/秒时必须明确判失败。
+11
View File
@@ -4745,3 +4745,14 @@ npm run verify:phase8
| TC-CMPP-500-P1-012 | API/Worker数据库池隔离 | API与Worker分别配置32/8连接并在Worker积压时持续提交 | 两进程使用各自有界连接池,API受理连接不被Worker抢占;总连接数不超过PostgreSQL上限且压后无`idle in transaction`泄漏 |
执行记录(2026-08-20,测试环境):P1-001至009已由API/Gateway自动化、真实PostgreSQL迁移和独立Worker恢复验证覆盖;P1-011专用Submit传输隔离后100、200条/秒分别2999/2999、3998/3998成功,零拒绝、零节流、零连接错误。P1-010在500条/秒档失败:测试环境池调优后10秒仅2979条,实际297.9条/秒且节流611次;全部2979条最终完成并排空,但完整链仅约24条/秒。故第一阶段不通过500条/秒总目标,不执行“完整500条/秒已达标”的结论。
### 完整处理500条/秒第二阶段
| 编号 | 场景 | 操作 | 预期 |
|---|---|---|---|
| TC-CMPP-500-P2-001 | 合并校验与Inbox写入 | 普通短短信走快路径并检查SQL与阶段指标 | 使用账号唯一索引在同一SQL校验应用/企业/接口/IP/Src_Id并幂等插入;正常请求仅一次DB往返且不再单独记录`application_lookup` |
| TC-CMPP-500-P2-002 | 合并SQL拒绝安全 | 分别停用应用、停用企业、关闭接口、使用错误IP和Src_Id | 返回对应拒绝且Inbox没有新增;不读取或缓存旧应用快照 |
| TC-CMPP-500-P2-003 | 幂等与并发唯一键竞争 | 同载荷重试、冲突载荷重试,并并发提交相同请求键 | 同载荷返回原MessageId;冲突载荷拒绝;并发竞争最多执行一次只读恢复,不发生重复Inbox或无意义更新 |
| TC-CMPP-500-P2-004 | Worker批量应用快照 | 同批领取多个相同及不同应用Inbox | 每个领取批次按唯一应用ID一次查询,逐条继续使用匹配快照;应用不匹配时安全退避,不跨租约持有事务 |
| TC-CMPP-500-P2-005 | 有界容量配置 | 在测试环境提高Worker、BullMQ、Gateway Submit和Outbox并发并观测连接 | 各池配置值与在途数可观测,PostgreSQL连接低于`max_connections`并无长事务;单通道限速不被绕过 |
| TC-CMPP-500-P2-006 | 100→200→500完整链阶梯 | 使用全新测试号段、隔离六通道模拟器,逐档注入并等待全部队列排空 | 每档报告入口实际速率/分位、Inbox完成、各队列峰值与排空、供应商阶段和数据库对账;500档只有入口及完整链均达到500条/秒且零丢重才通过 |
+7
View File
@@ -3805,3 +3805,10 @@ git diff --check
- 专用Submit池后100条/秒30秒发满2999条,2999/2999成功、零拒绝/节流/连接错误,P50/P95/P99=`42/147/173ms`API平均约37ms、Gateway API往返平均约54ms2999条Inbox全部completed,完整供应商链从开始到双Stream排空约132秒,折算约22.7条/秒。200条/秒20秒发满3998条,3998/3998成功、零拒绝/节流/连接错误,分位=`134/370/837ms`;Inbox在开始后约75秒全部完成,双Stream在开始后约166秒排空,完整链折算约24.1条/秒。
- 默认API池32、Submit池64下首次500条/秒10秒只生成1716条,1716/1716成功但节流615次,P95=5217ms,实际171.6条/秒,失败停止。基于PostgreSQL`max_connections=100`和进程连接证据,仅在测试环境把`API_DB_POOL_MAX`调为48、`GATEWAY_CMPP_INBOUND_MAX_CONCURRENCY`调为128Worker仍为8;复测生成2979条,2979/2979成功、零拒绝/连接错误,P50/P95/P99=`942/1162/3221ms`,但仍节流611次,实际297.9条/秒,500入口仍失败。10个测试连接各窗口32形成320总在途,Gateway平均往返约965ms,实测上限与窗口理论值一致;API平均约322ms,继续只加连接会放大PostgreSQL竞争。
- 最终数据库按四个新号码前缀对账:`1390005/6/7/8`的Inbox completed分别为`2999/3998/1716/2979`,对应`SmsMessageRecord`及唯一MessageId数量完全相同,非completed Inbox为0,命令Stream和结果Outbox均`pending=0/lag=0`,相关服务日志无panic/fatal/Prisma错误或Inbox retry。第一阶段结论为200条/秒入口可靠受理通过、500条/秒入口未通过、完整异步链约24条/秒;不得宣称已完整处理500条/秒。下一阶段必须合并每Submit应用查询与Inbox写入、提高测试连接/窗口总量,并把Worker业务事务、供应商六通道总TPS及结果/回执写入水平扩容,不能再靠单机连接池参数解决。
## 2026-08-20 完整处理500条/秒第二阶段:合并入口SQL与全链有界扩容(实施中)
- 本轮重新核对本地`HEAD=633e7a705539cc706aa0a6dbabfd47cc61265e7f``origin/main=c4f36fc50d7906dfb2f97c881e9ea43c6a64c370`;测试环境运行标识为`229a0b28fd8b84d910c359ff9ac442fc3e843cb4+workspace.p1submitpool.f50505ad9ee9`,预生产仍为`433b2ee56f6016ad8afff1bac73f510b8fd53083+gateway-v2.cd7bb8d05e7b`。受保护的构建产物、`outputs/``pnpm-lock.yaml`和空文件`=`保持原状,不归因本轮。
- Worker累计真实指标4695条显示:预检、模板、消息持久化、风控频次、计费和入队平均约`83.9/74.5/28.3/92.2/25.2/64.7ms`,与上一阶段完整链约24条/秒一致。该证据确认不能把入口吞吐冒充完整处理能力。
- 普通短短信快路径已改为单个PostgreSQL CTE:使用账号唯一索引读取应用和企业当前状态,在同一快照校验接口、IP白名单及Src_Id,并以请求键`ON CONFLICT DO NOTHING`幂等写Inbox和稳定响应。正常路径不再先执行Prisma应用查询;仅并发唯一键竞争且当前快照看不到胜者时执行一次只读恢复,避免用自更新制造表膨胀。长短信分片状态机保持不变。
- Inbox Worker领取后改为按本批唯一`applicationId`一次查询应用、企业和白名单,再逐条处理;不做跨批应用状态缓存,领取事务仍不包住风控、计费、Redis或供应商调用。专项SendChain 121项、API全量42套493项、API与前端TypeScript、Vite生产构建、Prisma validate、Gateway全量测试/vet、5份Stream契约、R0/R6/R7/R10、安全/部署门禁和`git diff --check`均通过;R9仍只被HEAD既有`dispatchDueScheduledTasks`哈希漂移阻断,未改写该方法。提交、恢复资产、测试环境发布和100→200→500阶梯结果待后续补记。