Back off from a link that dies as soon as it comes up

A bind that returns is not proof the link works: an unreadable stream is
only found afterwards, so resetting the delay there handed a link that
died on arrival a fresh minDelay every cycle — one connect and bind per
second forever, which is how an account gets blocked for bind flooding.
The loop resets only once a link has outlasted maxDelay.

Also covers the unreadable stream that has no loop to retry it.
This commit is contained in:
2026-08-31 22:45:29 +02:00
parent b1c790b9a0
commit 8e4d472f72
6 changed files with 78 additions and 14 deletions
+12 -1
View File
@@ -7,19 +7,23 @@ export type ReconnectLoopOptions = {
log: SmppLog;
maxDelay: number;
minDelay: number;
now?: (() => number) | undefined;
/** Brings the owner back up on a freshly opened socket. An err means try again. */
onConnected: (sock: Socket) => Promise<VoidResult>;
};
/** Reopens a dropped connection, backing off between attempts until it is told to stop. */
export class ReconnectLoop {
private readonly now: () => number;
private readonly options: ReconnectLoopOptions;
private attempting = false;
private delay: number;
private halted = false;
private timer: NodeJS.Timeout | undefined;
private upAt: number | undefined;
constructor(options: ReconnectLoopOptions) {
this.now = options.now ?? Date.now;
this.options = options;
this.delay = options.minDelay;
}
@@ -32,6 +36,13 @@ export class ReconnectLoop {
schedule(): void {
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;
}
this.upAt = undefined;
const delay = this.delay;
this.options.log.info('reconnect - retrying after a drop', { delay });
@@ -98,7 +109,7 @@ export class ReconnectLoop {
return true;
}
this.delay = this.options.minDelay;
this.upAt = this.now();
return false;
}