fix: align api keepalive with gateway pool
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
import { configureApiHttpServerTimeouts } from './http-server-timeouts';
|
||||||
|
|
||||||
|
describe('configureApiHttpServerTimeouts', () => {
|
||||||
|
it('keeps the API connection alive longer than the Gateway idle pool', () => {
|
||||||
|
const server = { keepAliveTimeout: 0, headersTimeout: 0 };
|
||||||
|
expect(configureApiHttpServerTimeouts(server, {})).toEqual({
|
||||||
|
keepAliveTimeoutMs: 120_000,
|
||||||
|
headersTimeoutMs: 125_000,
|
||||||
|
});
|
||||||
|
expect(server).toEqual({ keepAliveTimeout: 120_000, headersTimeout: 125_000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps headers timeout above a configured keep-alive timeout', () => {
|
||||||
|
const server = { keepAliveTimeout: 0, headersTimeout: 0 };
|
||||||
|
expect(configureApiHttpServerTimeouts(server, {
|
||||||
|
API_HTTP_KEEP_ALIVE_TIMEOUT_MS: '90000',
|
||||||
|
API_HTTP_HEADERS_TIMEOUT_MS: '1000',
|
||||||
|
})).toEqual({ keepAliveTimeoutMs: 90_000, headersTimeoutMs: 91_000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { Server } from 'node:http';
|
||||||
|
|
||||||
|
const DEFAULT_KEEP_ALIVE_TIMEOUT_MS = 120_000;
|
||||||
|
const DEFAULT_HEADERS_TIMEOUT_MS = 125_000;
|
||||||
|
|
||||||
|
export function configureApiHttpServerTimeouts(
|
||||||
|
server: Pick<Server, 'keepAliveTimeout' | 'headersTimeout'>,
|
||||||
|
env: NodeJS.ProcessEnv,
|
||||||
|
) {
|
||||||
|
const keepAliveTimeoutMs = positiveInteger(env.API_HTTP_KEEP_ALIVE_TIMEOUT_MS, DEFAULT_KEEP_ALIVE_TIMEOUT_MS);
|
||||||
|
const configuredHeadersTimeoutMs = positiveInteger(env.API_HTTP_HEADERS_TIMEOUT_MS, DEFAULT_HEADERS_TIMEOUT_MS);
|
||||||
|
const headersTimeoutMs = Math.max(configuredHeadersTimeoutMs, keepAliveTimeoutMs + 1_000);
|
||||||
|
server.keepAliveTimeout = keepAliveTimeoutMs;
|
||||||
|
server.headersTimeout = headersTimeoutMs;
|
||||||
|
return { keepAliveTimeoutMs, headersTimeoutMs };
|
||||||
|
}
|
||||||
|
|
||||||
|
function positiveInteger(value: string | undefined, fallback: number) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
||||||
|
}
|
||||||
+3
-1
@@ -7,6 +7,7 @@ import { AppModule } from './app.module';
|
|||||||
import { MetricsService } from './metrics/metrics.service';
|
import { MetricsService } from './metrics/metrics.service';
|
||||||
import { OpenApiModule } from './open-api/open-api.module';
|
import { OpenApiModule } from './open-api/open-api.module';
|
||||||
import { configureHttpBodyParsers } from './http-body-limits';
|
import { configureHttpBodyParsers } from './http-body-limits';
|
||||||
|
import { configureApiHttpServerTimeouts } from './http-server-timeouts';
|
||||||
|
|
||||||
Object.defineProperty(BigInt.prototype, 'toJSON', {
|
Object.defineProperty(BigInt.prototype, 'toJSON', {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
@@ -43,7 +44,8 @@ async function bootstrap() {
|
|||||||
const port = Number(process.env.API_PORT ?? 3000);
|
const port = Number(process.env.API_PORT ?? 3000);
|
||||||
// 生产环境只允许 Nginx 访问管理 API;显式绑定回环,避免默认的全网卡监听绕过入口鉴权与限流。
|
// 生产环境只允许 Nginx 访问管理 API;显式绑定回环,避免默认的全网卡监听绕过入口鉴权与限流。
|
||||||
const host = process.env.API_HOST?.trim() || '127.0.0.1';
|
const host = process.env.API_HOST?.trim() || '127.0.0.1';
|
||||||
await app.listen(port, host);
|
const apiServer = await app.listen(port, host);
|
||||||
|
configureApiHttpServerTimeouts(apiServer, process.env);
|
||||||
|
|
||||||
const metrics = app.get(MetricsService);
|
const metrics = app.get(MetricsService);
|
||||||
const metricsHost = process.env.API_METRICS_HOST?.trim() || '127.0.0.1';
|
const metricsHost = process.env.API_METRICS_HOST?.trim() || '127.0.0.1';
|
||||||
|
|||||||
@@ -2167,3 +2167,4 @@
|
|||||||
- 正在处理的企业必须从下一次领取候选中排除,避免同企业并发事务重新争抢账户锁;租约过期恢复、优先级/FIFO、`FOR UPDATE SKIP LOCKED`和逐请求幂等语义保持不变。
|
- 正在处理的企业必须从下一次领取候选中排除,避免同企业并发事务重新争抢账户锁;租约过期恢复、优先级/FIFO、`FOR UPDATE SKIP LOCKED`和逐请求幂等语义保持不变。
|
||||||
- 原生SQL对Prisma的无时区时间列统一使用`NOW() AT TIME ZONE 'UTC'`,覆盖Submit Outbox领取、租约、发布、重试及关联消息更新时间,禁止用数据库会话时区污染时延审计。
|
- 原生SQL对Prisma的无时区时间列统一使用`NOW() AT TIME ZONE 'UTC'`,覆盖Submit Outbox领取、租约、发布、重试及关联消息更新时间,禁止用数据库会话时区污染时延审计。
|
||||||
- 验收必须保持正价,分别执行单企业100/150 TPS和至少两个独立企业合计200 TPS,按非补发首次供应商Submit、回执、上行、计费、主备补发、业务拦截及全队列排空对账;触发拒绝、连接错误、持续积压、数据库异常或账务不一致立即停止。多Gateway P2不在本阶段范围。
|
- 验收必须保持正价,分别执行单企业100/150 TPS和至少两个独立企业合计200 TPS,按非补发首次供应商Submit、回执、上行、计费、主备补发、业务拦截及全队列排空对账;触发拒绝、连接错误、持续积压、数据库异常或账务不一致立即停止。多Gateway P2不在本阶段范围。
|
||||||
|
- Gateway到主API的回环HTTP连接池空闲时长不得超过API服务端keep-alive生命周期;API默认keep-alive 120秒、headers timeout 125秒,并由发布环境显式设置和校验,防止负载期复用已被Node关闭的连接而把本可受理的Submit误回Result 9。
|
||||||
|
|||||||
@@ -4885,3 +4885,4 @@ npm run verify:phase8
|
|||||||
| TC-CMPP-PHASE5-014 | 不同企业正价并行 | 至少两个独立企业合计200 TPS;各企业账户冻结串行且企业间并行,单价均为325,分别对账消息、账单、首提和最终状态 |
|
| TC-CMPP-PHASE5-014 | 不同企业正价并行 | 至少两个独立企业合计200 TPS;各企业账户冻结串行且企业间并行,单价均为325,分别对账消息、账单、首提和最终状态 |
|
||||||
| TC-CMPP-PHASE5-015 | Outbox UTC时间语义 | Asia/Shanghai数据库会话下领取、租约、发布、重试均写UTC无时区值;publishedAt-createdAt不再出现约8小时偏差 |
|
| TC-CMPP-PHASE5-015 | Outbox UTC时间语义 | Asia/Shanghai数据库会话下领取、租约、发布、重试均写UTC无时区值;publishedAt-createdAt不再出现约8小时偏差 |
|
||||||
| TC-CMPP-PHASE5-016 | 微批发布边界与停止线 | 仅单Gateway;分别验证单企业100/150与多企业200,发生拒绝、连接错误、持续积压、数据库异常或账务不一致立即停止 |
|
| TC-CMPP-PHASE5-016 | 微批发布边界与停止线 | 仅单Gateway;分别验证单企业100/150与多企业200,发生拒绝、连接错误、持续积压、数据库异常或账务不一致立即停止 |
|
||||||
|
| TC-CMPP-PHASE5-017 | Gateway到API长连接生命周期 | API keep-alive 120秒大于Gateway连接池90秒,headers timeout更大;跨越Node原默认5秒空闲边界后继续压测,不得出现loopback connection reset或Result 9 |
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ BRANCH="${BRANCH:-main}"
|
|||||||
PUBLIC_HTTP_PORT="${PUBLIC_HTTP_PORT:-12026}"
|
PUBLIC_HTTP_PORT="${PUBLIC_HTTP_PORT:-12026}"
|
||||||
API_PORT="${API_PORT:-3000}"
|
API_PORT="${API_PORT:-3000}"
|
||||||
API_HOST="${API_HOST:-127.0.0.1}"
|
API_HOST="${API_HOST:-127.0.0.1}"
|
||||||
|
API_HTTP_KEEP_ALIVE_TIMEOUT_MS="${API_HTTP_KEEP_ALIVE_TIMEOUT_MS:-120000}"
|
||||||
|
API_HTTP_HEADERS_TIMEOUT_MS="${API_HTTP_HEADERS_TIMEOUT_MS:-125000}"
|
||||||
API_METRICS_HOST="${API_METRICS_HOST:-127.0.0.1}"
|
API_METRICS_HOST="${API_METRICS_HOST:-127.0.0.1}"
|
||||||
API_METRICS_PORT="${API_METRICS_PORT:-9464}"
|
API_METRICS_PORT="${API_METRICS_PORT:-9464}"
|
||||||
API_ENABLE_SEND_WORKER="${API_ENABLE_SEND_WORKER:-true}"
|
API_ENABLE_SEND_WORKER="${API_ENABLE_SEND_WORKER:-true}"
|
||||||
@@ -200,6 +202,8 @@ write_env() {
|
|||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
API_PORT=${API_PORT}
|
API_PORT=${API_PORT}
|
||||||
API_HOST=${API_HOST}
|
API_HOST=${API_HOST}
|
||||||
|
API_HTTP_KEEP_ALIVE_TIMEOUT_MS=${API_HTTP_KEEP_ALIVE_TIMEOUT_MS}
|
||||||
|
API_HTTP_HEADERS_TIMEOUT_MS=${API_HTTP_HEADERS_TIMEOUT_MS}
|
||||||
API_METRICS_HOST=${API_METRICS_HOST}
|
API_METRICS_HOST=${API_METRICS_HOST}
|
||||||
API_METRICS_PORT=${API_METRICS_PORT}
|
API_METRICS_PORT=${API_METRICS_PORT}
|
||||||
API_ENABLE_SEND_WORKER=${API_ENABLE_SEND_WORKER}
|
API_ENABLE_SEND_WORKER=${API_ENABLE_SEND_WORKER}
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ set -a
|
|||||||
source "$ENV_FILE"
|
source "$ENV_FILE"
|
||||||
set +a
|
set +a
|
||||||
|
|
||||||
|
if [[ ! "${API_HTTP_KEEP_ALIVE_TIMEOUT_MS:-}" =~ ^[1-9][0-9]*$ || ! "${API_HTTP_HEADERS_TIMEOUT_MS:-}" =~ ^[1-9][0-9]*$ || "${API_HTTP_HEADERS_TIMEOUT_MS}" -le "${API_HTTP_KEEP_ALIVE_TIMEOUT_MS}" ]]; then
|
||||||
|
echo "API_HTTP_KEEP_ALIVE_TIMEOUT_MS must be positive and API_HTTP_HEADERS_TIMEOUT_MS must be greater in $ENV_FILE." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "${API_ENABLE_SEND_WORKER:-}" != "true" ]]; then
|
if [[ "${API_ENABLE_SEND_WORKER:-}" != "true" ]]; then
|
||||||
echo "API_ENABLE_SEND_WORKER=true is required in $ENV_FILE; refusing to deploy with SMS sending disabled." >&2
|
echo "API_ENABLE_SEND_WORKER=true is required in $ENV_FILE; refusing to deploy with SMS sending disabled." >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { resolve } from 'node:path';
|
|||||||
const deploy = readFileSync(resolve(import.meta.dirname, 'production-deploy.sh'), 'utf8');
|
const deploy = readFileSync(resolve(import.meta.dirname, 'production-deploy.sh'), 'utf8');
|
||||||
const bootstrap = readFileSync(resolve(import.meta.dirname, 'production-bootstrap.sh'), 'utf8');
|
const bootstrap = readFileSync(resolve(import.meta.dirname, 'production-bootstrap.sh'), 'utf8');
|
||||||
const apiMain = readFileSync(resolve(import.meta.dirname, '../../api/src/main.ts'), 'utf8');
|
const apiMain = readFileSync(resolve(import.meta.dirname, '../../api/src/main.ts'), 'utf8');
|
||||||
|
const apiTimeouts = readFileSync(resolve(import.meta.dirname, '../../api/src/http-server-timeouts.ts'), 'utf8');
|
||||||
const workerMain = readFileSync(resolve(import.meta.dirname, '../../api/src/send-worker.ts'), 'utf8');
|
const workerMain = readFileSync(resolve(import.meta.dirname, '../../api/src/send-worker.ts'), 'utf8');
|
||||||
const required = [
|
const required = [
|
||||||
'compression_config=/etc/nginx/conf.d/cmpp-compression.conf',
|
'compression_config=/etc/nginx/conf.d/cmpp-compression.conf',
|
||||||
@@ -26,6 +27,14 @@ for (const marker of ['API_HOST="${API_HOST:-127.0.0.1}"', 'API_HOST=${API_HOST}
|
|||||||
if (!apiMain.includes("process.env.API_HOST?.trim() || '127.0.0.1'") || !apiMain.includes('app.listen(port, host)')) {
|
if (!apiMain.includes("process.env.API_HOST?.trim() || '127.0.0.1'") || !apiMain.includes('app.listen(port, host)')) {
|
||||||
throw new Error('NestJS API must bind to API_HOST and default to loopback');
|
throw new Error('NestJS API must bind to API_HOST and default to loopback');
|
||||||
}
|
}
|
||||||
|
for (const marker of ['API_HTTP_KEEP_ALIVE_TIMEOUT_MS', 'API_HTTP_HEADERS_TIMEOUT_MS']) {
|
||||||
|
if (!`${deploy}\n${bootstrap}\n${apiTimeouts}`.includes(marker)) {
|
||||||
|
throw new Error(`production deployment is missing the API keep-alive contract: ${marker}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!apiMain.includes('configureApiHttpServerTimeouts(apiServer, process.env)')) {
|
||||||
|
throw new Error('NestJS API must apply the explicit keep-alive contract to its HTTP server');
|
||||||
|
}
|
||||||
|
|
||||||
for (const marker of [
|
for (const marker of [
|
||||||
'CMPP_INBOUND_FAST_PATH_ENABLED=true',
|
'CMPP_INBOUND_FAST_PATH_ENABLED=true',
|
||||||
|
|||||||
Reference in New Issue
Block a user