diff --git a/src/executor/config.ts b/src/executor/config.ts index c0d3d61bc..5bc67944b 100644 --- a/src/executor/config.ts +++ b/src/executor/config.ts @@ -14,7 +14,8 @@ const DEFAULTS = Object.freeze({ taskTimeout: 1000 * 60 * 5, // 5 min, maxTaskRetries: 3, enableLogging: true, - startupTimeout: 1000 * 30, // 30 sec + startupTimeout: 1000 * 90, // 90 sec + exitOnNoProposals: false, }); /** @@ -36,6 +37,7 @@ export class ExecutorConfig { readonly activityExecuteTimeout?: number; readonly jobStorage: JobStorage; readonly startupTimeout: number; + readonly exitOnNoProposals: boolean; constructor(options: ExecutorOptions & ActivityOptions) { const processEnv = !runtimeContextChecker.isBrowser @@ -86,5 +88,6 @@ export class ExecutorConfig { this.maxTaskRetries = options.maxTaskRetries ?? DEFAULTS.maxTaskRetries; this.jobStorage = options.jobStorage || new InMemoryJobStorage(); this.startupTimeout = options.startupTimeout ?? DEFAULTS.startupTimeout; + this.exitOnNoProposals = options.exitOnNoProposals ?? DEFAULTS.exitOnNoProposals; } } diff --git a/src/executor/executor.spec.ts b/src/executor/executor.spec.ts index 3df35a5fd..1d16a65a6 100644 --- a/src/executor/executor.spec.ts +++ b/src/executor/executor.spec.ts @@ -47,8 +47,14 @@ describe("Task Executor", () => { expect(executor).toBeDefined(); await executor.end(); }); - it("should handle a critical error if startup timeout is reached", async () => { - const executor = await TaskExecutor.create({ package: "test", startupTimeout: 0, logger, yagnaOptions }); + it("should handle a critical error if startup timeout is reached and exitOnNoProposals is enabled", async () => { + const executor = await TaskExecutor.create({ + package: "test", + startupTimeout: 0, + exitOnNoProposals: true, + logger, + yagnaOptions, + }); jest .spyOn(MarketService.prototype, "getProposalsCount") .mockImplementation(() => ({ confirmed: 0, initial: 0, rejected: 0 })); @@ -62,6 +68,29 @@ describe("Task Executor", () => { expect(handleErrorSpy).toHaveBeenCalled(); await executor.end(); }); + it("should only warn the user if startup timeout is reached and exitOnNoProposals is disabled", async () => { + const executor = await TaskExecutor.create({ + package: "test", + startupTimeout: 0, + exitOnNoProposals: false, + logger, + yagnaOptions, + }); + jest + .spyOn(MarketService.prototype, "getProposalsCount") + .mockImplementation(() => ({ confirmed: 0, initial: 0, rejected: 0 })); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleErrorSpy = jest.spyOn(executor as any, "handleCriticalError"); + const loggerWarnSpy = jest.spyOn(logger, "warn"); + + await sleep(10, true); + + expect(handleErrorSpy).not.toHaveBeenCalled(); + expect(loggerWarnSpy).toHaveBeenCalledWith( + "Could not start any work on Golem. Processed 0 initial proposals from yagna, filters accepted 0. Check your demand if it's not too restrictive or restart yagna.", + ); + await executor.end(); + }); }); describe("end()", () => { diff --git a/src/executor/executor.ts b/src/executor/executor.ts index bb162c576..e816f281e 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -64,14 +64,21 @@ export type ExecutorOptions = { */ skipProcessSignals?: boolean; /** - * Timeout for waiting for at least one offer from the market. - * This parameter (set to 30 sec by default) will throw an error when executing `TaskExecutor.run` - * if no offer from the market is accepted before this time. + * Timeout for waiting for at least one offer from the market expressed in milliseconds. + * This parameter (set to 90 sec by default) will issue a warning when executing `TaskExecutor.run` + * if no offer from the market is accepted before this time. If you'd like to change this behavior, + * and throw an error instead, set `exitOnNoProposals` to `true`. * You can set a slightly higher time in a situation where your parameters such as proposalFilter * or minimum hardware requirements are quite restrictive and finding a suitable provider * that meets these criteria may take a bit longer. */ startupTimeout?: number; + /** + * If set to `true`, the executor will exit with an error when no proposals are accepted. + * You can customize how long the executor will wait for proposals using the `startupTimeout` parameter. + * Default is `false`. + */ + exitOnNoProposals?: boolean; } & Omit & MarketOptions & TaskServiceOptions & @@ -550,11 +557,12 @@ export class TaskExecutor { : proposalsCount.initial === proposalsCount.rejected ? "All off proposals got rejected." : "Check your proposal filters if they are not too restrictive."; - this.handleCriticalError( - new Error( - `Could not start any work on Golem. Processed ${proposalsCount.initial} initial proposals from yagna, filters accepted ${proposalsCount.confirmed}. ${hint}`, - ), - ); + const errorMessage = `Could not start any work on Golem. Processed ${proposalsCount.initial} initial proposals from yagna, filters accepted ${proposalsCount.confirmed}. ${hint}`; + if (this.options.exitOnNoProposals) { + this.handleCriticalError(new Error(errorMessage)); + } else { + this.logger?.warn(errorMessage); + } } }, this.options.startupTimeout); }