Re-bind a dropped client link by default, and take false to turn it off
Surviving a drop is most of what the session layer is for, and behind an
opt-in an application that never read the options table got none of it.
`reconnect` still takes `{ minDelay, maxDelay }` to retune the backoff;
`false` is the one spelling for off.
This commit is contained in:
@@ -288,6 +288,14 @@ exactly 140.
|
|||||||
and fail on every developer machine, and a committed key leaks in a public repository. Valid while
|
and fail on every developer machine, and a committed key leaks in a public repository. Valid while
|
||||||
the dev image has no openssl.
|
the dev image has no openssl.
|
||||||
|
|
||||||
|
- **A client re-binds after a drop unless it is told not to.** Maintainer's call, 2026-08-31:
|
||||||
|
surviving a dropped link is most of what the session layer is for, and behind an opt-in an
|
||||||
|
application that never read the options table got none of it. `reconnect` takes
|
||||||
|
`{ minDelay, maxDelay }` to retune the backoff and `false` to turn it off, so absent means on and
|
||||||
|
there is one spelling for each. Only `client()` reconnects — a `server()` session is a connection
|
||||||
|
the peer opened, and nothing at this end can reopen it. The retry timer is `unref()`'d, so a
|
||||||
|
process with nothing else left to do still exits between attempts.
|
||||||
|
|
||||||
- **The notation a peer writes message ids in is named per place, and normalisation never reaches
|
- **The notation a peer writes message ids in is named per place, and normalisation never reaches
|
||||||
inside a `<base>-<n>` id.** An SMSC may answer `submit_sm_resp` in hex and write the receipt's
|
inside a `<base>-<n>` id.** An SMSC may answer `submit_sm_resp` in hex and write the receipt's
|
||||||
`id:` in decimal, so one transform over both sides cannot make them equal — `smsIdFormat` names
|
`id:` in decimal, so one transform over both sides cannot make them equal — `smsIdFormat` names
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ by hand on top of a library; it is built in here.
|
|||||||
| | |
|
| | |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| **Keepalive** | `enquire_link` every 20 s on a quiet link, and a peer that stops answering is dropped. |
|
| **Keepalive** | `enquire_link` every 20 s on a quiet link, and a peer that stops answering is dropped. |
|
||||||
| **Reconnect with backoff** | Opt-in `reconnect` reopens the socket and re-binds, 1 s doubling to 30 s. |
|
| **Reconnect with backoff** | A dropped client link reopens the socket and re-binds by default, 1 s doubling to 30 s. |
|
||||||
| **Submit window** | `maxOutstanding` holds requests in flight at 10; further sends queue instead of overrunning the SMSC. |
|
| **Submit window** | `maxOutstanding` holds requests in flight at 10; further sends queue instead of overrunning the SMSC. |
|
||||||
| **Delivery receipts** | Correlated by `receipted_message_id`/`message_state` where the SMSC sends them, falling back to parsing the receipt text — what Kannel and several others send. |
|
| **Delivery receipts** | Correlated by `receipted_message_id`/`message_state` where the SMSC sends them, falling back to parsing the receipt text — what Kannel and several others send. |
|
||||||
| **Multipart** | Long messages split on send; concatenated `deliver_sm` reassembled into one `sms`. |
|
| **Multipart** | Long messages split on send; concatenated `deliver_sm` reassembled into one `sms`. |
|
||||||
@@ -99,12 +99,12 @@ Every one is optional.
|
|||||||
| `systemType`, `addressRange`, `addrTon`, `addrNpi` | `''`, `''`, `0`, `0` | The remaining bind fields, for operators that require them. |
|
| `systemType`, `addressRange`, `addrTon`, `addrNpi` | `''`, `''`, `0`, `0` | The remaining bind fields, for operators that require them. |
|
||||||
| `tls` | `false` | `true` for defaults, or a `tls.ConnectionOptions` object for a private CA or a client certificate. |
|
| `tls` | `false` | `true` for defaults, or a `tls.ConnectionOptions` object for a private CA or a client certificate. |
|
||||||
| `enquireLinkInterval` | `20000` | How often to send `enquire_link` on a quiet link. |
|
| `enquireLinkInterval` | `20000` | How often to send `enquire_link` on a quiet link. |
|
||||||
| `idleTimeout` | `2 × enquireLinkInterval` | Give up on a link the peer has stopped answering; with `reconnect` set, it re-binds. |
|
| `idleTimeout` | `2 × enquireLinkInterval` | Give up on a link the peer has stopped answering, and re-bind unless `reconnect` is `false`. |
|
||||||
| `responseTimeout` | `30000` | How long to wait for a response before giving up on it; `0` waits forever. |
|
| `responseTimeout` | `30000` | How long to wait for a response before giving up on it; `0` waits forever. |
|
||||||
| `shutdownTimeout` | `5000` | How long `close()` and `unbind()` wait for the requests this end already sent; `0` waits forever. |
|
| `shutdownTimeout` | `5000` | How long `close()` and `unbind()` wait for the requests this end already sent; `0` waits forever. |
|
||||||
| `maxOutstanding` | `10` | Requests allowed on the wire at once; further sends queue. |
|
| `maxOutstanding` | `10` | Requests allowed on the wire at once; further sends queue. |
|
||||||
| `smsIdFormat` | — | The notation the SMSC writes message ids in, per place it writes them: `{ receipt: 'decimal', submitResp: 'hex' }`. Only needed where the two disagree. |
|
| `smsIdFormat` | — | The notation the SMSC writes message ids in, per place it writes them: `{ receipt: 'decimal', submitResp: 'hex' }`. Only needed where the two disagree. |
|
||||||
| `reconnect` | off | `{ minDelay, maxDelay }` to re-bind automatically after a drop or an idle timeout, with exponential backoff. |
|
| `reconnect` | on | Re-binds after a drop or an idle timeout, backing off from `minDelay` 1 s to `maxDelay` 30 s. `{ minDelay, maxDelay }` retunes it; `false` turns it off, so a drop ends the session. |
|
||||||
| `log` | silent | Any object with `debug`, `error`, `info`, `verbose` and `warn` methods — see [Logging](#logging). |
|
| `log` | silent | Any object with `debug`, `error`, `info`, `verbose` and `warn` methods — see [Logging](#logging). |
|
||||||
| `signal` | — | An `AbortSignal` that cancels connecting and tears the session down. |
|
| `signal` | — | An `AbortSignal` that cancels connecting and tears the session down. |
|
||||||
|
|
||||||
@@ -317,7 +317,7 @@ TypeScript users can import `SmppLog` to have the compiler check one.
|
|||||||
| `dlr` | A delivery report arrives, one per segment. `smsId` is undefined when the peer marked a receipt whose body carries no readable id. `statusMsg` names `statusId` unless the peer sent a `message_state` this library cannot name — then `statusId` is that raw value and `statusMsg` is whatever the body said, or `UNKNOWN`. |
|
| `dlr` | A delivery report arrives, one per segment. `smsId` is undefined when the peer marked a receipt whose body carries no readable id. `statusMsg` names `statusId` unless the peer sent a `message_state` this library cannot name — then `statusId` is that raw value and `statusMsg` is whatever the body said, or `UNKNOWN`. |
|
||||||
| `messageDlr` | Every segment of a multipart message sent with `dlr: true` has been reported on, carrying the worst status of the segments. Merging needs the SMSC to number its segment ids `<base>-<n>`, which is this library's own server's convention — an SMSC that hands out unrelated ids per segment never fires it. A base is merged once: a later message the SMSC gives the same ids is reported on through `dlr` alone, and an earlier one still collecting loses its merged report as well. |
|
| `messageDlr` | Every segment of a multipart message sent with `dlr: true` has been reported on, carrying the worst status of the segments. Merging needs the SMSC to number its segment ids `<base>-<n>`, which is this library's own server's convention — an SMSC that hands out unrelated ids per segment never fires it. A base is merged once: a later message the SMSC gives the same ids is reported on through `dlr` alone, and an earlier one still collecting loses its merged report as well. |
|
||||||
| `close` | The connection closed. |
|
| `close` | The connection closed. |
|
||||||
| `reconnected` | The client re-bound after a drop (only with `reconnect` configured). |
|
| `reconnected` | The client re-bound after a drop. Never fires with `reconnect: false`. |
|
||||||
| `sessionError` | Something failed on a live session, including a hook or listener that threw or, if it was `async`, rejected. |
|
| `sessionError` | Something failed on a live session, including a hook or listener that threw or, if it was `async`, rejected. |
|
||||||
| `data` | Raw bytes arrived on the socket. |
|
| `data` | Raw bytes arrived on the socket. |
|
||||||
| `incomingPdu` | A complete PDU arrived, as a buffer. |
|
| `incomingPdu` | A complete PDU arrived, as a buffer. |
|
||||||
|
|||||||
+16
-12
@@ -1,6 +1,6 @@
|
|||||||
import type { ConnectionOptions } from 'node:tls';
|
import type { ConnectionOptions } from 'node:tls';
|
||||||
import type { Result, VoidResult } from './result.ts';
|
import type { Result, VoidResult } from './result.ts';
|
||||||
import type { BindType } from './session-options.ts';
|
import type { BindType, ReconnectOptions } from './session-options.ts';
|
||||||
import type { SmppLog } from './log.ts';
|
import type { SmppLog } from './log.ts';
|
||||||
import type { SmsIdFormat } from './sms-id.ts';
|
import type { SmsIdFormat } from './sms-id.ts';
|
||||||
import type { Socket } from 'node:net';
|
import type { Socket } from 'node:net';
|
||||||
@@ -26,7 +26,7 @@ export type ClientOptions = {
|
|||||||
maxOutstanding?: number;
|
maxOutstanding?: number;
|
||||||
password?: string;
|
password?: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
reconnect?: { maxDelay?: number; minDelay?: number };
|
reconnect?: { maxDelay?: number; minDelay?: number } | false;
|
||||||
responseTimeout?: number;
|
responseTimeout?: number;
|
||||||
shutdownTimeout?: number;
|
shutdownTimeout?: number;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
@@ -139,6 +139,19 @@ async function bind(session: Session, options: ClientOptions): Promise<VoidResul
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reconnectFor(options: ClientOptions): ReconnectOptions | undefined {
|
||||||
|
if (options.reconnect === false) return undefined;
|
||||||
|
|
||||||
|
const tuning = options.reconnect ?? {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
connect: () => openSocket(options),
|
||||||
|
maxDelay: tuning.maxDelay,
|
||||||
|
minDelay: tuning.minDelay,
|
||||||
|
onConnected: reconnected => bind(reconnected, options),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function createSession(options: ClientOptions, log: SmppLog, sock: Socket): Session {
|
function createSession(options: ClientOptions, log: SmppLog, sock: Socket): Session {
|
||||||
const enquireLinkInterval = options.enquireLinkInterval ?? defaults.enquireLinkInterval;
|
const enquireLinkInterval = options.enquireLinkInterval ?? defaults.enquireLinkInterval;
|
||||||
|
|
||||||
@@ -147,20 +160,11 @@ function createSession(options: ClientOptions, log: SmppLog, sock: Socket): Sess
|
|||||||
idleTimeout: options.idleTimeout ?? enquireLinkInterval * defaults.idleTimeoutFactor,
|
idleTimeout: options.idleTimeout ?? enquireLinkInterval * defaults.idleTimeoutFactor,
|
||||||
log,
|
log,
|
||||||
maxOutstanding: options.maxOutstanding,
|
maxOutstanding: options.maxOutstanding,
|
||||||
|
reconnect: reconnectFor(options),
|
||||||
responseTimeout: options.responseTimeout,
|
responseTimeout: options.responseTimeout,
|
||||||
shutdownTimeout: options.shutdownTimeout,
|
shutdownTimeout: options.shutdownTimeout,
|
||||||
smsIdFormat: options.smsIdFormat,
|
smsIdFormat: options.smsIdFormat,
|
||||||
sock,
|
sock,
|
||||||
...(options.reconnect
|
|
||||||
? {
|
|
||||||
reconnect: {
|
|
||||||
connect: () => openSocket(options),
|
|
||||||
maxDelay: options.reconnect.maxDelay,
|
|
||||||
minDelay: options.reconnect.minDelay,
|
|
||||||
onConnected: reconnected => bind(reconnected, options),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -326,6 +326,43 @@ describe('sendSms()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('reconnect', () => {
|
describe('reconnect', () => {
|
||||||
|
test('re-binds after a drop with nothing asked for, since it is the default', async t => {
|
||||||
|
const smpp = await startServer(t);
|
||||||
|
const { session } = await connect(t, smpp);
|
||||||
|
|
||||||
|
assert.ok(session);
|
||||||
|
|
||||||
|
const reconnected = once<true>(resolve => { session.on('reconnected', () => { resolve(true); }); });
|
||||||
|
|
||||||
|
await peerOf(smpp).close();
|
||||||
|
await reconnected;
|
||||||
|
|
||||||
|
assert.ok(session.loggedIn);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('schedules nothing after a drop when reconnect is false', async t => {
|
||||||
|
const smpp = await startServer(t);
|
||||||
|
const noop = (): void => undefined;
|
||||||
|
const infos: string[] = [];
|
||||||
|
const log: SmppLog = {
|
||||||
|
debug: noop,
|
||||||
|
error: noop,
|
||||||
|
info: msg => { infos.push(msg); },
|
||||||
|
verbose: noop,
|
||||||
|
warn: noop,
|
||||||
|
};
|
||||||
|
const { session } = await connect(t, smpp, { log, reconnect: false });
|
||||||
|
|
||||||
|
assert.ok(session);
|
||||||
|
|
||||||
|
const closed = once<true>(resolve => { session.on('close', () => { resolve(true); }); });
|
||||||
|
|
||||||
|
await peerOf(smpp).close();
|
||||||
|
await closed;
|
||||||
|
|
||||||
|
assert.ok(!infos.includes('reconnect - retrying after a drop'));
|
||||||
|
});
|
||||||
|
|
||||||
test('re-binds after the connection drops, keeping the same session object', async t => {
|
test('re-binds after the connection drops, keeping the same session object', async t => {
|
||||||
const smpp = await startServer(t);
|
const smpp = await startServer(t);
|
||||||
const messages: string[] = [];
|
const messages: string[] = [];
|
||||||
|
|||||||
@@ -168,10 +168,6 @@ session message is a change to every call site.
|
|||||||
Mirror the `onRequest` seam — return a `Dlr` to own the receipt, `undefined` to fall through
|
Mirror the `onRequest` seam — return a `Dlr` to own the receipt, `undefined` to fall through
|
||||||
to the built-in parser.
|
to the built-in parser.
|
||||||
|
|
||||||
- [ ] **Turn `reconnect` on by default in `client()`.** Surviving a dropped link is most of why the
|
|
||||||
session layer exists, and it is opt-in behind an empty object today, so an application that
|
|
||||||
does not read the options table gets none of it. A default change, so it needs a decision.
|
|
||||||
|
|
||||||
## Declined
|
## Declined
|
||||||
|
|
||||||
- **Throughput throttling — a TPS cap, and backing off on `ESME_RTHROTTLED`.** Two reasons, either
|
- **Throughput throttling — a TPS cap, and backing off on `ESME_RTHROTTLED`.** Two reasons, either
|
||||||
|
|||||||
Reference in New Issue
Block a user