瀏覽代碼

Implement ‘decorrelated jitter’ as our backoff strategy.

See https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
Jason Grout 6 年之前
父節點
當前提交
c7863034fc

+ 1 - 1
packages/codemirror/src/editor.ts

@@ -129,7 +129,7 @@ export class CodeMirrorEditor implements CodeEditor.IEditor {
       factory: async () => {
         this._checkSync();
       },
-      frequency: { interval: 3000, jitter: false },
+      frequency: { interval: 3000, backoff: false },
       standby: () => {
         // If changed, only stand by when hidden, otherwise always stand by.
         return this._lastChange ? 'when-hidden' : true;

+ 46 - 102
packages/coreutils/src/poll.ts

@@ -59,27 +59,36 @@ export interface IPoll<T = any, U = any> {
 export namespace IPoll {
   /**
    * The polling frequency parameters.
+   *
+   * ### Notes
+   * We implement the "decorrelated jitter" strategy from
+   * https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/.
+   * Essentially, if consecutive retries are needed, we choose an integer:
+   * `sleep=Math.min(max, rand_between(interval, backoff*sleep))`. This ensures
+   * that the poll is never less than `interval`, and nicely spreads out retries
+   * for consecutive tries. Over time, if interval<<max, the random number will
+   * be above `max` about (1-1/backoff) of the time (sleeping the `max`), and
+   * the rest of the time the sleep will be a random number below `max`,
+   * decorrelating our trigger time from other pollers.
    */
   export type Frequency = {
     /**
-     * The polling interval in milliseconds (integer).
+     * The basic polling interval in milliseconds (integer).
      */
     readonly interval: number;
 
     /**
-     * Whether poll frequency jitters if boolean or jitter (float) quantity.
+     * Whether poll frequency backs off (boolean) or the backoff growth rate (float > 1).
+     *
+     * #### Notes
+     * If true, the default backoff growth rate is 3.
      */
-    readonly jitter: boolean | number;
+    readonly backoff: boolean | number;
 
     /**
      * The maximum milliseconds (integer) between poll requests.
      */
     readonly max: number;
-
-    /**
-     * The minimum milliseconds (integer) between poll requests.
-     */
-    readonly min: number;
   };
 
   /**
@@ -155,19 +164,10 @@ export class Poll<T = any, U = any> implements IDisposable, IPoll<T, U> {
     this._standby = options.standby || Private.DEFAULT_STANDBY;
     this._state = { ...Private.DEFAULT_TICK, timestamp: new Date().getTime() };
 
-    const base = Private.DEFAULT_FREQUENCY;
-    const override: Partial<IPoll.Frequency> = options.frequency || {};
-    const interval = 'interval' in override ? override.interval : base.interval;
-    const jitter = 'jitter' in override ? override.jitter : base.jitter;
-    const max =
-      'max' in override
-        ? override.max
-        : 'interval' in override
-          ? 10 * interval
-          : base.max;
-    const min = 'min' in override ? override.min : base.min;
-
-    this.frequency = { interval, jitter, max, min };
+    this.frequency = {
+      ...Private.DEFAULT_FREQUENCY,
+      ...(options.frequency || {})
+    };
     this.name = options.name || Private.DEFAULT_NAME;
     this.ready = (options.when || Promise.resolve())
       .then(() => {
@@ -221,21 +221,24 @@ export class Poll<T = any, U = any> implements IDisposable, IPoll<T, U> {
       return;
     }
 
-    let { interval, jitter, max, min } = frequency;
+    let { interval, backoff, max } = frequency;
 
-    interval = Math.round(Math.abs(interval));
-    max = Math.round(Math.abs(max));
-    min = Math.round(Math.abs(min));
+    interval = Math.round(interval);
+    max = Math.round(max);
 
-    if (interval > max) {
-      throw new Error('Poll interval cannot exceed max interval length');
+    // Delays are 32-bit integers in many browsers, so check for overflow.
+    // https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Maximum_delay_value
+    if (max > 2147483647) {
+      throw new Error('Max poll interval must be less than 2147483647');
     }
-
-    if (min > max || min > interval) {
-      throw new Error('Poll min cannot exceed poll interval or poll max');
+    if (interval < 0 || interval > max) {
+      throw new Error('Poll interval must be between 0 and max');
+    }
+    if (backoff < 1) {
+      throw new Error('backoff growth factor must be at least 1');
     }
 
-    this._frequency = { interval, jitter, max, min };
+    this._frequency = { interval, backoff, max };
   }
 
   /**
@@ -442,15 +445,12 @@ export class Poll<T = any, U = any> implements IDisposable, IPoll<T, U> {
 
     // If in standby mode schedule next tick without calling the factory.
     if (standby) {
-      const { interval, jitter, max, min } = this.frequency;
-
       this.schedule({
-        interval: Private.jitter(interval, jitter, max, min),
+        interval: this.frequency.interval,
         payload: null,
         phase: 'standby',
         timestamp: new Date().getTime()
       });
-
       return;
     }
 
@@ -462,10 +462,8 @@ export class Poll<T = any, U = any> implements IDisposable, IPoll<T, U> {
           return;
         }
 
-        const { interval, jitter, max, min } = this.frequency;
-
         this.schedule({
-          interval: Private.jitter(interval, jitter, max, min),
+          interval: this.frequency.interval,
           payload: resolved,
           phase: this.state.phase === 'rejected' ? 'reconnected' : 'resolved',
           timestamp: new Date().getTime()
@@ -476,11 +474,15 @@ export class Poll<T = any, U = any> implements IDisposable, IPoll<T, U> {
           return;
         }
 
-        const { jitter, max, min } = this.frequency;
-        const increased = Math.min(this.state.interval * 2, max);
+        const { backoff, max, interval } = this.frequency;
+        const growth = backoff === true ? 3 : backoff === false ? 1 : backoff;
+        const sleep = Math.min(
+          max,
+          Private.getRandomIntInclusive(interval, this.state.interval * growth)
+        );
 
         this.schedule({
-          interval: Private.jitter(increased, jitter, max, min),
+          interval: sleep,
           payload: rejected,
           phase: 'rejected',
           timestamp: new Date().getTime()
@@ -533,19 +535,6 @@ export namespace Poll {
 
     /**
      * The polling frequency parameters.
-     *
-     * #### Notes
-     * _interval_ defaults to `1000`.
-     * If set to `0`, the poll will schedule an animation frame after each
-     * promise resolution.
-     *
-     * _jitter_ defaults to `0`.
-     * If set to `true` jitter quantity is `0.25`.
-     * If set to `false` jitter quantity to `0`.
-     *
-     * _max_ defaults to `10 * interval`.
-     *
-     * _min_ defaults to `100`.
      */
     frequency?: Partial<IPoll.Frequency>;
 
@@ -593,16 +582,10 @@ namespace Private {
    */
   export const DEFAULT_FREQUENCY: IPoll.Frequency = {
     interval: 1000,
-    jitter: 0,
-    max: 10 * 1000,
-    min: 100
+    backoff: true,
+    max: 30 * 1000
   };
 
-  /**
-   * The jitter quantity if `jitter` is set to `true`.
-   */
-  export const DEFAULT_JITTER = 0.25;
-
   /**
    * The default poll name.
    */
@@ -633,45 +616,6 @@ namespace Private {
     timestamp: new Date(0).getTime()
   };
 
-  /**
-   * Returns a randomly jittered (integer) value.
-   *
-   * @param base - The base (integer) value that is being wobbled.
-   *
-   * @param quantity - The jitter (float) quantity or boolean flag. The jitter
-   * is a percentage of the base.
-   *
-   * @param max - The largest acceptable (integer) value to return.
-   *
-   * @param min - The smallest acceptable (integer) value to return.
-   *
-   * #### Notes
-   * The returned value will be a random value between base +- (base*jitter),
-   * inclusive, capped by min and max.
-   */
-  export function jitter(
-    base: number,
-    quantity: boolean | number,
-    max: number,
-    min: number
-  ): number {
-    base = Math.round(base);
-
-    // If quantity is 0 or false, no jitter
-    if (!quantity) {
-      return base;
-    }
-
-    if (quantity === true) {
-      quantity = DEFAULT_JITTER;
-    }
-
-    quantity = Math.abs(quantity);
-    min = Math.max(min, base * (1 - quantity));
-    max = Math.min(max, base * (1 + quantity));
-    return getRandomIntInclusive(min, max);
-  }
-
   /**
    * Get a random integer between min and max, inclusive of both.
    *
@@ -683,7 +627,7 @@ namespace Private {
    * that, but doing so would cause your random numbers to follow a non-uniform
    * distribution, which may not be acceptable for your needs.
    */
-  function getRandomIntInclusive(min: number, max: number) {
+  export function getRandomIntInclusive(min: number, max: number) {
     min = Math.ceil(min);
     max = Math.floor(max);
     return Math.floor(Math.random() * (max - min + 1)) + min;

+ 4 - 6
packages/services/src/kernel/manager.ts

@@ -42,9 +42,8 @@ export class KernelManager implements Kernel.IManager {
       factory: () => this.requestRunning(),
       frequency: {
         interval: 10 * 1000,
-        jitter: true,
-        max: 300 * 1000,
-        min: 100
+        backoff: true,
+        max: 300 * 1000
       },
       name: `@jupyterlab/services:KernelManager#models`,
       standby: options.standby || 'when-hidden',
@@ -54,9 +53,8 @@ export class KernelManager implements Kernel.IManager {
       factory: () => this.requestSpecs(),
       frequency: {
         interval: 61 * 1000,
-        jitter: true,
-        max: 300 * 1000,
-        min: 100
+        backoff: true,
+        max: 300 * 1000
       },
       name: `@jupyterlab/services:KernelManager#specs`,
       standby: options.standby || 'when-hidden',

+ 4 - 6
packages/services/src/session/manager.ts

@@ -44,9 +44,8 @@ export class SessionManager implements Session.IManager {
       factory: () => this.requestRunning(),
       frequency: {
         interval: 10 * 1000,
-        jitter: true,
-        max: 300 * 1000,
-        min: 100
+        backoff: true,
+        max: 300 * 1000
       },
       name: `@jupyterlab/services:SessionManager#models`,
       standby: options.standby || 'when-hidden',
@@ -56,9 +55,8 @@ export class SessionManager implements Session.IManager {
       factory: () => this.requestSpecs(),
       frequency: {
         interval: 61 * 1000,
-        jitter: true,
-        max: 300 * 1000,
-        min: 100
+        backoff: true,
+        max: 300 * 1000
       },
       name: `@jupyterlab/services:SessionManager#specs`,
       standby: options.standby || 'when-hidden',

+ 2 - 3
packages/services/src/terminal/manager.ts

@@ -42,9 +42,8 @@ export class TerminalManager implements TerminalSession.IManager {
         factory: () => this.requestRunning(),
         frequency: {
           interval: 10 * 1000,
-          jitter: true,
-          max: 300 * 1000,
-          min: 100
+          backoff: true,
+          max: 300 * 1000
         },
         name: `@jupyterlab/services:TerminalManager#models`,
         standby: options.standby || 'when-hidden',

+ 1 - 1
packages/statusbar/src/defaults/memoryUsage.tsx

@@ -65,7 +65,7 @@ export namespace MemoryUsage {
         factory: () => Private.factory(),
         frequency: {
           interval: options.refreshRate,
-          jitter: true
+          backoff: true
         },
         name: '@jupyterlab/statusbar:MemoryUsage#metrics'
       });

+ 20 - 30
tests/test-coreutils/src/poll.spec.ts

@@ -83,22 +83,22 @@ describe('Poll', () => {
         expect(poll.frequency.interval).to.equal(1000);
       });
 
-      it('should set jitter', () => {
-        const jitter = false;
+      it('should set backoff', () => {
+        const backoff = false;
         poll = new Poll({
           factory: () => Promise.resolve(),
-          frequency: { jitter },
-          name: '@jupyterlab/test-coreutils:Poll#frequency:jitter-1'
+          frequency: { backoff },
+          name: '@jupyterlab/test-coreutils:Poll#frequency:backoff-1'
         });
-        expect(poll.frequency.jitter).to.equal(jitter);
+        expect(poll.frequency.backoff).to.equal(backoff);
       });
 
-      it('should default jitter to `0`', () => {
+      it('should default backoff to `true`', () => {
         poll = new Poll({
           factory: () => Promise.resolve(),
-          name: '@jupyterlab/test-coreutils:Poll#frequency:jitter-2'
+          name: '@jupyterlab/test-coreutils:Poll#frequency:backoff-2'
         });
-        expect(poll.frequency.jitter).to.equal(0);
+        expect(poll.frequency.backoff).to.equal(true);
       });
 
       it('should set max value', () => {
@@ -111,9 +111,9 @@ describe('Poll', () => {
         expect(poll.frequency.max).to.equal(200000);
       });
 
-      it('should default max to 10x the interval', () => {
+      it('should default max to 30s', () => {
         const interval = 500;
-        const max = 10 * interval;
+        const max = 30 * 1000;
         poll = new Poll({
           frequency: { interval },
           factory: () => Promise.resolve(),
@@ -121,25 +121,15 @@ describe('Poll', () => {
         });
         expect(poll.frequency.max).to.equal(max);
       });
-
-      it('should set min', () => {
-        const min = 250;
-        poll = new Poll({
-          factory: () => Promise.resolve(),
-          frequency: { min },
-          name: '@jupyterlab/test-coreutils:Poll#min-1'
-        });
-        expect(poll.frequency.min).to.equal(min);
-      });
-
-      it('should default min to `100`', () => {
-        const min = 100;
+      it('should default max to 30s', () => {
+        const interval = 500;
+        const max = 30 * 1000;
         poll = new Poll({
+          frequency: { interval },
           factory: () => Promise.resolve(),
-          name: '@jupyterlab/test-coreutils:Poll#min-2'
+          name: '@jupyterlab/test-coreutils:Poll#frequency:max-2'
         });
-        expect(poll.frequency.min).to.equal(min);
-        poll.dispose();
+        expect(poll.frequency.max).to.equal(max);
       });
     });
   });
@@ -215,7 +205,7 @@ describe('Poll', () => {
     it('should resolve after a tick', async () => {
       poll = new Poll({
         factory: () => Promise.resolve(),
-        frequency: { interval: 2000, jitter: 0 },
+        frequency: { interval: 2000, backoff: false },
         name: '@jupyterlab/test-coreutils:Poll#tick-1'
       });
       const expected = 'when-resolved resolved';
@@ -242,7 +232,7 @@ describe('Poll', () => {
       const ticker: IPoll.Phase[] = [];
       poll = new Poll<void, void>({
         factory: () => Promise.resolve(),
-        frequency: { interval: 2000, jitter: 0 },
+        frequency: { interval: 2000, backoff: false },
         name: '@jupyterlab/test-coreutils:Poll#ticked-1'
       });
       poll.ticked.connect(() => {
@@ -258,7 +248,7 @@ describe('Poll', () => {
       const promise = Promise.reject();
       poll = new Poll({
         factory: () => Promise.resolve(),
-        frequency: { interval: 2000, jitter: 0 },
+        frequency: { interval: 2000, backoff: false },
         name: '@jupyterlab/test-coreutils:Poll#ticked-2',
         when: promise
       });
@@ -273,7 +263,7 @@ describe('Poll', () => {
     it('should emit a tick identical to the poll state', async () => {
       poll = new Poll<void, void>({
         factory: () => Promise.resolve(),
-        frequency: { interval: 100, jitter: 0 },
+        frequency: { interval: 100, backoff: false },
         name: '@jupyterlab/test-coreutils:Poll#ticked-3'
       });
       poll.ticked.connect((_, tick) => {