diff --git a/src/actor.ts b/src/actor.ts index 8fcdc15c75..d0623f0d90 100644 --- a/src/actor.ts +++ b/src/actor.ts @@ -749,7 +749,7 @@ export class Actor { * @ignore */ async call(actorId: string, input?: unknown, options: CallOptions = {}): Promise { - const timeout = options.timeout === 'inherit' ? this.getRemainingTime() : options.timeout; + const timeout = options.timeout === 'inherit' ? this.getRemainingTimeSecs() : options.timeout; const { token, ...rest } = options; const client = token ? this.newClient({ token }) : this.apifyClient; return client.actor(actorId).call(input, { ...rest, timeout }); @@ -780,7 +780,7 @@ export class Actor { * @ignore */ async start(actorId: string, input?: unknown, options: StartOptions = {}): Promise { - const timeout = options.timeout === 'inherit' ? this.getRemainingTime() : options.timeout; + const timeout = options.timeout === 'inherit' ? this.getRemainingTimeSecs() : options.timeout; const { token, ...rest } = options; const client = token ? this.newClient({ token }) : this.apifyClient; @@ -842,7 +842,7 @@ export class Actor { * @ignore */ async callTask(taskId: string, input?: Dictionary, options: CallTaskOptions = {}): Promise { - const timeout = options.timeout === 'inherit' ? this.getRemainingTime() : options.timeout; + const timeout = options.timeout === 'inherit' ? this.getRemainingTimeSecs() : options.timeout; const { token, ...rest } = options; const client = token ? this.newClient({ token }) : this.apifyClient; @@ -2299,13 +2299,17 @@ export class Actor { } /** - * Get time remaining from the Actor run timeout. Returns `undefined` if not on an Apify platform or the current - * run was started without a timeout. + * Get time remaining from the Actor run timeout in seconds, rounded up to whole seconds with minimum value of 1 second. + * + * The API treats a 0 second timeout as no timeout, the minimum acceptable timeout is 1 second. + * + * Returns `undefined` if not on the Apify platform or the current run was started without a timeout. */ - private getRemainingTime(): number | undefined { + private getRemainingTimeSecs(): number | undefined { const env = this.getEnv(); + const MINIMUM_API_TIMEOUT_SECS = 1; if (this.isAtHome() && env.timeoutAt !== null) { - return env.timeoutAt.getTime() - Date.now(); + return Math.max(Math.ceil((env.timeoutAt.getTime() - Date.now()) / 1000), MINIMUM_API_TIMEOUT_SECS); } log.warning( 'Using `inherit` argument is only possible when the Actor is running on the Apify platform and when the ' + diff --git a/test/apify/actor.test.ts b/test/apify/actor.test.ts index da10e0972e..cbe8719faa 100644 --- a/test/apify/actor.test.ts +++ b/test/apify/actor.test.ts @@ -930,7 +930,8 @@ describe('Actor', () => { const callSpy = vitest.spyOn(ActorClient.prototype, methodName).mockReturnValue(); await Actor[methodName](actId, input, options); expect(callSpy).toBeCalledWith(input, { - timeout: actorTimeout - usedTime, + // The client expects the timeout in seconds, while the remaining time is computed in milliseconds. + timeout: (actorTimeout - usedTime) / 1000, }); }, ); @@ -941,7 +942,40 @@ describe('Actor', () => { const callSpy = vitest.spyOn(TaskClient.prototype, 'call').mockReturnValue(); await Actor.callTask(actId, input, options); expect(callSpy).toBeCalledWith(input, { - timeout: actorTimeout - usedTime, + timeout: (actorTimeout - usedTime) / 1000, + }); + }); + + test(`inherited timeout is rounded up to a whole second`, async () => { + vi.setSystemTime(new Date(testStartTime.getTime() + usedTime + 500)); + + const callSpy = vitest.spyOn(ActorClient.prototype, 'call').mockReturnValue(); + await Actor.call(actId, input, { timeout: 'inherit' }); + expect(callSpy).toBeCalledWith(input, { + timeout: (actorTimeout - usedTime) / 1000, + }); + }); + + test.each([{ methodName: 'call' }, { methodName: 'start' }])( + `Actor.$methodName({timeout: 'inherit'}) is clamped to 1 second when the run is already past its timeout`, + async ({ methodName }) => { + vi.setSystemTime(new Date(testStartTime.getTime() + actorTimeout + 5000)); + + const callSpy = vitest.spyOn(ActorClient.prototype, methodName).mockReturnValue(); + await Actor[methodName](actId, input, { timeout: 'inherit' }); + expect(callSpy).toBeCalledWith(input, { + timeout: 1, + }); + }, + ); + + test(`Actor.callTask({timeout: 'inherit'}) is clamped to 1 second when the run is already past its timeout`, async () => { + vi.setSystemTime(new Date(testStartTime.getTime() + actorTimeout + 5000)); + + const callSpy = vitest.spyOn(TaskClient.prototype, 'call').mockReturnValue(); + await Actor.callTask(actId, input, { timeout: 'inherit' }); + expect(callSpy).toBeCalledWith(input, { + timeout: 1, }); }); });