Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .changeset/enquire-link-keepalive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@semyonf/smpp': minor
---

Add `enquire_link` handling to `Session`: answering incoming `enquire_link`, and dropping the link
when an outgoing one is not answered.

Until now an incoming `enquire_link` was only emitted as an event, and `auto_enquire_link_period`
sent `enquire_link` without ever checking the `enquire_link_resp`, so a dead link stayed "open"
until TCP noticed, which can take many minutes. New options, accepted by both `smpp.connect()` and
`smpp.createServer()`:

- `auto_enquire_link_response` (default `false`) answers every incoming `enquire_link`.
- `enquire_link_timeout` makes every `enquire_link` sent by `auto_enquire_link_period` wait that
many milliseconds for its response. If none arrives, an `enquire_link_timeout` event is emitted
with the unanswered `enquire_link`, and the session is destroyed unless
`close_on_enquire_link_timeout` is `false`.
- `auto_enquire_link_period` now also works on server sessions, and stops when `session.close()` or
`session.destroy()` is called instead of firing at a socket that can no longer be written to.
After `close()` the response to an `enquire_link` still in flight is still awaited, so if the
peer stopped answering and never closes its side, the timeout destroys the session instead of
leaving it half-open.

These `enquire_link` are not subject to `response_timeout`, which is for the requests you send
yourself, and never show up in `'response_timeout'` or `'response_aborted'` events.

All of them default to the previous behaviour. At runtime they are controlled with
`session.startEnquireLink(period, [timeout])`, `session.stopEnquireLink()` and the
`session.autoEnquireLinkResponse` property.
26 changes: 26 additions & 0 deletions .changeset/session-reconnect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
'@semyonf/smpp': patch
---

Fix `session.connect()`, which could not reconnect a client session: after reconnecting, the session
sent PDUs but never received one, so every response and incoming request was silently lost.

`connect()` reused the socket of the previous connection, and a closed `net.Socket` that connects
again never emits `'readable'`. Each call now opens a new socket, and also:

- resets everything bound to the previous connection: sequence numbers, a partly received PDU
(which used to corrupt the first PDU of the new connection), the read counters behind the
`socket.data.in` debug and metrics events, and the `closed` state, so `close(callback)` after a
reconnect waits for the new connection to close. Requests still awaiting a response fail with
`ESESSIONCLOSED` when `response_timeout` is set, and are dropped otherwise, as before;
- re-arms `connectTimeout` for every attempt;
- stops emitting events of the previous connection. Reconnecting from an `'error'` listener used to
be followed by that connection's `'close'`, which made the session look closed again;
- destroys the previous connection first if it is still open or still being established, instead
of connecting its socket again, which failed with `EALREADY` while it was still connecting;
- throws a clear error on a server session, instead of Node's `ERR_MISSING_ARGS`, and leaves the
session untouched when `net.connect()` or `tls.connect()` reject the options.

