Skip to content

feat: abort named child runs when the parent is gracefully aborted - #813

Open
barjin wants to merge 8 commits into
masterfrom
feat/abort-child-runs-with-parent
Open

barjin wants to merge 8 commits into
masterfrom
feat/abort-child-runs-with-parent

Conversation

@barjin

@barjin barjin commented Oct 19, 2026

Copy link
Copy Markdown
Member

Adds an abortWithParent option to Actor.start, call, startTask and callTask that is recorded with the named child run, so the run is gracefully aborted on the parent's aborting event, also after a migration.

Closes #739

Adds an `abortWithParent` option to `Actor.start`, `call`, `startTask` and `callTask`. It is recorded with the named child run, and on the `aborting` event every child run started with it is gracefully aborted.

Closes #739
@github-actions github-actions Bot added the t-tooling Issues with this label are in the ownership of the tooling team. label Oct 19, 2026
@apify-service-account apify-service-account added the tested Temporary label used only programatically for some analytics. label Oct 19, 2026
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

✅ The link checker did not find any broken links.

See more at https://github.com/apify/apify-sdk-js/actions/runs/39428850912#summary-118648267280

@barjin
barjin requested review from janbuchar and vdusek October 20, 2026 09:42

@vdusek vdusek left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One comment from me, two from Claude.

I'd like mine to be resolved before we merge either the JS or the Python PR.

Comment thread src/actor.ts Outdated
Comment on lines +877 to +883
const events = serviceLocator.getEventManager();
// Removing all listeners keeps the SDK's own one that aborts the child runs started with `abortWithParent`.
const keepsChildRunAbort = !listener && events.listeners(event).includes(this.#abortChildRunsWithParent);

events.off(event, listener);

if (keepsChildRunAbort) events.on(event, this.#abortChildRunsWithParent);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TLDR; how to approach "internal" listeners?

This only protects the listener when removal goes through Actor.off(). A direct serviceLocator.getEventManager().off('aborting') still drops it, and the listener also shows up in events.listeners('aborting').

Python keeps SDK-internal listeners in a separate list on the event manager and dispatches them from an emit() override, so no off() call can reach them. But maybe there is a better way? 🙂

Comment thread src/actor.ts Outdated
if (this.#gracefulShutdownHandlers.migrating) {
this.off(ACTOR_EVENT_NAMES.MIGRATING, this.#gracefulShutdownHandlers.migrating);
}
this.off(ACTOR_EVENT_NAMES.ABORTING, this.#abortChildRunsWithParent);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: The listener is removed before events.close() closes the websocket, so an aborting event that arrives during exit() aborts no child runs. Python removes it only after the event manager is closed. Moving this line below await events.close() would match.

✍️ Drafted by Claude Code

Comment thread src/child_run_tracker.ts
* Gracefully aborts the current run under each name tracked with `abortWithParent`, and makes any later start with
* `abortWithParent` throw. A start in flight is waited for, so the run it starts is aborted too.
*/
async abortRunsWithParent(defaultClient: ApifyClient) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: exit() waits for this listener inside events.close(), which has no timeout, and the process.exit fallback timer is only set after that. A slow abort request (client retries) can hold up exit. A timeout around the aborts would bound it.

✍️ Drafted by Claude Code

The listener that aborts the child runs started with `abortWithParent` is now an internal listener of `PlatformEventManager`, which `off()` doesn't remove and `listeners()` doesn't return, whether called through `Actor` or on the event manager directly. `exit()` removes it only after the events are closed, so an `aborting` event that arrives meanwhile still aborts the child runs.
`exit()` waits for the `aborting` listeners while closing the events, with no timeout of its own and before the `process.exit()` fallback is set, so a slow abort request could hold up the exit past the 30 seconds the platform gives a gracefully aborted run. The aborts are now waited for 10 seconds at most, and the ones still in progress go on in the background.
Documents that a run started with a custom `token` before a migration is aborted with the default client, tests that the abort uses the client the run was started with, and drops a re-throw for an error that can't reach the timeout's catch.
…ger constructor

Replaces the public `onInternal()` / `offInternal()` methods, which let any code register or remove listeners that `off()` can't reach. The Actor now passes its listener that aborts the child runs when it creates its event manager, and it stays there for the event manager's lifetime, so `init()` and `exit()` no longer register or remove it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

t-tooling Issues with this label are in the ownership of the tooling team. tested Temporary label used only programatically for some analytics.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Abort child runs when the parent is gracefully aborted

3 participants