Retry the first connect and bind on reconnect fromStart (#80)
* Regression tests for a client that retries its first connect and bind
* Retry the first connect and bind on reconnect: { fromStart: true }
* Pin that the signal bounding the first bind also closes the session
* Name a failed connect once, default the backoff where it lives, and fix the deadline advice
* Carry the failure that started the retries on the abort that ends them
* Pin one retry announcement per attempt when a link drops mid-bind
* Announce a retry when its wait is over, not when it is scheduled
* Say what the process-hold assertion expects, not what a failure would mean
This commit is contained in:
+21
-9
@@ -2,18 +2,27 @@ import type { Result, VoidResult } from './result.ts';
|
||||
import type { SmppLog } from './log.ts';
|
||||
import type { Socket } from 'node:net';
|
||||
|
||||
export const backoffDefaults = {
|
||||
maxDelay: 30_000,
|
||||
minDelay: 1000,
|
||||
};
|
||||
|
||||
export type ReconnectLoopOptions = {
|
||||
connect: () => Promise<Result<{ sock: Socket }>>;
|
||||
log: SmppLog;
|
||||
maxDelay: number;
|
||||
minDelay: number;
|
||||
maxDelay?: number | undefined;
|
||||
minDelay?: number | undefined;
|
||||
now?: (() => number) | undefined;
|
||||
/** Brings the owner back up on a freshly opened socket. An err means try again. */
|
||||
onConnected: (sock: Socket) => Promise<VoidResult>;
|
||||
/** Whether the wait between attempts lets the process exit. Default true. */
|
||||
unref?: boolean | undefined;
|
||||
};
|
||||
|
||||
/** Reopens a dropped connection, backing off between attempts until it is told to stop. */
|
||||
export class ReconnectLoop {
|
||||
private readonly maxDelay: number;
|
||||
private readonly minDelay: number;
|
||||
private readonly now: () => number;
|
||||
private readonly options: ReconnectLoopOptions;
|
||||
private attempting = false;
|
||||
@@ -23,9 +32,11 @@ export class ReconnectLoop {
|
||||
private upAt: number | undefined;
|
||||
|
||||
constructor(options: ReconnectLoopOptions) {
|
||||
this.maxDelay = options.maxDelay ?? backoffDefaults.maxDelay;
|
||||
this.minDelay = options.minDelay ?? backoffDefaults.minDelay;
|
||||
this.now = options.now ?? Date.now;
|
||||
this.options = options;
|
||||
this.delay = options.minDelay;
|
||||
this.delay = this.minDelay;
|
||||
}
|
||||
|
||||
/** Read through a method: stop() can land while an attempt is awaiting. */
|
||||
@@ -37,23 +48,24 @@ export class ReconnectLoop {
|
||||
if (this.timer || this.attempting || this.isStopped()) return;
|
||||
|
||||
// Coming up is not proof: a stream we cannot read is only found once the link is bound.
|
||||
if (this.upAt !== undefined && this.now() - this.upAt >= this.options.maxDelay) {
|
||||
this.delay = this.options.minDelay;
|
||||
if (this.upAt !== undefined && this.now() - this.upAt >= this.maxDelay) {
|
||||
this.delay = this.minDelay;
|
||||
}
|
||||
|
||||
this.upAt = undefined;
|
||||
|
||||
const delay = this.delay;
|
||||
|
||||
this.options.log.info('reconnect - retrying after a drop', { delay });
|
||||
|
||||
// Announced when the wait is over rather than when it starts: a cancelled one never happened.
|
||||
this.timer = setTimeout(() => {
|
||||
this.timer = undefined;
|
||||
this.options.log.info('reconnect - retrying', { delay });
|
||||
void this.run();
|
||||
}, delay);
|
||||
this.timer.unref();
|
||||
|
||||
this.delay = Math.min(delay * 2, this.options.maxDelay);
|
||||
if (this.options.unref ?? true) this.timer.unref();
|
||||
|
||||
this.delay = Math.min(delay * 2, this.maxDelay);
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
|
||||
Reference in New Issue
Block a user