Listeners, options and the `enquire_link` schedule are kept across the reconnect, and reconnecting
from an `enquire_link_timeout` listener keeps the new connection instead of having
`close_on_enquire_link_timeout` destroy it.
91 changes: 90 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,51 @@ still emitted as a `'pdu'` event and as its command event (e.g. `'submit_sm_resp
Without `response_timeout`, nothing changes: unanswered requests are never reported, and a
`failureCallback` is only called for requests that could not be written to the socket.

### Keeping the link alive (enquire_link)

Both sides of an SMPP session are expected to answer `enquire_link` with `enquire_link_resp`, and
to drop a link whose peer stops answering. Both are opt-in, on client sessions (`smpp.connect()`
options) and server sessions (`smpp.createServer()` options, applied to every accepted session):

| Option | Default | Description |
|---|---|---|
| `auto_enquire_link_response` | `false` | Answer every incoming `enquire_link` with an `enquire_link_resp`. Don't also answer it from your own `enquire_link` handler, or the peer gets two responses. |
| `auto_enquire_link_period` | unset | Send an `enquire_link` every this many ms. Client sessions start one period after the TCP connection is established (so before the TLS handshake and the bind), server sessions one period after accepting the connection. |
| `enquire_link_timeout` | unset | How long, in ms, to wait for the `enquire_link_resp` to each of those `enquire_link`. When it does not arrive in time, an `enquire_link_timeout` event is emitted. Unset or `0` disables the check. |
| `close_on_enquire_link_timeout` | `true` | Destroy the session after emitting `enquire_link_timeout`. Set to `false` to decide on your own in the event handler. |

``` javascript
var session = smpp.connect({
url: 'smpp://example.com:2775',
auto_enquire_link_response: true,
auto_enquire_link_period: 30000,
enquire_link_timeout: 10000
});
session.on('enquire_link_timeout', function(pdu) {
console.log('no enquire_link_resp for sequence', pdu.sequence_number, ', dropping the link');
});
session.on('close', function() {
setTimeout(function() {
session.connect(); // the enquire_link options apply to the new connection too
}, 5000);
});
```

The same can be controlled at runtime with `session.startEnquireLink()`, `session.stopEnquireLink()`
and `session.autoEnquireLinkResponse`.

The `enquire_link` sent by `auto_enquire_link_period` only wait for `enquire_link_timeout`:
`response_timeout` doesn't apply to them, and they never show up in `'response_timeout'` or
`'response_aborted'` events. An `enquire_link` you send yourself is an ordinary request.

Any response carrying the sequence number of the `enquire_link` counts as an answer, including a
`generic_nack`: some SMSCs reject an `enquire_link` sent before the bind that way, but they still
answered, so the link is alive.

Responses are only read while the session is not paused, so keep `session.pause()` shorter than
`enquire_link_timeout`, or the link is considered dead. A paused session doesn't answer incoming
`enquire_link` automatically either, so the peer's own timeout applies as well.

### Proxy protocol

[Proxy Protocol v1](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) is now
Expand Down Expand Up @@ -293,14 +338,49 @@ that don't honor gracefull tear-down. ( Looking at you SMPPSim )
If supplied, the `callback` is called once the session is fully closed.

#### session.connect()
Can be used to reconnect a closed connection.
Connects a client session again, typically from a `'close'`, `'error'` or `'enquire_link_timeout'`
listener. The session gets a new connection: sequence numbers start over, and a PDU the previous
connection delivered only partly is discarded. Requests still waiting for a response on the
previous connection fail with `ESESSIONCLOSED` if `response_timeout` is set (see
[Response timeout](#response-timeout)), and are dropped otherwise. Event listeners, options and
the `enquire_link` schedule are kept, and the connect listener passed to `smpp.connect()` runs
again.

If the previous connection is still open or still being established, it is destroyed first. Either
way none of its events are emitted any more, so a `'close'` that the previous connection emits
after the error that you reconnected on does not reach your listeners.

Throws when called on a server session, and when `net.connect()` or `tls.connect()` reject the
options, in which case the session is left as it was.

#### session.pause()
Can be used to postpone incoming pdu events untill calling `session.resume()`.

#### session.resume()
Resumes the session after a call to `pause()`.

#### session.startEnquireLink(period, [timeout])
Sends an `enquire_link` every `period` ms. If `timeout` is given (it defaults to the
`enquire_link_timeout` option), every `enquire_link` that gets no response within `timeout` ms
emits `enquire_link_timeout` and, unless `close_on_enquire_link_timeout` is `false`, destroys the
session. Replaces the schedule set by a previous call or by the `auto_enquire_link_period` option;
`enquire_link` already sent are still awaited with their original timeout. The schedule is kept
across `session.connect()`.
On a client session that is not connected yet, the schedule starts once the connection is
established.

#### session.stopEnquireLink()
Stops sending `enquire_link` and stops waiting for responses to the ones already sent.
`session.close()` and `session.destroy()` also stop sending them. After `session.close()` the
response to an `enquire_link` still in flight is awaited, so if the peer never closes its side of
the connection, the timeout destroys the session (unless `close_on_enquire_link_timeout` is
`false`). With nothing in flight, `session.close()` waits for the peer like before: call
`session.destroy()` if the `'close'` event doesn't follow in time.

#### session.autoEnquireLinkResponse
Whether incoming `enquire_link` are answered automatically. Initialized from the
`auto_enquire_link_response` option and can be changed at any time.

#### session.getPeerCertificate([detailed])
Returns the certificate presented by the TLS peer, as returned by node's
[`tls.TLSSocket.getPeerCertificate()`](https://nodejs.org/api/tls.html#tlssocketgetpeercertificatedetailed).
Expand Down Expand Up @@ -349,6 +429,12 @@ Emitted when a pdu is being sent over the session with the pdu as the argument.
#### Event: 'pdu' `(pdu)`
Emitted upon receiving a pdu.

#### Event: 'enquire_link_timeout' `(pdu)`
Emitted when an `enquire_link` sent by `auto_enquire_link_period` or `session.startEnquireLink()`
got no `enquire_link_resp` within the timeout. `pdu` is that `enquire_link`. Unless
`close_on_enquire_link_timeout` is `false`, the session is destroyed right after, so a `'close'`
event follows.

#### Event: 'unknown' `(pdu)`
Emitted upon receiving an unknown pdu.

Expand All @@ -369,6 +455,9 @@ Creates a new SMPP server. The `sessionListener` argument is automatically set
as a listener for the 'session' event.
If options include `key` and `cert`, a TLS secured server will be created.
Include `rejectUnauthorized: false` to disable the certificate validation.
The `enquire_link` options described in
[Keeping the link alive](#keeping-the-link-alive-enquire_link) apply to every session the server
accepts.

### smpp.Server
The base object for a SMPP server created with `smpp.createServer()`.
Expand Down
Loading
Loading