Stop accepting before the drain and report the link that dropped during it
This commit is contained in:
+3
-3
@@ -196,18 +196,18 @@ export async function client(options: ClientOptions = {}): Promise<Result<{ sess
|
||||
const signal = options.signal;
|
||||
|
||||
if (signal?.aborted === true) {
|
||||
void session.close();
|
||||
void session.close({ signal });
|
||||
|
||||
return { err: new Error('Aborted before binding') };
|
||||
}
|
||||
|
||||
// Registered before the bind: an abort landing while it is in flight has to close the session.
|
||||
signal?.addEventListener('abort', () => { void session.close(); }, { once: true });
|
||||
signal?.addEventListener('abort', () => { void session.close({ signal }); }, { once: true });
|
||||
|
||||
const bound = await bind(session, options);
|
||||
|
||||
if (bound.err) {
|
||||
void session.close();
|
||||
void session.close({ signal });
|
||||
|
||||
return { err: bound.err };
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export type {
|
||||
ServerOptions,
|
||||
} from './server.ts';
|
||||
export type {
|
||||
CloseOptions,
|
||||
MessageDlr,
|
||||
ReconnectOptions,
|
||||
SendOptions,
|
||||
|
||||
+15
-3
@@ -37,10 +37,20 @@ export class SendWindow {
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolves once nothing is in flight, or on the timeout with how many still are. 0 never times out. */
|
||||
idle(timeout: number): Promise<number> {
|
||||
/** Everything the caller is still owed: on the wire, plus queued behind a full window. */
|
||||
unfinished(): number {
|
||||
return this.inFlight + this.waiting.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves 0 once nothing is left, or with what still is when the timeout or the signal cuts the
|
||||
* wait short. A timeout of 0 waits forever.
|
||||
*/
|
||||
idle(timeout: number, signal?: AbortSignal): Promise<number> {
|
||||
if (this.inFlight === 0) return Promise.resolve(0);
|
||||
|
||||
if (signal?.aborted === true) return Promise.resolve(this.unfinished());
|
||||
|
||||
return new Promise<number>(resolve => {
|
||||
let timer: NodeJS.Timeout | undefined = undefined;
|
||||
const done = (): void => {
|
||||
@@ -49,7 +59,8 @@ export class SendWindow {
|
||||
if (timer) clearTimeout(timer);
|
||||
if (index !== -1) this.waitingForIdle.splice(index, 1);
|
||||
|
||||
resolve(this.inFlight);
|
||||
signal?.removeEventListener('abort', done);
|
||||
resolve(this.unfinished());
|
||||
};
|
||||
|
||||
if (timeout > 0) {
|
||||
@@ -57,6 +68,7 @@ export class SendWindow {
|
||||
timer.unref();
|
||||
}
|
||||
|
||||
signal?.addEventListener('abort', done, { once: true });
|
||||
this.waitingForIdle.push(done);
|
||||
});
|
||||
}
|
||||
|
||||
+11
-5
@@ -1,3 +1,4 @@
|
||||
import type { CloseOptions } from './session-options.ts';
|
||||
import type { PduObject, TlvInput } from './pdu.ts';
|
||||
import type { Result, VoidResult } from './result.ts';
|
||||
import type { Server as NetServer, Socket } from 'node:net';
|
||||
@@ -115,19 +116,22 @@ export class SmppServer extends EventEmitter<ServerEvents> {
|
||||
if (event !== 'serverError') this.emit('serverError', error);
|
||||
}
|
||||
|
||||
/** Stops listening and closes every live session, draining each one first. */
|
||||
async close(): Promise<void> {
|
||||
/** Stops listening, then drains and closes every session that was live when it stopped. */
|
||||
async close(options: CloseOptions = {}): Promise<void> {
|
||||
// Before the drain, or the listener keeps accepting connections nothing will ever close.
|
||||
const stopped = new Promise<void>(resolve => { this.server.close(() => { resolve(); }); });
|
||||
const live = [...this.sessions];
|
||||
|
||||
this.sessions.clear();
|
||||
|
||||
await Promise.all(live.map(async session => {
|
||||
const closed = await session.close().catch((thrown: unknown) => ({ err: errorFrom(thrown) }));
|
||||
const closed = await session.close(options)
|
||||
.catch((thrown: unknown) => ({ err: errorFrom(thrown) }));
|
||||
|
||||
if (closed.err) this.emit('serverError', closed.err);
|
||||
}));
|
||||
|
||||
return new Promise(resolve => { this.server.close(() => { resolve(); }); });
|
||||
return stopped;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,7 +315,9 @@ function onListening(listener: NetServer, smpp: SmppServer, options: ServerOptio
|
||||
|
||||
log.info('server - listening', { host: options.host ?? '*', port: smpp.port });
|
||||
|
||||
options.signal?.addEventListener('abort', () => { void smpp.close(); }, { once: true });
|
||||
const signal = options.signal;
|
||||
|
||||
signal?.addEventListener('abort', () => { void smpp.close({ signal }); }, { once: true });
|
||||
}
|
||||
|
||||
/** Starts listening for SMPP connections. Resolves once the socket is bound. */
|
||||
|
||||
@@ -49,6 +49,9 @@ export function bindCarries(bindType: BindType | undefined, cmdName: string): bo
|
||||
|
||||
export type SendOptions = { signal?: AbortSignal | undefined };
|
||||
|
||||
/** An already-aborted signal skips the drain; one that fires during it cuts the wait short. */
|
||||
export type CloseOptions = { signal?: AbortSignal | undefined };
|
||||
|
||||
/**
|
||||
* First refusal on every incoming request. Returning true means the hook answered it and the
|
||||
* built-in handling is skipped — this is how the server owns bind without the session also
|
||||
|
||||
+31
-27
@@ -2,7 +2,7 @@ import type { ErrorName } from './defs/errors.ts';
|
||||
import type { MessageDlr } from './dlr-merger.ts';
|
||||
import type { ParamValue } from './defs/types.ts';
|
||||
import type { PduObject, PduObjectInput, TlvInput } from './pdu.ts';
|
||||
import type { BindType, ReconnectOptions, SendOptions, SessionEvents, SessionOptions } from './session-options.ts';
|
||||
import type { BindType, CloseOptions, ReconnectOptions, SendOptions, SessionEvents, SessionOptions } from './session-options.ts';
|
||||
import type { Result, VoidResult } from './result.ts';
|
||||
import type { SendSmsOptions, SendSmsResult } from './send-sms.ts';
|
||||
import type { SmppLog } from './log.ts';
|
||||
@@ -23,6 +23,7 @@ import { silentLog } from './log.ts';
|
||||
import { submitSms } from './send-sms.ts';
|
||||
|
||||
export type {
|
||||
CloseOptions,
|
||||
MessageDlr,
|
||||
ReconnectOptions,
|
||||
SendOptions,
|
||||
@@ -221,17 +222,15 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
* unbind, which is fine. Reports the unbind's own failure ahead of an unfinished drain.
|
||||
*/
|
||||
async unbind(): Promise<VoidResult> {
|
||||
this.reconnectLoop?.stop();
|
||||
|
||||
const drained = await this.drain();
|
||||
const drained = await this.drain(undefined);
|
||||
const wasOpen = !this.closed;
|
||||
// request(), not send(): the drain gate would refuse it, and the window is empty by now.
|
||||
// request(), not send(): the drain gate refuses a send, and the unbind goes out either way.
|
||||
const sent = wasOpen
|
||||
? await this.request({ cmdName: 'unbind' }, {})
|
||||
: { err: new Error('Session is closed') };
|
||||
const closedOnUnbind = wasOpen && this.closed;
|
||||
|
||||
this.shutdown();
|
||||
this.end();
|
||||
|
||||
return sent.err && !closedOnUnbind ? { err: sent.err } : drained;
|
||||
}
|
||||
@@ -240,12 +239,10 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
* Closes for good: refuses new sends, waits out the requests already on the wire up to
|
||||
* `shutdownTimeout`, then tears down whatever is left. A session closed this way never reconnects.
|
||||
*/
|
||||
async close(): Promise<VoidResult> {
|
||||
this.reconnectLoop?.stop();
|
||||
async close(options: CloseOptions = {}): Promise<VoidResult> {
|
||||
const drained = await this.drain(options.signal);
|
||||
|
||||
const drained = await this.drain();
|
||||
|
||||
this.shutdown();
|
||||
this.end();
|
||||
|
||||
return drained;
|
||||
}
|
||||
@@ -330,34 +327,41 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
return response;
|
||||
}
|
||||
|
||||
/** Stops new sends and waits out the ones already issued. A dead link has nothing to wait for. */
|
||||
private async drain(): Promise<VoidResult> {
|
||||
/**
|
||||
* Stops new sends and waits out the ones already issued, which only covers what this end sent:
|
||||
* a request the peer sent us is answered through sendReturn(), which never enters the window.
|
||||
*/
|
||||
private async drain(signal: AbortSignal | undefined): Promise<VoidResult> {
|
||||
this.reconnectLoop?.stop();
|
||||
this.draining = true;
|
||||
this.timers.clear();
|
||||
|
||||
if (this.closed || this.sock.destroyed) return {};
|
||||
|
||||
const timeout = this.options.shutdownTimeout ?? defaults.shutdownTimeout;
|
||||
const inFlight = await this.window.idle(timeout);
|
||||
const unfinished = await this.window.idle(timeout, signal);
|
||||
|
||||
if (inFlight === 0) return {};
|
||||
// The window empties on a drop too: teardown() settles everything the link was carrying.
|
||||
if (this.isClosed()) return { err: new Error('The link dropped before the drain finished') };
|
||||
|
||||
this.log.warn('session - shutting down with requests still in flight', { inFlight, timeout });
|
||||
if (unfinished === 0) return {};
|
||||
|
||||
return { err: new Error(`Shut down with ${String(inFlight)} request(s) still in flight`) };
|
||||
this.log.warn('session - shutting down with requests unfinished', { timeout, unfinished });
|
||||
|
||||
return { err: new Error(`Shut down with ${String(unfinished)} request(s) unfinished`) };
|
||||
}
|
||||
|
||||
private shutdown(): void {
|
||||
/** Read through a method: teardown() can land while the drain is awaiting. */
|
||||
private isClosed(): boolean {
|
||||
return this.closed;
|
||||
}
|
||||
|
||||
/** The session is over now, drained or not. Nothing brings it back. */
|
||||
private end(): void {
|
||||
this.reconnectLoop?.stop();
|
||||
this.teardown();
|
||||
this.dlrMerger.clear();
|
||||
}
|
||||
|
||||
/** The link is unusable, so nothing can answer and there is nothing to drain. */
|
||||
private abort(): void {
|
||||
this.reconnectLoop?.stop();
|
||||
this.shutdown();
|
||||
}
|
||||
|
||||
private teardown(): void {
|
||||
if (this.closed) return;
|
||||
|
||||
@@ -395,7 +399,7 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
if (framed.err) {
|
||||
this.log.warn('session - unusable stream, closing', { message: framed.err.message });
|
||||
this.emit('sessionError', framed.err);
|
||||
this.abort();
|
||||
this.end();
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -416,7 +420,7 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
message: parsed.err.message,
|
||||
});
|
||||
this.emit('sessionError', parsed.err);
|
||||
this.abort();
|
||||
this.end();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user