Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions src/__tests__/utils/polling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,25 @@ describe('utils/polling.poll_until', ()=>{
expect(result.last_status).toBe('ready');
});

it('throws timeout error when status never completes', async()=>{
const fetch_once = vi.fn<() => Promise<Poll_item>>()
.mockResolvedValue({status: 'running'});
await expect(poll_until<Poll_item>({
timeout_seconds: 2,
fetch_once,
get_status: (r: Poll_item)=>r.status,
running_statuses: ['running'],
interval_ms: 0,
timeout_label: 'data',
})).rejects.toThrow('Timeout after 2 seconds waiting for data.');
it('throws timeout error when the wall-clock budget passes', async()=>{
vi.useFakeTimers();
try {
const fetch_once = vi.fn<() => Promise<Poll_item>>()
.mockResolvedValue({status: 'running'});
const promise = poll_until<Poll_item>({
timeout_seconds: 2,
fetch_once,
get_status: (r: Poll_item)=>r.status,
running_statuses: ['running'],
interval_ms: 1000,
timeout_label: 'data',
});
const assertion = expect(promise).rejects.toThrow(
'Timeout after 2 seconds waiting for data.');
await vi.advanceTimersByTimeAsync(2000);
await assertion;
} finally {
vi.useRealTimers();
}
});
});
17 changes: 14 additions & 3 deletions src/utils/polling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,27 +46,38 @@ const parse_timeout = (
const poll_until = async<T>(opts: Poll_opts<T>): Promise<Poll_result<T>>=>{
const interval_ms = opts.interval_ms ?? DEFAULT_POLL_INTERVAL_MS;
const timeout_label = opts.timeout_label ?? 'completion';
for (let attempt=0; attempt<opts.timeout_seconds; attempt++)
// timeout_seconds is a wall-clock budget. The loop runs until this much
// real time has elapsed, counting both each request round-trip and the
// sleep interval. The previous version looped exactly timeout_seconds
// times, so every request's round-trip pushed the real runtime well past
// the stated budget (e.g. --timeout 600 took ~12 min, not ~10).
const deadline = Date.now()+opts.timeout_seconds*1000;
let attempt = 0;
for (;;)
{
attempt++;
const result = await opts.fetch_once();
const status = opts.get_status(result);
if (!status || !opts.running_statuses.includes(status))
{
return {
result,
attempts: attempt+1,
attempts: attempt,
last_status: status,
};
}
if (opts.on_running)
{
opts.on_running({
attempt: attempt+1,
attempt,
timeout_seconds: opts.timeout_seconds,
status,
result,
});
}
// Stop before a sleep that would run past the budget.
if (Date.now()+interval_ms >= deadline)
break;
await sleep(interval_ms);
}
throw new Error(
Expand Down