Skip to content
Merged
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
18 changes: 11 additions & 7 deletions src/actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -749,7 +749,7 @@ export class Actor<Data extends Dictionary = Dictionary> {
* @ignore
*/
async call(actorId: string, input?: unknown, options: CallOptions = {}): Promise<ClientActorRun> {
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 });
Expand Down Expand Up @@ -780,7 +780,7 @@ export class Actor<Data extends Dictionary = Dictionary> {
* @ignore
*/
async start(actorId: string, input?: unknown, options: StartOptions = {}): Promise<ClientActorRun> {
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;

Expand Down Expand Up @@ -842,7 +842,7 @@ export class Actor<Data extends Dictionary = Dictionary> {
* @ignore
*/
async callTask(taskId: string, input?: Dictionary, options: CallTaskOptions = {}): Promise<ClientActorRun> {
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;

Expand Down Expand Up @@ -2299,13 +2299,17 @@ export class Actor<Data extends Dictionary = Dictionary> {
}

/**
* 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 ' +
Expand Down
38 changes: 36 additions & 2 deletions test/apify/actor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
},
);
Expand All @@ -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,
});
});
});
Expand Down
Loading