Refuse a message past the held-message bound with ESME_RTHROTTLED, and answer a full reassembly buffer the same way
Test / lint (pull_request) Successful in 23s
Test / test (18) (pull_request) Successful in 31s
Test / test (20) (pull_request) Successful in 31s
Test / test (22) (pull_request) Successful in 31s
Test / test (24) (pull_request) Successful in 31s
Test / test (26) (pull_request) Successful in 31s
Mirror / push (push) Successful in 4s

This commit is contained in:
2026-09-26 13:18:56 +02:00
parent d8232bcd67
commit 855ccbe75f
9 changed files with 113 additions and 104 deletions
+7 -26
View File
@@ -30,7 +30,6 @@ export class HeldMessages {
private readonly held: ExpiringGroups<Held>;
private readonly idleWaiters = new IdleWaiters();
private readonly log: SmppLog;
private readonly max: number;
private readonly maxOctets: number;
private octets = 0;
@@ -42,7 +41,6 @@ export class HeldMessages {
timeout: options.timeout,
});
this.log = options.log;
this.max = options.max;
this.maxOctets = options.maxOctets;
}
@@ -50,7 +48,13 @@ export class HeldMessages {
return this.held.size;
}
/** An application that answers no message at all may not grow this without end. */
/** Whether a message arriving now is past the bound, once the expired are swept. */
full(): boolean {
this.sweep();
return this.held.full || this.octets >= this.maxOctets;
}
hold(pduObjs: PduObject[]): void {
const key = keyOf(pduObjs);
@@ -63,17 +67,10 @@ export class HeldMessages {
if (replaced) {
this.log.warn('heldMessages - replacing a message on a re-used sequence number', { seqNr: Number(key) });
this.delete(key, replaced);
} else if (this.held.full) {
this.dropOldest();
}
const octets = pduObjs.reduce((sum, pduObj) => sum + retainedOctets(pduObj), 0);
// The message just held stays even alone past the cap: the peer is still owed its answer.
while (this.held.size > 0 && this.octets + octets > this.maxOctets) {
this.dropOldest();
}
this.held.set(key, { octets, pduObjs });
this.octets += octets;
}
@@ -109,22 +106,6 @@ export class HeldMessages {
return this.idleWaiters.wait(() => this.held.size, timeout, signal);
}
private dropOldest(): void {
const oldest = this.held.takeOldest();
if (!oldest) return;
const [seqNr, held] = oldest;
this.octets -= held.octets;
this.log.warn('heldMessages - buffer full, dropping the oldest message', {
max: this.max,
maxOctets: this.maxOctets,
octets: this.octets,
seqNr: Number(seqNr),
});
}
/** Drops every message past its deadline. Runs before each hold and on its own timer. */
sweep(): void {
const expired = this.held.takeExpired();
+16 -2
View File
@@ -19,7 +19,11 @@ import { paramText } from './defs/types.ts';
import { respIdParams, segmentId } from './sms-id.ts';
import { respNameFor } from './defs/commands.ts';
/** SMPP 3.4 lists ESME_RMSGQFUL under submit_sm_resp only; 4.6.2's retryable code is another. */
/** Asks the peer to keep the message and retry: RTHROTTLED is the SMSC's to send, so an ESME's is another. */
function throttledStatus(carriedAs: string): ErrorName {
return carriedAs === 'submit_sm' ? 'ESME_RTHROTTLED' : 'ESME_RX_T_APPN';
}
export function refusedSegmentStatus(
carriedAs: string,
refusal: Refusal,
@@ -30,7 +34,7 @@ export function refusedSegmentStatus(
return spelling === 'sar' ? 'ESME_RINVTLVVAL' : 'ESME_RINVESMCLASS';
}
return carriedAs === 'submit_sm' ? 'ESME_RMSGQFUL' : 'ESME_RX_T_APPN';
return throttledStatus(carriedAs);
}
const lostReasons: Record<LostGroup['reason'], string> = {
@@ -212,6 +216,16 @@ export class IncomingRequests {
* one request at a time never sends the second segment until the first has been answered.
*/
private async onMessage(pduObj: PduObject): Promise<void> {
if (this.held.full()) {
this.log.info('session - unanswered messages at their bound, asking the peer to retry', {
cmdName: pduObj.cmdName,
seqNr: pduObj.seqNr,
});
await this.session.sendReturn(pduObj, throttledStatus(this.carriedAs(pduObj)));
return;
}
const concat = concatOf(pduObj);
if (!concat) {