perf: expand gateway capacity and prevent receipt replay

This commit is contained in:
hectorzhao
2026-08-25 16:08:30 +08:00
parent 761c123b65
commit 9292352be1
48 changed files with 2001 additions and 144 deletions
@@ -367,6 +367,10 @@ export class ChannelConnectionService {
cmppVersion: channel.cmppVersion,
rateLimitPerSecond: channel.rateLimitPerSecond,
windowSize: getPositiveRuntimeInteger(getConfigValue(channel.config, 'windowSize'), 16, 'windowSize'),
connectionWarmupSeconds: Number(getConfigValue(channel.config, 'connectionWarmupSeconds') ?? 30),
connectionDrainTimeoutSeconds: getPositiveRuntimeInteger(getConfigValue(channel.config, 'connectionDrainTimeoutSeconds'), 60, 'connectionDrainTimeoutSeconds'),
submitResponseTimeoutSeconds: getPositiveRuntimeInteger(getConfigValue(channel.config, 'submitResponseTimeoutSeconds'), 60, 'submitResponseTimeoutSeconds'),
connectionFailureCooldownSeconds: getPositiveRuntimeInteger(getConfigValue(channel.config, 'connectionFailureCooldownSeconds'), 30, 'connectionFailureCooldownSeconds'),
heartbeatIntervalSeconds: getPositiveRuntimeInteger(
getConfigValue(channel.config, 'heartbeatIntervalSeconds'),
DEFAULT_HEARTBEAT_INTERVAL_SECONDS,
@@ -0,0 +1,14 @@
import { BadRequestException } from '@nestjs/common';
import { normalizeChannelRuntimeConfig } from './channels.helpers';
describe('Gateway channel capacity validation', () => {
it.each([1, 2, 4, 8])('accepts %i supplier connections', (desiredConnections) => {
expect(normalizeChannelRuntimeConfig(undefined, undefined, desiredConnections, 16)).toEqual(expect.objectContaining({ desiredConnections, windowSize: 16 }));
});
it.each([1, 16, 32, 64])('accepts supplier window %i', (windowSize) => {
expect(normalizeChannelRuntimeConfig(undefined, undefined, 1, windowSize)).toEqual(expect.objectContaining({ desiredConnections: 1, windowSize }));
});
it.each([[0, 16], [9, 16], [1, 0], [1, 65]])('rejects capacity outside 1..8 connections and 1..64 window', (connections, window) => {
expect(() => normalizeChannelRuntimeConfig(undefined, undefined, connections, window)).toThrow(BadRequestException);
});
});
+15 -3
View File
@@ -228,7 +228,7 @@ export function defaultChannelConnectionId(channelId: string) {
export function getDesiredConnections(config?: Prisma.JsonValue | null) {
if (config && typeof config === 'object' && !Array.isArray(config) && 'desiredConnections' in config) {
const value = Number(config.desiredConnections);
if (Number.isInteger(value) && value > 0) {
if (Number.isInteger(value) && value >= 1 && value <= 8) {
return value;
}
}
@@ -327,8 +327,12 @@ export function normalizeChannelRuntimeConfig(
? incomingConfig
: {};
const base = { ...existing, ...incoming };
base.desiredConnections = getPositiveRuntimeInteger(desiredConnections ?? base.desiredConnections, 1, 'desiredConnections');
base.windowSize = getPositiveRuntimeInteger(windowSize ?? base.windowSize, 16, 'windowSize');
base.desiredConnections = boundedRuntimeInteger(desiredConnections ?? base.desiredConnections, 1, 8, 1, 'desiredConnections');
base.windowSize = boundedRuntimeInteger(windowSize ?? base.windowSize, 1, 64, 16, 'windowSize');
base.connectionWarmupSeconds = boundedRuntimeInteger(base.connectionWarmupSeconds, 0, 300, 30, 'connectionWarmupSeconds');
base.connectionDrainTimeoutSeconds = boundedRuntimeInteger(base.connectionDrainTimeoutSeconds, 1, 600, 60, 'connectionDrainTimeoutSeconds');
base.submitResponseTimeoutSeconds = boundedRuntimeInteger(base.submitResponseTimeoutSeconds, 1, 300, 60, 'submitResponseTimeoutSeconds');
base.connectionFailureCooldownSeconds = boundedRuntimeInteger(base.connectionFailureCooldownSeconds, 1, 300, 30, 'connectionFailureCooldownSeconds');
base.heartbeatIntervalSeconds = getPositiveRuntimeInteger(
heartbeatIntervalSeconds ?? base.heartbeatIntervalSeconds,
DEFAULT_HEARTBEAT_INTERVAL_SECONDS,
@@ -345,6 +349,14 @@ export function normalizeChannelRuntimeConfig(
return base;
}
function boundedRuntimeInteger(value: unknown, minimum: number, maximum: number, fallback: number, field: string) {
const normalized = value === undefined || value === null || value === '' ? fallback : Number(value);
if (!Number.isInteger(normalized) || normalized < minimum || normalized > maximum) {
throw new BadRequestException(`${field} must be an integer between ${minimum} and ${maximum}`);
}
return normalized;
}
export function normalizeLongMessageReceiptMode(value: unknown) {
const normalized = String(value ?? 'per_segment').trim() || 'per_segment';
if (!['per_segment', 'message_level'].includes(normalized)) {