Name the retry status by direction, and correct what the held-message decision claims
Test / lint (pull_request) Successful in 28s
Test / test (18) (pull_request) Successful in 31s
Test / test (20) (pull_request) Successful in 30s
Test / test (22) (pull_request) Successful in 31s
Test / test (24) (pull_request) Successful in 30s
Test / test (26) (pull_request) Successful in 37s
Mirror / push (push) Successful in 4s

This commit was merged in pull request #29.
This commit is contained in:
2026-09-26 14:53:09 +02:00
parent f2977638f2
commit 42b1239f3d
5 changed files with 28 additions and 29 deletions
+2 -2
View File
@@ -304,8 +304,8 @@ the file.
- The drain's wait on the application ignores `shutdownTimeout: 0`.
- What the application holds unanswered is capped on constants, and a message past the cap is
refused.
- A store at its bound answers `ESME_RTHROTTLED` to a `submit_sm` and `ESME_RX_T_APPN` to a
`deliver_sm`.
- A store at its bound answers `ESME_RTHROTTLED` to a submission and `ESME_RX_T_APPN` to a delivery,
a `data_sm` by whichever it stands in for.
- A reconnect keeps the delivery-receipt merges; everything else the link held is dropped.
- A message id base is merged at most once.
- A send that never reached the socket waits for the next link; one that did is counted, not resent.
+1 -1
View File
@@ -42,7 +42,7 @@
the cap is lost, since its segments were already answered.
- A message arriving while the application holds 1000 unanswered, or 64 MiB of them counted the way
`maxOctets` counts segments, is refused with `ESME_RTHROTTLED` (`ESME_RX_T_APPN` on a
`deliver_sm`), so the peer keeps it and retries. **Call `sendResp()` on every `sms`, multipart
delivery), so the peer keeps it and retries. **Call `sendResp()` on every `sms`, multipart
included**: 1000 left unanswered now stop inbound traffic for up to five minutes, where the oldest
used to be dropped with a warning.
- A `submit_sm` segment the reassembly buffer has no room for is refused with `ESME_RTHROTTLED`,
+6 -7
View File
@@ -432,13 +432,12 @@ const { err, pduObj } = await session.send({
each is two messages.
- **Answered on arrival.** Each segment was answered as it landed, before you see the message:
[Server in depth](#server-in-depth).
- **Unanswered messages.** While 1000 messages you have not called `sendResp()` on, or 64 MiB of
them counted the way `maxOctets` counts segments, are held, every new message is refused with
`ESME_RTHROTTLED` (`ESME_RX_T_APPN` on a `deliver_sm`) so the peer retries it, and no `sms` fires.
Reaching the bound logs one `warn`, and the first message accepted once both are down to half one
`info`. A message left five
minutes is dropped from the count with a `warn`; a later `sendResp()` still answers it. Neither
bound is an option.
- **Unanswered messages.** While a session holds 1000 messages you have not called `sendResp()` on,
or 64 MiB of them counted the way `maxOctets` counts segments, every new message and segment is
refused so the peer retries it: `ESME_RTHROTTLED` on a submission, `ESME_RX_T_APPN` on a delivery.
No `sms` fires. Reaching the bound logs one `warn`, and the first message accepted once both are
down to half one `info`. A message left five minutes is dropped from the count with a `warn`; a
later `sendResp()` still answers it. None of the three is an option.
- **Where the body is.** A body in the `message_payload` TLV, SMPP's way of carrying up to 64 KB and
the only place a `data_sm` has, reads exactly like one in `short_message`, concatenated messages
and receipts included. A PDU filling both is read from `short_message`.
+18 -18
View File
@@ -679,25 +679,25 @@ rule and an index of the titles below.
- **What the application holds unanswered is capped on constants, and a message past the cap is
refused.** A bound the application cannot raise is the point: an application that answers nothing
would otherwise grow it for the life of the link, which goal 4 forbids. Reassembly's `maxOctets`
is an option because it bounds what the peer sends; this bounds what the application leaves
unanswered. Maintainer's call, 2026-09-26: a message arriving past the cap is refused with the
retry status, so the peer keeps it (goal 2). Rejected: dropping the oldest to make room, which
frees nothing while the application still holds its `Sms`, and stops the drain waiting for a
message the peer is owed. Rejected: pausing the socket, which also stalls every answer and
`enquire_link` on the link. Reaching the bound is a log line and nothing more (goal 8): an event
or a public count would be surface for what the application already knows, since it is the one
not answering. A message held past its timeout is still dropped, so `close()` can
report fewer unanswered than there were — accepted, because the alternative is holding what
nothing will answer.
would otherwise hold ever more messages, which goal 4 forbids. Reassembly's `maxOctets` is an
option because it bounds what the peer sends; this bounds what the application leaves unanswered.
Maintainer's call, 2026-09-26. Refusing leaves the message with the peer, which will send it again
(goal 2). Rejected: dropping the oldest to make room, which frees nothing while the application
still holds its `Sms`, and stops the drain waiting for a message the peer is owed. Rejected:
pausing the socket, which also stalls every answer and `enquire_link` on the link. Reaching the
bound shows only in the log (goal 8): an event or a public count would be surface for what the
application already knows, since it is the one not answering. A message held past its timeout is
still dropped, so `close()` can report fewer unanswered than there were — accepted, because the
alternative is holding what nothing will answer.
- **A store at its bound answers `ESME_RTHROTTLED` to a `submit_sm` and `ESME_RX_T_APPN` to a
`deliver_sm`.** Maintainer's call, 2026-09-26, for reassembly and held messages alike, so "keep it
and retry" has one spelling per direction. `ESME_RTHROTTLED` asks the sender to slow down, which
is what the peer outrunning us needs, and operators send it (Vonage, LINK Mobility, Route Mobile,
Jasmin), so clients built against them meet it (goal 1). Rejected: `ESME_RMSGQFUL`, which names an
exhausted queue and no rate. `ESME_RTHROTTLED` is the SMSC's to send, so an ESME answers with
SMPP 3.4's temporary receiver error, the one an SMSC retries on (goal 3).
- **A store at its bound answers `ESME_RTHROTTLED` to a submission and `ESME_RX_T_APPN` to a
delivery, a `data_sm` by whichever it stands in for.** Maintainer's call, 2026-09-26, for
reassembly and held messages alike, so "keep it and retry" has one spelling per direction.
`ESME_RTHROTTLED` asks the sender to slow down, which is what the peer outrunning us needs, and
operators send it (Vonage, LINK Mobility, Route Mobile, Jasmin), so clients built against them
meet it (goal 1). Rejected: `ESME_RMSGQFUL`, which names an exhausted queue and no rate.
`ESME_RTHROTTLED` is the SMSC's to send, so an ESME answers with SMPP 3.4's temporary receiver
error, the one an SMSC retries on (goal 3).
- **A reconnect keeps the delivery-receipt merges; everything else the link held is dropped.**
`onDelivery()` answers each receipt before the group it belongs to is complete, and `teardown()`
+1 -1
View File
@@ -19,7 +19,7 @@ import { paramText } from './defs/types.ts';
import { respIdParams, segmentId } from './sms-id.ts';
import { respNameFor } from './defs/commands.ts';
/** Asks the peer to keep the message and retry: RTHROTTLED is the SMSC's to send, so an ESME's is another. */
/** Asks the peer to keep the message and retry. */
function throttledStatus(carriedAs: string): ErrorName {
return carriedAs === 'submit_sm' ? 'ESME_RTHROTTLED' : 'ESME_RX_T_APPN';
}