From fa16ab45aa4f451af5434fc2cf5526440819422f Mon Sep 17 00:00:00 2001 From: tim-kos Date: Mon, 19 Oct 2026 17:21:18 +0200 Subject: [PATCH 1/3] Retry forever instead of hanging on retries: Infinity Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- lib/retry.js | 9 ++++++++- test/integration/test-forever.js | 20 ++++++++++++++++++++ test/integration/test-timeouts.js | 8 ++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 976f688..a3a1ab2 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ milliseconds. If `options` is an array, a copy of that array is returned. `options` is a JS object that can contain any of the following keys: -* `retries`: The maximum amount of times to retry the operation. Default is `10`. Seting this to `1` means `do it once, then retry it once`. +* `retries`: The maximum amount of times to retry the operation. Default is `10`. Seting this to `1` means `do it once, then retry it once`. Setting it to `Infinity` retries forever like `forever: true` does, with timeouts calculated for the default `10` retries. * `factor`: The exponential factor to use. Default is `2`. * `minTimeout`: The number of milliseconds before starting the first retry. Default is `1000`. * `maxTimeout`: The maximum number of milliseconds between two retries. Default is `Infinity`. diff --git a/lib/retry.js b/lib/retry.js index 5e85e79..0badb9e 100644 --- a/lib/retry.js +++ b/lib/retry.js @@ -29,12 +29,19 @@ exports.timeouts = function(options) { throw new Error('minTimeout is greater than maxTimeout'); } + // An endless list of timeouts can't be built. Retry forever instead, backing off over the + // default number of retries and then repeating the last timeout. + if (opts.retries === Infinity) { + opts.retries = 10; + opts.forever = true; + } + var timeouts = []; for (var i = 0; i < opts.retries; i++) { timeouts.push(this.createTimeout(i, opts)); } - if (options && options.forever && !timeouts.length) { + if (opts.forever && !timeouts.length) { timeouts.push(this.createTimeout(i, opts)); } diff --git a/test/integration/test-forever.js b/test/integration/test-forever.js index b41307c..0db2642 100644 --- a/test/integration/test-forever.js +++ b/test/integration/test-forever.js @@ -22,3 +22,23 @@ var retry = require(common.dir.lib + '/retry'); } }); })(); + +(function testInfiniteRetriesRetryForever() { + var operation = retry.operation({ + retries: Infinity, + minTimeout: 1, + factor: 1 + }); + + operation.attempt(function(numAttempt) { + if (operation.retry(new Error('foo'))) { + if (numAttempt === 20) { + operation.stop(); + assert.equal(operation.errors().length, 1); + } + return; + } + + assert.fail('stopped retrying after ' + numAttempt + ' attempts'); + }); +})(); diff --git a/test/integration/test-timeouts.js b/test/integration/test-timeouts.js index 7206b0f..43cb810 100644 --- a/test/integration/test-timeouts.js +++ b/test/integration/test-timeouts.js @@ -67,3 +67,11 @@ var retry = require(common.dir.lib + '/retry'); var timeouts = retry.timeouts({retries: 2}); assert.strictEqual(timeouts.length, 2); })(); + +(function testInfiniteRetriesBuildFiniteTimeouts() { + var timeouts = retry.timeouts({retries: Infinity}); + + assert.equal(timeouts.length, 10); + assert.equal(timeouts[0], 1000); + assert.equal(timeouts[9], 512000); +})(); From 0037072e128741c4590f4a5cb7ae902bebe74c38 Mon Sep 17 00:00:00 2001 From: tim-kos Date: Mon, 19 Oct 2026 17:35:27 +0200 Subject: [PATCH 2/3] Back off until maxTimeout when retrying forever via retries: Infinity Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++-- lib/retry.js | 10 +++++++--- test/integration/test-timeouts.js | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a3a1ab2..bd75c6d 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ main() Creates a new `RetryOperation` object. `options` is the same as `retry.timeouts()`'s `options`, with three additions: -* `forever`: Whether to retry forever, defaults to `false`. +* `forever`: Whether to retry forever, defaults to `false`. Setting `retries` to `Infinity` does the same, backing off until `maxTimeout` (or over the default `10` retries without one) and then repeating that timeout. * `unref`: Whether to [unref](https://nodejs.org/api/timers.html#timers_unref) the setTimeout's, defaults to `false`. * `maxRetryTime`: The maximum time (in milliseconds) that the retried operation is allowed to run. Default is `Infinity`. @@ -116,7 +116,7 @@ milliseconds. If `options` is an array, a copy of that array is returned. `options` is a JS object that can contain any of the following keys: -* `retries`: The maximum amount of times to retry the operation. Default is `10`. Seting this to `1` means `do it once, then retry it once`. Setting it to `Infinity` retries forever like `forever: true` does, with timeouts calculated for the default `10` retries. +* `retries`: The maximum amount of times to retry the operation. Default is `10`. Seting this to `1` means `do it once, then retry it once`. `retry.timeouts()` can't return endless timeouts, so for `Infinity` it returns the timeouts up to `maxTimeout`, or for the default `10` retries if there is no `maxTimeout`. * `factor`: The exponential factor to use. Default is `2`. * `minTimeout`: The number of milliseconds before starting the first retry. Default is `1000`. * `maxTimeout`: The maximum number of milliseconds between two retries. Default is `Infinity`. diff --git a/lib/retry.js b/lib/retry.js index 0badb9e..9937922 100644 --- a/lib/retry.js +++ b/lib/retry.js @@ -29,11 +29,15 @@ exports.timeouts = function(options) { throw new Error('minTimeout is greater than maxTimeout'); } - // An endless list of timeouts can't be built. Retry forever instead, backing off over the - // default number of retries and then repeating the last timeout. + // An endless list of timeouts can't be built. Retry forever instead, backing off until + // maxTimeout (or over the default 10 retries without one) and then repeating the last timeout. if (opts.retries === Infinity) { - opts.retries = 10; opts.forever = true; + opts.retries = 10; + if (opts.maxTimeout !== Infinity && opts.factor > 1) { + var steps = Math.log(opts.maxTimeout / Math.max(opts.minTimeout, 1)) / Math.log(opts.factor); + opts.retries = Math.min(Math.max(Math.ceil(steps) + 1, 1), 1000); + } } var timeouts = []; diff --git a/test/integration/test-timeouts.js b/test/integration/test-timeouts.js index 43cb810..054347e 100644 --- a/test/integration/test-timeouts.js +++ b/test/integration/test-timeouts.js @@ -75,3 +75,21 @@ var retry = require(common.dir.lib + '/retry'); assert.equal(timeouts[0], 1000); assert.equal(timeouts[9], 512000); })(); + +(function testInfiniteRetriesBackOffUntilMaxTimeout() { + var timeouts = retry.timeouts({ + retries: Infinity, + factor: 1.1, + minTimeout: 1000, + maxTimeout: 60000 + }); + + assert.equal(timeouts.length, 44); + assert.ok(timeouts[42] < 60000); + assert.equal(timeouts[43], 60000); +})(); + +(function testInfiniteRetriesWithoutGrowth() { + assert.deepEqual(retry.timeouts({retries: Infinity, factor: 1, maxTimeout: 5000}), [1000]); + assert.deepEqual(retry.timeouts({retries: Infinity, minTimeout: 5000, maxTimeout: 5000}), [5000]); +})(); From 7d38c9085f2b92bdf0392691a201d7f5b1dbcbf8 Mon Sep 17 00:00:00 2001 From: tim-kos Date: Mon, 19 Oct 2026 17:36:03 +0200 Subject: [PATCH 3/3] Use a single timeout for retries: Infinity when timeouts don't grow Co-Authored-By: Claude Opus 5.5 --- lib/retry.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/retry.js b/lib/retry.js index 9937922..b9b9035 100644 --- a/lib/retry.js +++ b/lib/retry.js @@ -34,7 +34,9 @@ exports.timeouts = function(options) { if (opts.retries === Infinity) { opts.forever = true; opts.retries = 10; - if (opts.maxTimeout !== Infinity && opts.factor > 1) { + if (opts.factor <= 1) { + opts.retries = 1; + } else if (opts.maxTimeout !== Infinity) { var steps = Math.log(opts.maxTimeout / Math.max(opts.minTimeout, 1)) / Math.log(opts.factor); opts.retries = Math.min(Math.max(Math.ceil(steps) + 1, 1), 1000); }