Skip to content

Commit 39c3b2d

Browse files
authored
fix (datafile management): Update polling behavior (#266)
Summary: - When autoUpdate is true, start the timer for the following request immediately, instead of waiting for the response to start the timer - When the timeout fires before the response is received, wait for the response to complete and then start the next request immediately. This ensures there is at most 1 request in flight at any time. Test plan: Added new unit tests. Manually tested. Issues: https://optimizely.atlassian.net/browse/OASIS-4628
1 parent 27f4159 commit 39c3b2d

File tree

3 files changed

+62
-13
lines changed

3 files changed

+62
-13
lines changed

packages/datafile-manager/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ Changes that have landed but are not yet released.
99

1010
### Changed
1111
- Changed value for node in engines in package.json from >=4.0.0 to >=6.0.0
12+
- Updated polling behavior:
13+
- Start update interval timer immediately after request
14+
- When update interval timer fires during request, wait until request completes, then immediately start next request
1215

1316
## [0.2.0] - April 9, 2019
1417

packages/datafile-manager/__test__/httpPollingDatafileManager.spec.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,17 +221,14 @@ describe('httpPollingDatafileManager', () => {
221221
})
222222

223223
describe('live updates', () => {
224-
it('passes the update interval to its timeoutFactory setTimeout method', async () => {
224+
it('passes the update interval to its timeoutFactory setTimeout method', () => {
225225
manager.queuedResponses.push({
226226
statusCode: 200,
227227
body: '{"foo3": "bar3"}',
228228
headers: {},
229229
})
230-
231230
const setTimeoutSpy: jest.SpyInstance<() => void, [() => void, number]> = jest.spyOn(testTimeoutFactory, 'setTimeout')
232-
233231
manager.start()
234-
await manager.onReady()
235232
expect(setTimeoutSpy).toBeCalledTimes(1)
236233
expect(setTimeoutSpy.mock.calls[0][1]).toBe(1000)
237234
})
@@ -278,6 +275,36 @@ describe('httpPollingDatafileManager', () => {
278275
expect(manager.get()).toEqual({ foo3: 'bar3' })
279276
})
280277

278+
describe('when the update interval time fires before the request is complete', () => {
279+
it('waits until the request is complete before making the next request', async () => {
280+
let resolveResponsePromise: (resp: Response) => void
281+
const responsePromise: Promise<Response> = new Promise(res => {
282+
resolveResponsePromise = res
283+
})
284+
const makeGetRequestSpy = jest.spyOn(manager, 'makeGetRequest').mockReturnValueOnce({
285+
abort() {},
286+
responsePromise,
287+
})
288+
const setTimeoutSpy = jest.spyOn(testTimeoutFactory, 'setTimeout')
289+
290+
manager.start()
291+
expect(setTimeoutSpy).toBeCalledTimes(1)
292+
expect(makeGetRequestSpy).toBeCalledTimes(1)
293+
294+
testTimeoutFactory.timeoutFns[0]()
295+
expect(makeGetRequestSpy).toBeCalledTimes(1)
296+
297+
resolveResponsePromise!({
298+
statusCode: 200,
299+
body: '{"foo": "bar"}',
300+
headers: {},
301+
})
302+
await responsePromise
303+
expect(makeGetRequestSpy).toBeCalledTimes(2)
304+
expect(setTimeoutSpy).toBeCalledTimes(2)
305+
})
306+
})
307+
281308
it('cancels a pending timeout when stop is called', async () => {
282309
manager.queuedResponses.push(
283310
{

packages/datafile-manager/src/httpPollingDatafileManager.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export default abstract class HTTPPollingDatafileManager implements DatafileMana
6161

6262
private readonly updateInterval: number
6363

64-
private cancelTimeout?: () => void
64+
private cancelTimeout: (() => void) | null
6565

6666
private isStarted: boolean
6767

@@ -71,10 +71,16 @@ export default abstract class HTTPPollingDatafileManager implements DatafileMana
7171

7272
private timeoutFactory: TimeoutFactory
7373

74-
private currentRequest?: AbortableRequest
74+
private currentRequest: AbortableRequest | null
7575

7676
private backoffController: BackoffController
7777

78+
// When true, this means the update interval timeout fired before the current
79+
// sync completed. In that case, we should sync again immediately upon
80+
// completion of the current request, instead of waiting another update
81+
// interval.
82+
private syncOnCurrentRequestComplete: boolean
83+
7884
constructor(config: DatafileManagerConfig) {
7985
const configWithDefaultsApplied: DatafileManagerConfig = {
8086
...this.getConfigDefaults(),
@@ -117,7 +123,10 @@ export default abstract class HTTPPollingDatafileManager implements DatafileMana
117123
logger.warn('Invalid updateInterval %s, defaulting to %s', updateInterval, DEFAULT_UPDATE_INTERVAL)
118124
this.updateInterval = DEFAULT_UPDATE_INTERVAL
119125
}
126+
this.cancelTimeout = null
127+
this.currentRequest = null
120128
this.backoffController = new BackoffController()
129+
this.syncOnCurrentRequestComplete = false
121130
}
122131

123132
get(): object | null {
@@ -138,14 +147,14 @@ export default abstract class HTTPPollingDatafileManager implements DatafileMana
138147
this.isStarted = false
139148
if (this.cancelTimeout) {
140149
this.cancelTimeout()
141-
this.cancelTimeout = undefined
150+
this.cancelTimeout = null
142151
}
143152

144153
this.emitter.removeAllListeners()
145154

146155
if (this.currentRequest) {
147156
this.currentRequest.abort()
148-
this.currentRequest = undefined
157+
this.currentRequest = null
149158
}
150159

151160
return Promise.resolve()
@@ -209,15 +218,17 @@ export default abstract class HTTPPollingDatafileManager implements DatafileMana
209218
return
210219
}
211220

212-
this.currentRequest = undefined
221+
this.currentRequest = null
213222

214-
if (this.autoUpdate) {
215-
this.scheduleNextUpdate()
216-
}
217223
if (!this.isReadyPromiseSettled && !this.autoUpdate) {
218224
// We will never resolve ready, so reject it
219225
this.rejectReadyPromise(new Error('Failed to become ready'))
220226
}
227+
228+
if (this.autoUpdate && this.syncOnCurrentRequestComplete) {
229+
this.syncDatafile()
230+
}
231+
this.syncOnCurrentRequestComplete = false
221232
}
222233

223234
private syncDatafile(): void {
@@ -241,6 +252,10 @@ export default abstract class HTTPPollingDatafileManager implements DatafileMana
241252
this.currentRequest.responsePromise
242253
.then(onRequestResolved, onRequestRejected)
243254
.then(onRequestComplete, onRequestComplete)
255+
256+
if (this.autoUpdate) {
257+
this.scheduleNextUpdate()
258+
}
244259
}
245260

246261
private resolveReadyPromise(): void {
@@ -258,7 +273,11 @@ export default abstract class HTTPPollingDatafileManager implements DatafileMana
258273
const nextUpdateDelay = Math.max(currentBackoffDelay, this.updateInterval)
259274
logger.debug('Scheduling sync in %s ms', nextUpdateDelay)
260275
this.cancelTimeout = this.timeoutFactory.setTimeout(() => {
261-
this.syncDatafile()
276+
if (this.currentRequest) {
277+
this.syncOnCurrentRequestComplete = true
278+
} else {
279+
this.syncDatafile()
280+
}
262281
}, nextUpdateDelay)
263282
}
264283

0 commit comments

Comments
 (0)