Commit a3e1cb81 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch '38394-smarter-interval' into 'master'

don't re-run smart interval callback if there is already one in progress

Closes #38394

See merge request gitlab-org/gitlab-ce!15032
parents c8eb789b 862d781d
...@@ -3,9 +3,10 @@ ...@@ -3,9 +3,10 @@
* and controllable by a public API. * and controllable by a public API.
*/ */
class SmartInterval { export default class SmartInterval {
/** /**
* @param { function } opts.callback Function to be called on each iteration (required) * @param { function } opts.callback Function that returns a promise, called on each iteration
* unless still in progress (required)
* @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially
* @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this
* @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this
...@@ -42,13 +43,16 @@ class SmartInterval { ...@@ -42,13 +43,16 @@ class SmartInterval {
const cfg = this.cfg; const cfg = this.cfg;
const state = this.state; const state = this.state;
if (cfg.immediateExecution) { if (cfg.immediateExecution && !this.isLoading) {
cfg.immediateExecution = false; cfg.immediateExecution = false;
cfg.callback(); this.triggerCallback();
} }
state.intervalId = window.setInterval(() => { state.intervalId = window.setInterval(() => {
cfg.callback(); if (this.isLoading) {
return;
}
this.triggerCallback();
if (this.getCurrentInterval() === cfg.maxInterval) { if (this.getCurrentInterval() === cfg.maxInterval) {
return; return;
...@@ -76,7 +80,7 @@ class SmartInterval { ...@@ -76,7 +80,7 @@ class SmartInterval {
// start a timer, using the existing interval // start a timer, using the existing interval
resume() { resume() {
this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped this.stopTimer(); // stop existing timer, in case timer was not previously stopped
this.start(); this.start();
} }
...@@ -104,6 +108,18 @@ class SmartInterval { ...@@ -104,6 +108,18 @@ class SmartInterval {
this.initPageUnloadHandling(); this.initPageUnloadHandling();
} }
triggerCallback() {
this.isLoading = true;
this.cfg.callback()
.then(() => {
this.isLoading = false;
})
.catch((err) => {
this.isLoading = false;
throw err;
});
}
initVisibilityChangeHandling() { initVisibilityChangeHandling() {
// cancel interval when tab no longer shown (prevents cached pages from polling) // cancel interval when tab no longer shown (prevents cached pages from polling)
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
...@@ -154,4 +170,3 @@ class SmartInterval { ...@@ -154,4 +170,3 @@ class SmartInterval {
} }
} }
window.gl.SmartInterval = SmartInterval;
import SmartInterval from '~/smart_interval';
import Flash from '../flash'; import Flash from '../flash';
import { import {
WidgetHeader, WidgetHeader,
...@@ -81,7 +82,7 @@ export default { ...@@ -81,7 +82,7 @@ export default {
return new MRWidgetService(endpoints); return new MRWidgetService(endpoints);
}, },
checkStatus(cb) { checkStatus(cb) {
this.service.checkStatus() return this.service.checkStatus()
.then(res => res.json()) .then(res => res.json())
.then((res) => { .then((res) => {
this.handleNotification(res); this.handleNotification(res);
...@@ -97,7 +98,7 @@ export default { ...@@ -97,7 +98,7 @@ export default {
}); });
}, },
initPolling() { initPolling() {
this.pollingInterval = new gl.SmartInterval({ this.pollingInterval = new SmartInterval({
callback: this.checkStatus, callback: this.checkStatus,
startingInterval: 10000, startingInterval: 10000,
maxInterval: 30000, maxInterval: 30000,
...@@ -106,7 +107,7 @@ export default { ...@@ -106,7 +107,7 @@ export default {
}); });
}, },
initDeploymentsPolling() { initDeploymentsPolling() {
this.deploymentsInterval = new gl.SmartInterval({ this.deploymentsInterval = new SmartInterval({
callback: this.fetchDeployments, callback: this.fetchDeployments,
startingInterval: 30000, startingInterval: 30000,
maxInterval: 120000, maxInterval: 120000,
...@@ -121,7 +122,7 @@ export default { ...@@ -121,7 +122,7 @@ export default {
} }
}, },
fetchDeployments() { fetchDeployments() {
this.service.fetchDeployments() return this.service.fetchDeployments()
.then(res => res.json()) .then(res => res.json())
.then((res) => { .then((res) => {
if (res.length) { if (res.length) {
......
---
title: Update Merge Request polling so there is only one request at a time
merge_request: 15032
author:
type: fixed
import '~/smart_interval'; import SmartInterval from '~/smart_interval';
(() => { describe('SmartInterval', function () {
const DEFAULT_MAX_INTERVAL = 100; const DEFAULT_MAX_INTERVAL = 100;
const DEFAULT_STARTING_INTERVAL = 5; const DEFAULT_STARTING_INTERVAL = 5;
const DEFAULT_SHORT_TIMEOUT = 75; const DEFAULT_SHORT_TIMEOUT = 75;
...@@ -9,7 +9,7 @@ import '~/smart_interval'; ...@@ -9,7 +9,7 @@ import '~/smart_interval';
function createDefaultSmartInterval(config) { function createDefaultSmartInterval(config) {
const defaultParams = { const defaultParams = {
callback: () => {}, callback: () => Promise.resolve(),
startingInterval: DEFAULT_STARTING_INTERVAL, startingInterval: DEFAULT_STARTING_INTERVAL,
maxInterval: DEFAULT_MAX_INTERVAL, maxInterval: DEFAULT_MAX_INTERVAL,
incrementByFactorOf: DEFAULT_INCREMENT_FACTOR, incrementByFactorOf: DEFAULT_INCREMENT_FACTOR,
...@@ -22,158 +22,171 @@ import '~/smart_interval'; ...@@ -22,158 +22,171 @@ import '~/smart_interval';
_.extend(defaultParams, config); _.extend(defaultParams, config);
} }
return new gl.SmartInterval(defaultParams); return new SmartInterval(defaultParams);
} }
describe('SmartInterval', function () { describe('Increment Interval', function () {
describe('Increment Interval', function () { beforeEach(function () {
beforeEach(function () { this.smartInterval = createDefaultSmartInterval();
this.smartInterval = createDefaultSmartInterval(); });
});
it('should increment the interval delay', function (done) { it('should increment the interval delay', function (done) {
const interval = this.smartInterval; const interval = this.smartInterval;
setTimeout(() => { setTimeout(() => {
const intervalConfig = this.smartInterval.cfg; const intervalConfig = this.smartInterval.cfg;
const iterationCount = 4; const iterationCount = 4;
const maxIntervalAfterIterations = intervalConfig.startingInterval * const maxIntervalAfterIterations = intervalConfig.startingInterval *
(intervalConfig.incrementByFactorOf ** (iterationCount - 1)); // 40 (intervalConfig.incrementByFactorOf ** (iterationCount - 1)); // 40
const currentInterval = interval.getCurrentInterval(); const currentInterval = interval.getCurrentInterval();
// Provide some flexibility for performance of testing environment // Provide some flexibility for performance of testing environment
expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval); expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval);
expect(currentInterval <= maxIntervalAfterIterations).toBeTruthy(); expect(currentInterval <= maxIntervalAfterIterations).toBeTruthy();
done(); done();
}, DEFAULT_SHORT_TIMEOUT); // 4 iterations, increment by 2x = (5 + 10 + 20 + 40) }, DEFAULT_SHORT_TIMEOUT); // 4 iterations, increment by 2x = (5 + 10 + 20 + 40)
}); });
it('should not increment past maxInterval', function (done) { it('should not increment past maxInterval', function (done) {
const interval = this.smartInterval; const interval = this.smartInterval;
setTimeout(() => { setTimeout(() => {
const currentInterval = interval.getCurrentInterval(); const currentInterval = interval.getCurrentInterval();
expect(currentInterval).toBe(interval.cfg.maxInterval); expect(currentInterval).toBe(interval.cfg.maxInterval);
done(); done();
}, DEFAULT_LONG_TIMEOUT); }, DEFAULT_LONG_TIMEOUT);
});
}); });
describe('Public methods', function () { it('does not increment while waiting for callback', function () {
beforeEach(function () { jasmine.clock().install();
this.smartInterval = createDefaultSmartInterval();
const smartInterval = createDefaultSmartInterval({
callback: () => new Promise($.noop),
}); });
it('should cancel an interval', function (done) { jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
const interval = this.smartInterval;
const oneInterval = smartInterval.cfg.startingInterval * DEFAULT_INCREMENT_FACTOR;
expect(smartInterval.getCurrentInterval()).toEqual(oneInterval);
setTimeout(() => { jasmine.clock().uninstall();
interval.cancel(); });
});
const intervalId = interval.state.intervalId; describe('Public methods', function () {
const currentInterval = interval.getCurrentInterval(); beforeEach(function () {
const intervalLowerLimit = interval.cfg.startingInterval; this.smartInterval = createDefaultSmartInterval();
});
expect(intervalId).toBeUndefined(); it('should cancel an interval', function (done) {
expect(currentInterval).toBe(intervalLowerLimit); const interval = this.smartInterval;
done(); setTimeout(() => {
}, DEFAULT_SHORT_TIMEOUT); interval.cancel();
});
it('should resume an interval', function (done) { const intervalId = interval.state.intervalId;
const interval = this.smartInterval; const currentInterval = interval.getCurrentInterval();
const intervalLowerLimit = interval.cfg.startingInterval;
setTimeout(() => { expect(intervalId).toBeUndefined();
interval.cancel(); expect(currentInterval).toBe(intervalLowerLimit);
interval.resume(); done();
}, DEFAULT_SHORT_TIMEOUT);
});
const intervalId = interval.state.intervalId; it('should resume an interval', function (done) {
const interval = this.smartInterval;
expect(intervalId).toBeTruthy(); setTimeout(() => {
interval.cancel();
done(); interval.resume();
}, DEFAULT_SHORT_TIMEOUT);
}); const intervalId = interval.state.intervalId;
expect(intervalId).toBeTruthy();
done();
}, DEFAULT_SHORT_TIMEOUT);
}); });
});
describe('DOM Events', function () { describe('DOM Events', function () {
beforeEach(function () { beforeEach(function () {
// This ensures DOM and DOM events are initialized for these specs. // This ensures DOM and DOM events are initialized for these specs.
setFixtures('<div></div>'); setFixtures('<div></div>');
this.smartInterval = createDefaultSmartInterval(); this.smartInterval = createDefaultSmartInterval();
}); });
it('should pause when page is not visible', function (done) { it('should pause when page is not visible', function (done) {
const interval = this.smartInterval; const interval = this.smartInterval;
setTimeout(() => { setTimeout(() => {
expect(interval.state.intervalId).toBeTruthy(); expect(interval.state.intervalId).toBeTruthy();
// simulates triggering of visibilitychange event // simulates triggering of visibilitychange event
interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
expect(interval.state.intervalId).toBeUndefined(); expect(interval.state.intervalId).toBeUndefined();
done(); done();
}, DEFAULT_SHORT_TIMEOUT); }, DEFAULT_SHORT_TIMEOUT);
}); });
it('should change to the hidden interval when page is not visible', function (done) { it('should change to the hidden interval when page is not visible', function (done) {
const HIDDEN_INTERVAL = 1500; const HIDDEN_INTERVAL = 1500;
const interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL }); const interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL });
setTimeout(() => { setTimeout(() => {
expect(interval.state.intervalId).toBeTruthy(); expect(interval.state.intervalId).toBeTruthy();
expect(interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL && expect(interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL &&
interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL).toBeTruthy(); interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL).toBeTruthy();
// simulates triggering of visibilitychange event // simulates triggering of visibilitychange event
interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
expect(interval.state.intervalId).toBeTruthy(); expect(interval.state.intervalId).toBeTruthy();
expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL); expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL);
done(); done();
}, DEFAULT_SHORT_TIMEOUT); }, DEFAULT_SHORT_TIMEOUT);
}); });
it('should resume when page is becomes visible at the previous interval', function (done) { it('should resume when page is becomes visible at the previous interval', function (done) {
const interval = this.smartInterval; const interval = this.smartInterval;
setTimeout(() => { setTimeout(() => {
expect(interval.state.intervalId).toBeTruthy(); expect(interval.state.intervalId).toBeTruthy();
// simulates triggering of visibilitychange event // simulates triggering of visibilitychange event
interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
expect(interval.state.intervalId).toBeUndefined(); expect(interval.state.intervalId).toBeUndefined();
// simulates triggering of visibilitychange event // simulates triggering of visibilitychange event
interval.handleVisibilityChange({ target: { visibilityState: 'visible' } }); interval.handleVisibilityChange({ target: { visibilityState: 'visible' } });
expect(interval.state.intervalId).toBeTruthy(); expect(interval.state.intervalId).toBeTruthy();
done(); done();
}, DEFAULT_SHORT_TIMEOUT); }, DEFAULT_SHORT_TIMEOUT);
}); });
it('should cancel on page unload', function (done) { it('should cancel on page unload', function (done) {
const interval = this.smartInterval; const interval = this.smartInterval;
setTimeout(() => { setTimeout(() => {
$(document).triggerHandler('beforeunload'); $(document).triggerHandler('beforeunload');
expect(interval.state.intervalId).toBeUndefined(); expect(interval.state.intervalId).toBeUndefined();
expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval); expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
done(); done();
}, DEFAULT_SHORT_TIMEOUT); }, DEFAULT_SHORT_TIMEOUT);
}); });
it('should execute callback before first interval', function () { it('should execute callback before first interval', function () {
const interval = createDefaultSmartInterval({ immediateExecution: true }); const interval = createDefaultSmartInterval({ immediateExecution: true });
expect(interval.cfg.immediateExecution).toBeFalsy(); expect(interval.cfg.immediateExecution).toBeFalsy();
});
}); });
}); });
})(window.gl || (window.gl = {})); });
...@@ -121,24 +121,28 @@ describe('mrWidgetOptions', () => { ...@@ -121,24 +121,28 @@ describe('mrWidgetOptions', () => {
describe('initPolling', () => { describe('initPolling', () => {
it('should call SmartInterval', () => { it('should call SmartInterval', () => {
spyOn(gl, 'SmartInterval').and.returnValue({ spyOn(vm, 'checkStatus').and.returnValue(Promise.resolve());
resume() {}, jasmine.clock().install();
stopTimer() {},
});
vm.initPolling(); vm.initPolling();
expect(vm.checkStatus).not.toHaveBeenCalled();
jasmine.clock().tick(10000);
expect(vm.pollingInterval).toBeDefined(); expect(vm.pollingInterval).toBeDefined();
expect(gl.SmartInterval).toHaveBeenCalled(); expect(vm.checkStatus).toHaveBeenCalled();
jasmine.clock().uninstall();
}); });
}); });
describe('initDeploymentsPolling', () => { describe('initDeploymentsPolling', () => {
it('should call SmartInterval', () => { it('should call SmartInterval', () => {
spyOn(gl, 'SmartInterval'); spyOn(vm, 'fetchDeployments').and.returnValue(Promise.resolve());
vm.initDeploymentsPolling(); vm.initDeploymentsPolling();
expect(vm.deploymentsInterval).toBeDefined(); expect(vm.deploymentsInterval).toBeDefined();
expect(gl.SmartInterval).toHaveBeenCalled(); expect(vm.fetchDeployments).toHaveBeenCalled();
}); });
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment