diff --git a/README.md b/README.md index e8ef7f3..edcab4c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/index.ts b/src/index.ts index af4fab9..153b3e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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; @@ -81,6 +86,7 @@ export interface SarusClassParams { retryProcessTimePeriod?: number; reconnectAutomatically?: boolean; retryConnectionDelay?: boolean | number; + exponentialBackoff?: ExponentialBackoffParams; storageType?: string; storageKey?: string; } @@ -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 @@ -109,6 +116,7 @@ export default class Sarus { retryProcessTimePeriod?: number; reconnectAutomatically?: boolean; retryConnectionDelay: number; + exponentialBackoff?: ExponentialBackoffParams; storageType: string; storageKey: string; @@ -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; @@ -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