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`. - 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 - What the application holds unanswered is capped on constants, and a message past the cap is
refused. refused.
- A store at its bound answers `ESME_RTHROTTLED` to a `submit_sm` and `ESME_RX_T_APPN` to a - A store at its bound answers `ESME_RTHROTTLED` to a submission and `ESME_RX_T_APPN` to a delivery,
`deliver_sm`. a `data_sm` by whichever it stands in for.
- A reconnect keeps the delivery-receipt merges; everything else the link held is dropped. - A reconnect keeps the delivery-receipt merges; everything else the link held is dropped.
- A message id base is merged at most once. - 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. - 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. 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 - 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 `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 included**: 1000 left unanswered now stop inbound traffic for up to five minutes, where the oldest
used to be dropped with a warning. used to be dropped with a warning.
- A `submit_sm` segment the reassembly buffer has no room for is refused with `ESME_RTHROTTLED`, - 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. each is two messages.
- **Answered on arrival.** Each segment was answered as it landed, before you see the message: - **Answered on arrival.** Each segment was answered as it landed, before you see the message:
[Server in depth](#server-in-depth). [Server in depth](#server-in-depth).
- **Unanswered messages.** While 1000 messages you have not called `sendResp()` on, or 64 MiB of - **Unanswered messages.** While a session holds 1000 messages you have not called `sendResp()` on,
them counted the way `maxOctets` counts segments, are held, every new message is refused with or 64 MiB of them counted the way `maxOctets` counts segments, every new message and segment is
`ESME_RTHROTTLED` (`ESME_RX_T_APPN` on a `deliver_sm`) so the peer retries it, and no `sms` fires. refused so the peer retries it: `ESME_RTHROTTLED` on a submission, `ESME_RX_T_APPN` on a delivery.
Reaching the bound logs one `warn`, and the first message accepted once both are down to half one No `sms` fires. Reaching the bound logs one `warn`, and the first message accepted once both are
`info`. A message left five down to half one `info`. A message left five minutes is dropped from the count with a `warn`; a
minutes is dropped from the count with a `warn`; a later `sendResp()` still answers it. Neither later `sendResp()` still answers it. None of the three is an option.
bound is an option.
- **Where the body is.** A body in the `message_payload` TLV, SMPP's way of carrying up to 64 KB and - **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 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`. 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 - **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 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` would otherwise hold ever more messages, which goal 4 forbids. Reassembly's `maxOctets` is an
is an option because it bounds what the peer sends; this bounds what the application leaves option because it bounds what the peer sends; this bounds what the application leaves unanswered.
unanswered. Maintainer's call, 2026-09-26: a message arriving past the cap is refused with the Maintainer's call, 2026-09-26. Refusing leaves the message with the peer, which will send it again
retry status, so the peer keeps it (goal 2). Rejected: dropping the oldest to make room, which (goal 2). Rejected: dropping the oldest to make room, which frees nothing while the application
frees nothing while the application still holds its `Sms`, and stops the drain waiting for a still holds its `Sms`, and stops the drain waiting for a message the peer is owed. Rejected:
message the peer is owed. Rejected: pausing the socket, which also stalls every answer and pausing the socket, which also stalls every answer and `enquire_link` on the link. Reaching the
`enquire_link` on the link. Reaching the bound is a log line and nothing more (goal 8): an event bound shows only in the log (goal 8): an event or a public count would be surface for what the
or a public count would be surface for what the application already knows, since it is the one application already knows, since it is the one not answering. A message held past its timeout is
not answering. A message held past its timeout is still dropped, so `close()` can still dropped, so `close()` can report fewer unanswered than there were — accepted, because the
report fewer unanswered than there were — accepted, because the alternative is holding what alternative is holding what nothing will answer.
nothing will answer.
- **A store at its bound answers `ESME_RTHROTTLED` to a `submit_sm` and `ESME_RX_T_APPN` to a - **A store at its bound answers `ESME_RTHROTTLED` to a submission and `ESME_RX_T_APPN` to a
`deliver_sm`.** Maintainer's call, 2026-09-26, for reassembly and held messages alike, so "keep it delivery, a `data_sm` by whichever it stands in for.** Maintainer's call, 2026-09-26, for
and retry" has one spelling per direction. `ESME_RTHROTTLED` asks the sender to slow down, which reassembly and held messages alike, so "keep it and retry" has one spelling per direction.
is what the peer outrunning us needs, and operators send it (Vonage, LINK Mobility, Route Mobile, `ESME_RTHROTTLED` asks the sender to slow down, which is what the peer outrunning us needs, and
Jasmin), so clients built against them meet it (goal 1). Rejected: `ESME_RMSGQFUL`, which names an operators send it (Vonage, LINK Mobility, Route Mobile, Jasmin), so clients built against them
exhausted queue and no rate. `ESME_RTHROTTLED` is the SMSC's to send, so an ESME answers with meet it (goal 1). Rejected: `ESME_RMSGQFUL`, which names an exhausted queue and no rate.
SMPP 3.4's temporary receiver error, the one an SMSC retries on (goal 3). `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.** - **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()` `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 { respIdParams, segmentId } from './sms-id.ts';
import { respNameFor } from './defs/commands.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 { function throttledStatus(carriedAs: string): ErrorName {
return carriedAs === 'submit_sm' ? 'ESME_RTHROTTLED' : 'ESME_RX_T_APPN'; return carriedAs === 'submit_sm' ? 'ESME_RTHROTTLED' : 'ESME_RX_T_APPN';
} }