# All files / remote backoff.ts

100% Statements 33/33
100% Branches 8/8
100% Functions 7/7
100% Lines 32/32

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132

    2x

    2x

    2x

    1311x

    1311x

    1311x

    1311x

    1311x

    1311x

    1311x

    2x
    5009x

    2x
    2x

    2x

    74x

    74x
    74x
    36x

    74x

    74x
    74x
    38x

    74x
    2x

    2x
    864x
    74x
    74x

    2x
    74x

    2x
     /**
     * Copyright 2017 Google Inc.
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *   http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */

    import * as log from '../util/log';
    import { CancelablePromise } from '../util/promise';
    import { AsyncQueue, TimerId } from '../util/async_queue';
    const LOG_TAG = 'ExponentialBackoff';

    /**
     * A helper for running delayed tasks following an exponential backoff curve
     * between attempts.
     *
     * Each delay is made up of a "base" delay which follows the exponential
     * backoff curve, and a +/- 50% "jitter" that is calculated and added to the
     * base delay. This prevents clients from accidentally synchronizing their
     * delays causing spikes of load to the backend.
     */
    export class ExponentialBackoff {
      private currentBaseMs: number;
      private timerPromise: CancelablePromise<void> | null = null;

      constructor(
        /**
         * The AsyncQueue to run backoff operations on.
         */
        private readonly queue: AsyncQueue,
        /**
         * The ID to use when scheduling backoff operations on the AsyncQueue.
         */
        private readonly timerId: TimerId,
        /**
         * The initial delay (used as the base delay on the first retry attempt).
         * Note that jitter will still be applied, so the actual delay could be as
         * little as 0.5*initialDelayMs.
         */
        private readonly initialDelayMs: number,
        /**
         * The multiplier to use to determine the extended base delay after each
         * attempt.
         */
        private readonly backoffFactor: number,
        /**
         * The maximum base delay after which no further backoff is performed.
         * Note that jitter will still be applied, so the actual delay could be as
         * much as 1.5*maxDelayMs.
         */
        private readonly maxDelayMs: number
      ) {
        this.reset();
      }

      /**
       * Resets the backoff delay.
       *
       * The very next backoffAndWait() will have no delay. If it is called again
       * (i.e. due to an error), initialDelayMs (plus jitter) will be used, and
       * subsequent ones will increase according to the backoffFactor.
       */
      reset(): void {
        this.currentBaseMs = 0;
      }

      /**
       * Resets the backoff delay to the maximum delay (e.g. for use after a
       * RESOURCE_EXHAUSTED error).
       */
      resetToMax(): void {
        this.currentBaseMs = this.maxDelayMs;
      }

      /**
       * Returns a promise that resolves after currentDelayMs, and increases the
       * delay for any subsequent attempts. If there was a pending backoff operation
       * already, it will be canceled.
       */
      backoffAndRun(op: () => Promise<void>): void {
        // Cancel any pending backoff operation.
        this.cancel();

        // First schedule using the current base (which may be 0 and should be
        // honored as such).
        const delayWithJitterMs = this.currentBaseMs + this.jitterDelayMs();
        if (this.currentBaseMs > 0) {
          log.debug(
            LOG_TAG,
            `Backing off for ${delayWithJitterMs} ms ` +
              `(base delay: ${this.currentBaseMs} ms)`
          );
        }
        this.timerPromise = this.queue.enqueueAfterDelay(
          this.timerId,
          delayWithJitterMs,
          op
        );

        // Apply backoff factor to determine next delay and ensure it is within
        // bounds.
        this.currentBaseMs *= this.backoffFactor;
        if (this.currentBaseMs < this.initialDelayMs) {
          this.currentBaseMs = this.initialDelayMs;
        }
        if (this.currentBaseMs > this.maxDelayMs) {
          this.currentBaseMs = this.maxDelayMs;
        }
      }

      cancel(): void {
        if (this.timerPromise !== null) {
          this.timerPromise.cancel();
          this.timerPromise = null;
        }
      }

      /** Returns a random value in the range [-currentBaseMs/2, currentBaseMs/2] */
      private jitterDelayMs(): number {
        return (Math.random() - 0.5) * this.currentBaseMs;
      }
    }