Skip to content

Commit 1d5a714

Browse files
authored
Support incremental executionId in runInLoop (#227)
* Support incremental executionId in runInLoop * Review fixes
1 parent 5659e0b commit 1d5a714

2 files changed

Lines changed: 101 additions & 14 deletions

File tree

src/run-in-loop/index.test.ts

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,82 @@
1-
import { createLogger } from '../logger';
1+
import { type Logger } from '../logger';
2+
import * as utils from '../utils';
23

34
import { runInLoop } from './index';
45

6+
const createMockLogger = (): Logger => ({
7+
runWithContext: jest.fn((_: Record<string, any>, fn: () => any) => fn()) as Logger['runWithContext'],
8+
debug: jest.fn(),
9+
info: jest.fn(),
10+
warn: jest.fn(),
11+
error: jest.fn() as Logger['error'],
12+
child: jest.fn() as Logger['child'],
13+
});
14+
15+
const createTestRunInLoopFunction = (executions: number) => {
16+
let callCount = 0;
17+
18+
return jest.fn(async () => {
19+
callCount += 1;
20+
return { shouldContinueRunning: callCount < executions };
21+
});
22+
};
23+
24+
const getRunContexts = (mockedLogger: Logger) => {
25+
const runWithContextMock = jest.mocked(mockedLogger.runWithContext);
26+
return runWithContextMock.mock.calls.map(([context]) => context);
27+
};
28+
529
describe(runInLoop.name, () => {
6-
const logger = createLogger({
7-
colorize: true,
8-
enabled: true,
9-
minLevel: 'info',
10-
format: 'json',
30+
afterEach(() => {
31+
jest.restoreAllMocks();
1132
});
1233

1334
it('stops the loop after getting the stop signal', async () => {
14-
const fn = async () => ({ shouldContinueRunning: false });
15-
const fnSpy = jest
16-
.spyOn({ fn }, 'fn')
17-
.mockImplementationOnce(async () => ({ shouldContinueRunning: true }))
18-
.mockImplementationOnce(async () => ({ shouldContinueRunning: true }))
19-
.mockImplementationOnce(async () => ({ shouldContinueRunning: false }));
20-
await runInLoop(fnSpy as any, { logger });
35+
const mockedLogger = createMockLogger();
36+
const fnSpy = createTestRunInLoopFunction(3);
37+
await runInLoop(fnSpy, { logger: mockedLogger });
2138
expect(fnSpy).toHaveBeenCalledTimes(3);
2239
});
40+
41+
it('uses random execution IDs by default', async () => {
42+
const mockedLogger = createMockLogger();
43+
const fnSpy = createTestRunInLoopFunction(1);
44+
jest.spyOn(utils, 'generateRandomBytes32').mockReturnValue('0xrandom');
45+
46+
await runInLoop(fnSpy, { logger: mockedLogger });
47+
48+
expect(getRunContexts(mockedLogger)).toStrictEqual([{ executionId: '0xrandom' }]);
49+
});
50+
51+
it('uses incremental execution IDs', async () => {
52+
const mockedLogger = createMockLogger();
53+
const fnSpy = createTestRunInLoopFunction(3);
54+
55+
await runInLoop(fnSpy, {
56+
logger: mockedLogger,
57+
executionIdOptions: { type: 'incremental' },
58+
});
59+
60+
expect(getRunContexts(mockedLogger)).toStrictEqual([
61+
{ executionId: '0' },
62+
{ executionId: '1' },
63+
{ executionId: '2' },
64+
]);
65+
});
66+
67+
it('uses incremental execution IDs with prefix', async () => {
68+
const mockedLogger = createMockLogger();
69+
const fnSpy = createTestRunInLoopFunction(3);
70+
71+
await runInLoop(fnSpy, {
72+
logger: mockedLogger,
73+
executionIdOptions: { type: 'incremental', prefix: 'worker-' },
74+
});
75+
76+
expect(getRunContexts(mockedLogger)).toStrictEqual([
77+
{ executionId: 'worker-0' },
78+
{ executionId: 'worker-1' },
79+
{ executionId: 'worker-2' },
80+
]);
81+
});
2382
});

src/run-in-loop/index.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,24 @@ import { go } from '@api3/promise-utils';
33
import { type Logger } from '../logger';
44
import { generateRandomBytes32, sleep } from '../utils';
55

6+
export type RunInLoopExecutionIdOptions =
7+
| {
8+
/**
9+
* Generate a random 32-byte execution ID for each iteration.
10+
*/
11+
type: 'random';
12+
}
13+
| {
14+
/**
15+
* Generate execution IDs as incrementing numbers starting from 0.
16+
*/
17+
type: 'incremental';
18+
/**
19+
* Optional prefix prepended to the incrementing number (e.g. "my-prefix-0").
20+
*/
21+
prefix?: string;
22+
};
23+
624
export interface RunInLoopOptions {
725
/** An API3 logger instance required to execute the callback with context. */
826
logger: Logger;
@@ -44,8 +62,16 @@ export interface RunInLoopOptions {
4462
* callback is executed immediately.
4563
*/
4664
initialDelayMs?: number;
65+
66+
/**
67+
* Configures how execution IDs are generated. Defaults to random IDs.
68+
*/
69+
executionIdOptions?: RunInLoopExecutionIdOptions;
4770
}
4871

72+
const getExecutionId = (iteration: number, options: RunInLoopExecutionIdOptions) =>
73+
options.type === 'random' ? generateRandomBytes32() : `${options.prefix ?? ''}${iteration}`;
74+
4975
export const runInLoop = async (
5076
fn: () => Promise<{ shouldContinueRunning: boolean } | void>,
5177
options: RunInLoopOptions
@@ -60,6 +86,7 @@ export const runInLoop = async (
6086
hardTimeoutMs,
6187
enabled = true,
6288
initialDelayMs,
89+
executionIdOptions = { type: 'random' },
6390
} = options;
6491

6592
if (hardTimeoutMs && hardTimeoutMs < softTimeoutMs) {
@@ -71,9 +98,10 @@ export const runInLoop = async (
7198

7299
if (initialDelayMs) await sleep(initialDelayMs);
73100

101+
let iteration = 0;
74102
while (true) {
75103
const executionStart = performance.now();
76-
const executionId = generateRandomBytes32();
104+
const executionId = getExecutionId(iteration++, executionIdOptions);
77105

78106
if (enabled) {
79107
const context = logLabel ? { executionId, label: logLabel } : { executionId };

0 commit comments

Comments
 (0)