Skip to content

Commit

Permalink
Add optional exponential backoff config
Browse files Browse the repository at this point in the history
Allow configuring and enabling exponential backoff for failing
connections, instead of relying on reconnections in regular intervals.

Exponential backoff can be configured in order to lighten server load if
multiple clients are reconnecting.

The new configuration parameter is called `exponentialBackoff` and
requires two parameters `backoffRate` and `backoffLimit`.
  • Loading branch information
justuswilhelm committed Apr 19, 2024
1 parent ed40a6c commit 0bb979b
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 0 deletions.
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,68 @@ If you think that there is a potential case for you ending up queuing at least
wrap `sarus.send` function calls in a try/catch statement, so as to handle
those messages, should they occur.

### Exponential backoff

Configure exponential backoff like so:

```typescript
import Sarus from '@anephenix/sarus';

const sarus = new Sarus({
url: 'wss://ws.anephenix.com',
exponentialBackoff: {
// Exponential factor, here 2 will result in
// 1 s, 2 s, 4 s, and so on increasing delays
backoffRate: 2,
// Never wait more than 2000 seconds
backoffLimit: 2000,
},
});
```

When a connection attempt repeatedly fails, decreasing the delay
exponentially between each subsequent reconnection attempt is called
[Exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff). The
idea is that if a connection attempt failed after 1 second, and 2 seconds, then it is
not necessary to check it on the 3rd second, since the probability of a
reconnection succeeding on the third attempt is most likely not going up.
Therefore, increasing the delay between each attempt factors in the assumption
that a connection is not more likely to succeed by repeatedly probing in regular
intervals.

This decreases both the load on the client, as well as on the server. For
a client, fewer websocket connection attempts decrease the load on the client
and on the network connection. For the server, should websocket requests fail
within, then the load for handling repeatedly failing requests will fall
as well. Furthermore, the burden on the network will also be decreased. Should
for example a server refuse to accept websocket connections for one client,
then there is the possibility that other clients will also not be able to connect.

Sarus implements _truncated exponential backoff_, meaning that the maximum
reconnection delay is capped by another factor `backoffLimit` and will never
exceed it. The exponential backoff rate itself is determined by `backoffRate`.
If `backoffRate` is 2, then the delays will be 1 s, 2 s, 4 s, and so on.

The algorithm for reconnection looks like this in pseudocode:

```javascript
// Configurable
const backoffRate = 2;
// The maximum delay will be 400s
const backoffLimit = 400;
let notConnected = false;
let connectionAttempts = 1;
while (notConnected) {
const delay = Math.min(
Math.pow(connectionAttempts, backoffRate),
backoffLimit,
);
await delay(delay);
notConnected = tryToConnect();
connectionAttempts += 1;
}
```

### Advanced options

Sarus has a number of other options that you can pass to the client during
Expand Down
16 changes: 16 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ const validateWebSocketUrl = (rawUrl: string): URL => {
return url;
};

export interface ExponentialBackoffParams {
backoffRate: number;
backoffLimit: number;
}

export interface SarusClassParams {
url: string;
binaryType?: BinaryType;
Expand All @@ -81,6 +86,7 @@ export interface SarusClassParams {
retryProcessTimePeriod?: number;
reconnectAutomatically?: boolean;
retryConnectionDelay?: boolean | number;
exponentialBackoff?: ExponentialBackoffParams;
storageType?: string;
storageKey?: string;
}
Expand All @@ -96,6 +102,7 @@ export interface SarusClassParams {
* @param {boolean} param0.reconnectAutomatically - An optional boolean flag to indicate whether to reconnect automatically when a websocket connection is severed
* @param {number} param0.retryProcessTimePeriod - An optional number for how long the time period between retrying to send a messgae to a WebSocket server should be
* @param {boolean|number} param0.retryConnectionDelay - An optional parameter for whether to delay WebSocket reconnection attempts by a time period. If true, the delay is 1000ms, otherwise it is the number passed. The default value when this parameter is undefined will be interpreted as 1000ms.
* @param {ExponentialBackoffParams} param0.exponentialBackoff - An optional containing configuration for exponential backoff. If this parameter is undefined, exponential backoff is disabled. The minimum delay is determined by retryConnectionDelay. If retryConnectionDelay is set is false, this setting will not be in effect.
* @param {string} param0.storageType - An optional string specifying the type of storage to use for persisting messages in the message queue
* @param {string} param0.storageKey - An optional string specifying the key used to store the messages data against in sessionStorage/localStorage
* @returns {object} The class instance
Expand All @@ -109,6 +116,7 @@ export default class Sarus {
retryProcessTimePeriod?: number;
reconnectAutomatically?: boolean;
retryConnectionDelay: number;
exponentialBackoff?: ExponentialBackoffParams;
storageType: string;
storageKey: string;

Expand Down Expand Up @@ -164,6 +172,7 @@ export default class Sarus {
reconnectAutomatically,
retryProcessTimePeriod, // TODO - write a test case to check this
retryConnectionDelay,
exponentialBackoff,
storageType = "memory",
storageKey = "sarus",
} = props;
Expand Down Expand Up @@ -208,6 +217,13 @@ export default class Sarus {
? undefined
: retryConnectionDelay) ?? 1000;

/*
When a exponential backoff parameter object is provided, reconnection
attemptions will be increasingly delayed by an exponential factor.
This feature is disabled by default.
*/
this.exponentialBackoff = exponentialBackoff;

/*
Sets the storage type for the messages in the message queue. By default
it is an in-memory option, but can also be set as 'session' for
Expand Down

0 comments on commit 0bb979b

Please sign in to comment.