This repository has been archived by the owner on Nov 16, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: extend existing polls instead of stacking them (#619)
* refactor: use setInterval with array instead of setTimeout with restart This has disadvantage of not being able to wait for the `request` inside and to guarantee it doesn't take longer than interval.
- Loading branch information
1 parent
a345bc7
commit e2dcd5e
Showing
4 changed files
with
267 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
/** | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. | ||
*/ | ||
import * as poller from './poller' | ||
import { delay } from '../util' | ||
|
||
describe('Poller', () => { | ||
test('poll should invoke onExpired callback when polling exceeds max duration', async () => { | ||
// Arrange | ||
const onExpiredMock = jest.fn() | ||
const onUpdateMock = jest.fn(trainingStatus => { | ||
}) | ||
const pollConfig: poller.IPollConfig<number> = { | ||
id: 'pc1', | ||
maxDuration: 500, | ||
request: async () => { | ||
return 0 | ||
}, | ||
isResolved: n => false, | ||
onExpired: onExpiredMock, | ||
onUpdate: onUpdateMock | ||
} | ||
|
||
const poller1 = new poller.Poller({ interval: 100 }) | ||
await poller1.addPoll(pollConfig) | ||
|
||
expect(onExpiredMock.mock.calls.length).toBe(1) | ||
expect(onUpdateMock.mock.calls.length).toBeGreaterThan(3) | ||
}) | ||
|
||
test('poll should invoke request, isResolved, and onUpdate for each interval', async () => { | ||
const requestMock = jest.fn(async () => { | ||
return 0 | ||
}) | ||
const isResolvedMock = jest.fn(n => false) | ||
const onExpiredMock = jest.fn() | ||
const onUpdateMock = jest.fn(trainingStatus => { | ||
}) | ||
const pollConfig: poller.IPollConfig<number> = { | ||
id: 'pc1', | ||
maxDuration: 500, | ||
request: requestMock, | ||
isResolved: isResolvedMock, | ||
onExpired: onExpiredMock, | ||
onUpdate: onUpdateMock | ||
} | ||
|
||
const poller1 = new poller.Poller({ interval: 100 }) | ||
await poller1.addPoll(pollConfig) | ||
|
||
expect(requestMock.mock.calls.length).toBe(4) | ||
expect(isResolvedMock.mock.calls.length).toBe(4) | ||
expect(onUpdateMock.mock.calls.length).toBe(4) | ||
}) | ||
|
||
test('poll should stop polling after isResolved returns true', async () => { | ||
const onExpiredMock = jest.fn() | ||
const onUpdateMock = jest.fn(trainingStatus => { | ||
}) | ||
const pollConfig: poller.IPollConfig<number> = { | ||
id: 'pc1', | ||
maxDuration: 500, | ||
request: async () => { | ||
return 0 | ||
}, | ||
isResolved: n => true, | ||
onExpired: onExpiredMock, | ||
onUpdate: onUpdateMock | ||
} | ||
|
||
const poller1 = new poller.Poller({ interval: 100 }) | ||
await poller1.addPoll(pollConfig) | ||
|
||
expect(onUpdateMock.mock.calls.length).toBe(1) | ||
}) | ||
|
||
test('calling poll with same id should extend existing polls', async () => { | ||
const pollConfig1: poller.IPollConfig<number> = { | ||
id: 'pc1', | ||
maxDuration: 400, | ||
request: async () => { | ||
return 0 | ||
}, | ||
isResolved: n => false, | ||
onExpired: () => {}, | ||
onUpdate: () => {} | ||
} | ||
|
||
const pollConfig2: poller.IPollConfig<number> = { | ||
id: 'pc1', | ||
maxDuration: 400, | ||
request: async () => { | ||
return 0 | ||
}, | ||
isResolved: n => false, | ||
onExpired: () => {}, | ||
onUpdate: () => {} | ||
} | ||
|
||
const now = new Date().getTime() | ||
const poller1 = new poller.Poller({ interval: 100 }) | ||
const p1 = poller1.addPoll(pollConfig1) | ||
await delay(200) | ||
poller1.addPoll(pollConfig2) | ||
await p1 | ||
const after = new Date().getTime() | ||
|
||
// 200 + 400 | ||
expect(after - now).toBeGreaterThanOrEqual(600) | ||
}) | ||
|
||
test('calling poll with different id should NOT extend existing polls', async () => { | ||
const pollConfig1: poller.IPollConfig<number> = { | ||
id: 'pc1', | ||
maxDuration: 400, | ||
request: async () => { | ||
return 0 | ||
}, | ||
isResolved: n => false, | ||
onExpired: () => {}, | ||
onUpdate: () => {} | ||
} | ||
|
||
const pollConfig2: poller.IPollConfig<number> = { | ||
id: 'pc2', | ||
maxDuration: 400, | ||
request: async () => { | ||
return 0 | ||
}, | ||
isResolved: n => false, | ||
onExpired: () => {}, | ||
onUpdate: () => {} | ||
} | ||
|
||
const poller1 = new poller.Poller({ interval: 100 }) | ||
|
||
const now = new Date().getTime() | ||
const p1 = poller1.addPoll(pollConfig1) | ||
await delay(200) | ||
poller1.addPoll(pollConfig2) | ||
|
||
await p1 // Will still resolve after 400 expiration | ||
const after = new Date().getTime() | ||
|
||
expect(after - now).toBeGreaterThan(400) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
/** | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. | ||
*/ | ||
|
||
export interface Deferred { | ||
resolve: Function | ||
reject: Function | ||
pollConfig: IPollConfig<any> | ||
} | ||
|
||
export interface ActivePoll { | ||
id: string | ||
end: number | ||
deferred: Deferred[] | ||
} | ||
|
||
export interface IPollConfig<T> { | ||
id: string | ||
maxDuration: number | ||
request: () => Promise<T> | ||
isResolved: (t: T) => boolean | ||
onExpired: () => void | ||
onUpdate: (t: T) => void | ||
} | ||
|
||
export interface IPollerOptions { | ||
interval: number | ||
} | ||
|
||
const global = window | ||
export class Poller { | ||
private polls: ActivePoll[] = [] | ||
constructor(options: IPollerOptions) { | ||
global.setInterval(async () => await this.poll(), options.interval) | ||
} | ||
|
||
addPoll<T>(pollConfig: IPollConfig<T>) { | ||
const { id, maxDuration } = pollConfig | ||
const start = new Date().getTime() | ||
const end = start + maxDuration | ||
const activeApp = this.polls.find(p => p.id === id) | ||
|
||
if (activeApp) { | ||
console.log(`Existing polling found for id: ${id} increasing end from ${activeApp.end} to: ${end}`) | ||
activeApp.end = end | ||
const promise = new Promise((resolve, reject) => { | ||
activeApp.deferred.push({ resolve, reject, pollConfig }) | ||
}) | ||
|
||
return promise | ||
} | ||
|
||
console.log(`No polling found for id: ${id}. Starting new polling until: ${end}`) | ||
const promise = new Promise((resolve, reject) => { | ||
this.polls.push({ | ||
id, | ||
end, | ||
deferred: [{ resolve, reject, pollConfig }] | ||
}) | ||
}) | ||
|
||
return promise | ||
} | ||
|
||
private async poll() { | ||
const now = (new Date()).getTime() | ||
// Alternate approach is to split this into three phases: Filter those expired, await all requests, then filter all resolved. | ||
this.polls = (await Promise.all(this.polls.map(async poll => { | ||
const { end } = poll | ||
// If current time is after max allowed polling duration then resolve | ||
if (now >= end) { | ||
poll.deferred.forEach(deferred => { | ||
deferred.pollConfig.onExpired() | ||
deferred.resolve() | ||
}) | ||
return undefined | ||
} | ||
|
||
// Get training status and if it's one of the resolved states resolve promise | ||
const firstConfig = poll.deferred[0].pollConfig | ||
const result = await firstConfig.request() | ||
firstConfig.onUpdate(result) | ||
|
||
// If trainings status is one of resolved states, remove app from polls to discontinue | ||
if (firstConfig.isResolved(result)) { | ||
poll.deferred.forEach(deferred => deferred.resolve()) | ||
return undefined | ||
} | ||
|
||
return poll | ||
}))).filter(x => x) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters