Skip to content

Commit 4bf3c0e

Browse files
Pijukatelbarjinjanbuchar
authored
feat: Add inherit timeout option for Actor.call and Actor.start (#518)
- Add `RemainingTime` timeout option for `Actor.call`, `Actor.start` and `Actor.callTask`. This will set the timeout of another `Actor` run to whatever is left from the timeout of the current `Actor` run Related to: apify/apify-client-js#632 Python implementation: apify/apify-sdk-python#473 --------- Co-authored-by: Jindřich Bär <[email protected]> Co-authored-by: Jan Buchar <[email protected]>
1 parent 956cc09 commit 4bf3c0e

File tree

2 files changed

+97
-6
lines changed

2 files changed

+97
-6
lines changed

packages/apify/src/actor.ts

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -227,18 +227,32 @@ export interface ApifyEnv {
227227

228228
export type UserFunc<T = unknown> = () => Awaitable<T>;
229229

230-
export interface CallOptions extends ActorCallOptions {
230+
export interface CallOptions extends Omit<ActorCallOptions, 'timeout'> {
231231
/**
232232
* User API token that is used to run the Actor. By default, it is taken from the `APIFY_TOKEN` environment variable.
233233
*/
234234
token?: string;
235+
/**
236+
* Timeout for the Actor run in seconds, or `'inherit'`.
237+
*
238+
* Using `inherit` will set timeout of the newly started Actor run to the time
239+
* remaining until this Actor run times out so that the new run does not outlive this one.
240+
*/
241+
timeout?: number | 'inherit';
235242
}
236243

237-
export interface CallTaskOptions extends TaskCallOptions {
244+
export interface CallTaskOptions extends Omit<TaskCallOptions, 'timeout'> {
238245
/**
239246
* User API token that is used to run the Actor. By default, it is taken from the `APIFY_TOKEN` environment variable.
240247
*/
241248
token?: string;
249+
/**
250+
* Timeout for the Actor task in seconds, or `'inherit'`.
251+
*
252+
* Using `inherit` will set timeout of the newly started Actor task to the time
253+
* remaining until this Actor run times out so that the new run does not outlive this one.
254+
*/
255+
timeout?: number | 'inherit';
242256
}
243257

244258
export interface AbortOptions extends RunAbortOptions {
@@ -668,10 +682,13 @@ export class Actor<Data extends Dictionary = Dictionary> {
668682
input?: unknown,
669683
options: CallOptions = {},
670684
): Promise<ClientActorRun> {
685+
const timeout =
686+
options.timeout === 'inherit'
687+
? this.getRemainingTime()
688+
: options.timeout;
671689
const { token, ...rest } = options;
672690
const client = token ? this.newClient({ token }) : this.apifyClient;
673-
674-
return client.actor(actorId).call(input, rest);
691+
return client.actor(actorId).call(input, { ...rest, timeout });
675692
}
676693

677694
/**
@@ -703,10 +720,14 @@ export class Actor<Data extends Dictionary = Dictionary> {
703720
input?: unknown,
704721
options: CallOptions = {},
705722
): Promise<ClientActorRun> {
723+
const timeout =
724+
options.timeout === 'inherit'
725+
? this.getRemainingTime()
726+
: options.timeout;
706727
const { token, ...rest } = options;
707728
const client = token ? this.newClient({ token }) : this.apifyClient;
708729

709-
return client.actor(actorId).start(input, rest);
730+
return client.actor(actorId).start(input, { ...rest, timeout });
710731
}
711732

712733
/**
@@ -771,10 +792,14 @@ export class Actor<Data extends Dictionary = Dictionary> {
771792
input?: Dictionary,
772793
options: CallTaskOptions = {},
773794
): Promise<ClientActorRun> {
795+
const timeout =
796+
options.timeout === 'inherit'
797+
? this.getRemainingTime()
798+
: options.timeout;
774799
const { token, ...rest } = options;
775800
const client = token ? this.newClient({ token }) : this.apifyClient;
776801

777-
return client.task(taskId).call(input, rest);
802+
return client.task(taskId).call(input, { ...rest, timeout });
778803
}
779804

780805
/**
@@ -2325,6 +2350,22 @@ export class Actor<Data extends Dictionary = Dictionary> {
23252350
);
23262351
}
23272352

2353+
/**
2354+
* Get time remaining from the Actor run timeout. Returns `undefined` if not on an Apify platform or the current
2355+
* run was started without a timeout.
2356+
*/
2357+
private getRemainingTime(): number | undefined {
2358+
const env = this.getEnv();
2359+
if (this.isAtHome() && env.timeoutAt !== null) {
2360+
return env.timeoutAt.getTime() - Date.now();
2361+
}
2362+
log.warning(
2363+
'Using `inherit` argument is only possible when the Actor is running on the Apify platform and when the ' +
2364+
'timeout for the Actor run is set.',
2365+
);
2366+
return undefined;
2367+
}
2368+
23282369
private async inferDefaultsFromInputSchema<T extends Dictionary>(
23292370
input: T,
23302371
): Promise<T> {

test/apify/actor.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,56 @@ describe('Actor', () => {
891891
});
892892
});
893893

894+
describe('inherit timeout option', () => {
895+
const { actId, input } = globalOptions;
896+
const actorTimeout = 10000;
897+
const usedTime = actorTimeout / 2;
898+
let testStartTime: Date;
899+
900+
beforeEach(() => {
901+
testStartTime = new Date();
902+
process.env[ACTOR_ENV_VARS.TIMEOUT_AT] = new Date(
903+
testStartTime.getTime() + actorTimeout,
904+
).toISOString();
905+
process.env[APIFY_ENV_VARS.IS_AT_HOME] = '1';
906+
vi.setSystemTime(new Date(testStartTime.getTime() + usedTime));
907+
});
908+
909+
afterEach(() => {
910+
delete process.env[ACTOR_ENV_VARS.TIMEOUT_AT];
911+
delete process.env[APIFY_ENV_VARS.IS_AT_HOME];
912+
vitest.restoreAllMocks();
913+
vi.useRealTimers();
914+
});
915+
916+
test.each([{ methodName: 'call' }, { methodName: 'start' }])(
917+
`Actor.$methodName({timeout: 'inherit'})`,
918+
async ({ methodName }) => {
919+
const options = { timeout: 'inherit' as const };
920+
921+
const callSpy = vitest
922+
.spyOn(ActorClient.prototype, methodName)
923+
.mockReturnValue();
924+
await Actor[methodName](actId, input, options);
925+
expect(callSpy).toBeCalledWith(input, {
926+
timeout: actorTimeout - usedTime,
927+
});
928+
},
929+
);
930+
931+
test(`Actor.callTask({timeout: 'inherit'})`, async () => {
932+
const options = { timeout: 'inherit' as const };
933+
934+
const callSpy = vitest
935+
.spyOn(TaskClient.prototype, 'call')
936+
.mockReturnValue();
937+
await Actor.callTask(actId, input, options);
938+
expect(callSpy).toBeCalledWith(input, {
939+
timeout: actorTimeout - usedTime,
940+
});
941+
});
942+
});
943+
894944
// TODO we should remove the duplication if possible
895945
describe('Actor.call()', () => {
896946
const { contentType, build, actId, input, token } = globalOptions;

0 commit comments

Comments
 (0)