diff --git a/src/run-in-loop/index.test.ts b/src/run-in-loop/index.test.ts index 1380055a..7df3d98a 100644 --- a/src/run-in-loop/index.test.ts +++ b/src/run-in-loop/index.test.ts @@ -1,23 +1,82 @@ -import { createLogger } from '../logger'; +import { type Logger } from '../logger'; +import * as utils from '../utils'; import { runInLoop } from './index'; +const createMockLogger = (): Logger => ({ + runWithContext: jest.fn((_: Record, fn: () => any) => fn()) as Logger['runWithContext'], + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn() as Logger['error'], + child: jest.fn() as Logger['child'], +}); + +const createTestRunInLoopFunction = (executions: number) => { + let callCount = 0; + + return jest.fn(async () => { + callCount += 1; + return { shouldContinueRunning: callCount < executions }; + }); +}; + +const getRunContexts = (mockedLogger: Logger) => { + const runWithContextMock = jest.mocked(mockedLogger.runWithContext); + return runWithContextMock.mock.calls.map(([context]) => context); +}; + describe(runInLoop.name, () => { - const logger = createLogger({ - colorize: true, - enabled: true, - minLevel: 'info', - format: 'json', + afterEach(() => { + jest.restoreAllMocks(); }); it('stops the loop after getting the stop signal', async () => { - const fn = async () => ({ shouldContinueRunning: false }); - const fnSpy = jest - .spyOn({ fn }, 'fn') - .mockImplementationOnce(async () => ({ shouldContinueRunning: true })) - .mockImplementationOnce(async () => ({ shouldContinueRunning: true })) - .mockImplementationOnce(async () => ({ shouldContinueRunning: false })); - await runInLoop(fnSpy as any, { logger }); + const mockedLogger = createMockLogger(); + const fnSpy = createTestRunInLoopFunction(3); + await runInLoop(fnSpy, { logger: mockedLogger }); expect(fnSpy).toHaveBeenCalledTimes(3); }); + + it('uses random execution IDs by default', async () => { + const mockedLogger = createMockLogger(); + const fnSpy = createTestRunInLoopFunction(1); + jest.spyOn(utils, 'generateRandomBytes32').mockReturnValue('0xrandom'); + + await runInLoop(fnSpy, { logger: mockedLogger }); + + expect(getRunContexts(mockedLogger)).toStrictEqual([{ executionId: '0xrandom' }]); + }); + + it('uses incremental execution IDs', async () => { + const mockedLogger = createMockLogger(); + const fnSpy = createTestRunInLoopFunction(3); + + await runInLoop(fnSpy, { + logger: mockedLogger, + executionIdOptions: { type: 'incremental' }, + }); + + expect(getRunContexts(mockedLogger)).toStrictEqual([ + { executionId: '0' }, + { executionId: '1' }, + { executionId: '2' }, + ]); + }); + + it('uses incremental execution IDs with prefix', async () => { + const mockedLogger = createMockLogger(); + const fnSpy = createTestRunInLoopFunction(3); + + await runInLoop(fnSpy, { + logger: mockedLogger, + executionIdOptions: { type: 'incremental', prefix: 'worker-' }, + }); + + expect(getRunContexts(mockedLogger)).toStrictEqual([ + { executionId: 'worker-0' }, + { executionId: 'worker-1' }, + { executionId: 'worker-2' }, + ]); + }); }); diff --git a/src/run-in-loop/index.ts b/src/run-in-loop/index.ts index 1cba13da..4197901b 100644 --- a/src/run-in-loop/index.ts +++ b/src/run-in-loop/index.ts @@ -3,6 +3,24 @@ import { go } from '@api3/promise-utils'; import { type Logger } from '../logger'; import { generateRandomBytes32, sleep } from '../utils'; +export type RunInLoopExecutionIdOptions = + | { + /** + * Generate a random 32-byte execution ID for each iteration. + */ + type: 'random'; + } + | { + /** + * Generate execution IDs as incrementing numbers starting from 0. + */ + type: 'incremental'; + /** + * Optional prefix prepended to the incrementing number (e.g. "my-prefix-0"). + */ + prefix?: string; + }; + export interface RunInLoopOptions { /** An API3 logger instance required to execute the callback with context. */ logger: Logger; @@ -44,8 +62,16 @@ export interface RunInLoopOptions { * callback is executed immediately. */ initialDelayMs?: number; + + /** + * Configures how execution IDs are generated. Defaults to random IDs. + */ + executionIdOptions?: RunInLoopExecutionIdOptions; } +const getExecutionId = (iteration: number, options: RunInLoopExecutionIdOptions) => + options.type === 'random' ? generateRandomBytes32() : `${options.prefix ?? ''}${iteration}`; + export const runInLoop = async ( fn: () => Promise<{ shouldContinueRunning: boolean } | void>, options: RunInLoopOptions @@ -60,6 +86,7 @@ export const runInLoop = async ( hardTimeoutMs, enabled = true, initialDelayMs, + executionIdOptions = { type: 'random' }, } = options; if (hardTimeoutMs && hardTimeoutMs < softTimeoutMs) { @@ -71,9 +98,10 @@ export const runInLoop = async ( if (initialDelayMs) await sleep(initialDelayMs); + let iteration = 0; while (true) { const executionStart = performance.now(); - const executionId = generateRandomBytes32(); + const executionId = getExecutionId(iteration++, executionIdOptions); if (enabled) { const context = logLabel ? { executionId, label: logLabel } : { executionId };