Skip to content

feat: support named child runs that survive migration and resurrection - #759

Merged
barjin merged 33 commits into
masterfrom
feat/track-child-runs
Oct 15, 2026
Merged

barjin merged 33 commits into
masterfrom
feat/track-child-runs

Conversation

@barjin

@barjin barjin commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Adds a runName option to Actor.start, startTask, call and callTask that reattaches to the recorded child run after a restart, and the Actor.childRuns getter to read them.

Closes #738, closes #740

@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

⚠️ There are broken links in the documentation.

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

@barjin barjin left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

A few ideas / comments where this implementation differs from the Orchestrator ⬇️

Comment thread src/actor.ts Outdated
Comment thread src/child_run_tracker.ts
Comment thread src/actor.ts Outdated
@barjin barjin self-assigned this Sep 30, 2026
@barjin
barjin marked this pull request as ready for review September 30, 2026 09:55
@barjin
barjin requested review from B4nan and janbuchar September 30, 2026 09:55
@apify-service-account apify-service-account added tested Temporary label used only programatically for some analytics. t-tooling Issues with this label are in the ownership of the tooling team. labels Sep 30, 2026
Comment thread src/actor.ts Outdated
Comment thread src/child_run_tracker.ts
Comment thread src/child_run_tracker.ts
Comment thread src/child_run_tracker.ts Outdated
Comment thread src/actor.ts
barjin and others added 4 commits October 6, 2026 13:35
Co-authored-by: Martin Adámek <banan23@gmail.com>
Co-authored-by: Martin Adámek <banan23@gmail.com>
Co-authored-by: Martin Adámek <banan23@gmail.com>
`Date` and `URL` inputs were hashed as `{}` and function inputs were dropped, so inputs differing only in those
values got the same checksum and the earlier run was resumed. Hash the JSON form `apify-client` sends instead,
and sort keys by code units, as `localeCompare` depends on the locale.
@barjin
barjin requested a review from B4nan October 6, 2026 12:56
@B4nan

B4nan commented Oct 6, 2026

Copy link
Copy Markdown
Member

Lets compare this with apify/apify-sdk-python#1149

@B4nan

B4nan commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Compared with the Python counterpart, which is split into apify/apify-sdk-python#1149 (named runs) and apify/apify-sdk-python#1151 (Actor.child_runs()):

JS (this PR) Python
Option runName name
Methods start, call, callTask start, call (no call_task)
KVS key CHILD_RUNS APIFY_CHILD_RUNS
Stored record runId, status, startedAt, checksum, history[{runId, status, startedAt}] actorId, runId, previousRunIds
Name reused for a different request throws if the Actor/task or input differs ValueError only if the actor ID differs, a new input is accepted
ABORTED / TIMED-OUT replaced by a new run resurrected (with build, memory, timeout and max charge passed on)
ABORTING / TIMING-OUT a new run starts right away, while the old one is still finishing waits for it to finish, then resurrects it
FAILED / run gone new run, old one kept in history same
Concurrent calls with the same name per-name promise chain per-name asyncio.Lock
call log on a resumed run fromStart: !resumed same, plus a status message watcher; a SUCCEEDED run returns without streaming
childRuns() stored snapshot with the last observed status, no API calls fetches every run from the API; run: None if it's gone
Malformed stored data not checked raises ValueError
Tests unit unit + e2e

Things we should settle across both SDKs:

  1. KVS key and record format. These should match, so the stored data looks the same whichever SDK wrote it.
  2. Resurrect or replace aborted/timed-out runs (your question above). Either way, we should wait out ABORTING / TIMING-OUT before starting a replacement, otherwise the old and new runs overlap.
  3. Whether the name check compares input (JS) or just the Actor (Python).
  4. Whether childRuns() fetches fresh status from the API.
  5. Python has no named call_task yet.

@vdusek

vdusek commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Python PRs (apify/apify-sdk-python#1149, apify/apify-sdk-python#1151) have been changed to match JS behavior in two places:

  • the option is now named run_name,
  • and call_task supports it as well.

I'm still thinking about the rest.

@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.

See this separate comment #759 (comment).

Comment thread src/child_run_tracker.ts Outdated
Comment thread src/child_run_tracker.ts Outdated
Comment thread src/child_run_tracker.ts
const trackedRun = tracked ? await client.run(tracked.runId).get() : undefined;

if (trackedRun && ['SUCCEEDED', 'READY', 'RUNNING'].includes(trackedRun.status)) {
await this.verifyRequest(runName, request);

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.

The checksum is only checked on this reattach path, so a name whose recorded run failed or got lost accepts a different input, and the new checksum overwrites the old one. Python raises in that case too, so a name stays bound to one Actor/task + input. Which way we want to go?

✍️ Drafted by Claude Code

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'd prefer to only fail on a (wrong) reattach.

If the user calls await Actor.call('foo/bar', { foo: "baz" }, { runName: 'boo' }); and it gives him a run of the foo/bar Actor with { foo: "baz" }, everything's imo good and there's no reason to fail.

I'd keep the error only for situations when the runName call returns something different from a regular, non-runName call (e.g., reattaches to an existing run with a different input).

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.

Makes sense, I agree. That was the last open point, so I guess I can approve it. Once this is merged, I'll make the same changes in the Python SDK.

Comment thread src/child_run_tracker.ts
@vdusek

vdusek commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Compared with the Python counterpart (apify/apify-sdk-python#1149 for named runs, apify/apify-sdk-python#1151 for child_runs). Every row is now settled. On a replaced run with a different input, Python will be changed to match JS after this is merged.

Full comparison table:

Area JS (this PR) Python Aligned Notes
Option name runName run_name ✅ Python renamed name → run_name
Supported methods start, call, startTask, callTask start, call, start_task, call_task ✅ JS added Actor.startTask() in this PR (#764)
Record format runId, status, startedAt, checksum, history[] same ✅
Checksum sha256 of sorted-key JSON of {type, id, input} same, byte-equal ✅
Write timing right after start right after start ✅
READY / RUNNING reattach reattach ✅
SUCCEEDED return as is return as is ✅
FAILED / run gone new run, old one moved to history same ✅
ABORTED / TIMED-OUT resurrected with the call's build, memory, timeout and max charge same ✅ Agreed, see r4230501550
ABORTING / TIMING-OUT waits for it to finish, then resurrects same ✅ See r4230501533
Actor.resurrect() added in #769 added in apify/apify-sdk-python#1165 ✅
404 on the recorded run retried for 3 s first same ✅ See r4230501519
Different input, run reattached throws raises ✅ Applies to a resurrected run too
Different input, run replaced allowed, checksum overwritten raises, will be allowed as in JS ✅ Agreed to fail only on a reattach; Python follow-up after merge
Concurrent calls under one name per-name lock per-name lock ✅
childRuns API sync getter, Record<string, RunClient> sync property, dict[str, RunClientAsync] ✅ Python aligned in apify/apify-sdk-python#1151
Registry load in init() in __aenter__ ✅
Custom token in childRuns kept in-process, default client after migration same ✅ ported from Python
History in childRuns KVS record only KVS record only ✅
Access before init warns, returns {} raises ✅ each SDK's existing convention, fine as is
Malformed record throws at init ValueError at init ✅
Max charge / max_items forwarded to the start and to the resurrection same ✅
Tests unit + e2e (reattach and resurrect) unit + e2e (reboot → reattach) ✅

Edit: updated on 2026-10-14

✍️ Drafted by Claude Code

vdusek added a commit to apify/apify-sdk-python that referenced this pull request Oct 9, 2026
`Actor.start`, `Actor.call`, `Actor.start_task` and `Actor.call_task`
accept an optional `run_name`. Right after the child starts, the SDK
records it in the default key-value store under `__ACTOR_CHILD_RUNS`.
After a migration or resurrection of the parent, the same call finds the
recorded run and reuses it:

- `READY` / `RUNNING`: reattaches to it
- `SUCCEEDED`: returns it as is
- `ABORTED` / `TIMED-OUT`: resurrects it with the call's build, memory,
timeout, `max_items` and max charge (an `ABORTING` / `TIMING-OUT` run is
waited for first)
- `FAILED`, or the run no longer exists: starts a new run and moves the
old one to `history`

The record has the same shape as in the JS SDK (apify/apify-sdk-js#759):
`runId`, `status`, `startedAt`, `checksum` and `history`. The checksum
covers the Actor or task ID and the input, computed the same way as in
JS. Reusing a name for a different Actor, task or input raises
`ValueError`. Concurrent calls under one name in the same process share
a single run.

A named `call` on a reattached or resurrected run streams only new log
lines. If the API reports a recorded run as missing, the SDK retries for
3 seconds before replacing it, because a run started moments ago may not
be on every API replica yet.

A hard kill between the platform starting the child and the registry
write can still orphan the child. Closing that gap needs an idempotency
key on the run-start endpoint.

Closes: #1127

*✍️ Drafted by Claude Code*
vdusek added a commit to apify/apify-sdk-python that referenced this pull request Oct 9, 2026
`Actor.child_runs` is a sync property that maps each run name to a
`RunClientAsync` for the current run under that name. It has the same
shape as `Actor.childRuns` in the JS SDK (apify/apify-sdk-js#759). The
client covers the run and its storages:

```python
run = await Actor.child_runs['scrape-eu'].wait_for_finish()
```

The SDK reads the registry from the default key-value store when the
Actor initializes, so runs recorded before a migration or resurrection
are included and reading the property makes no API calls. That adds one
record read to every init. A malformed record fails the init and leaves
the Actor uninitialized. Runs started without a `run_name` aren't
tracked. Runs that were replaced under a name stay in the `history` of
the `__ACTOR_CHILD_RUNS` record and aren't exposed.

A run started or reattached with a custom `token` gets a client with
that token. A run recorded before a migration and not reattached since
gets the default client, because the token isn't stored.

It also fixes a duplicate `max_items` deprecation warning from #1149: a
named start that resurrects its recorded run no longer warns a second
time from inside the SDK.

Docs are in #1160.

Closes: #1129

*✍️ Drafted by Claude Code*
B4nan
B4nan previously requested changes Oct 9, 2026
Comment thread src/actor.ts Outdated
Comment thread src/actor.ts Outdated
Comment thread src/actor.ts
Comment thread src/actor.ts Outdated
Comment thread src/actor.ts Outdated
Comment thread src/utils.ts Outdated
Comment thread src/child_run_tracker.ts Outdated
Comment thread test/apify/child_runs.test.ts Outdated
B4nan added a commit that referenced this pull request Oct 14, 2026
Enables oxlint's `import/extensions` rule, so a relative import without
the `.js` extension fails lint. `src` is already covered by `NodeNext`
resolution, but `test/tsconfig.json` uses `Bundler` and lets these
through (see
#759 (comment)).
Package imports are ignored, and master passes with the rule on.
@barjin
barjin requested review from B4nan, janbuchar and vdusek October 14, 2026 09:11
@barjin

barjin commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

All the discrepancies between Python and JS implementation have been cleared in the recent commits.

The last difference is the behaviour on failed / lost runs (see thread here and vote).

@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.

LGTM 🚀

(I just updated the PR title to start with a verb.)

@vdusek vdusek changed the title feat: named child runs that survive migration and resurrection feat: support named child runs that survive migration and resurrection Oct 14, 2026

@janbuchar janbuchar 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.

LGTM, it seems to have convinced Vláďa's LLM assistant

@barjin
barjin dismissed B4nan’s stale review October 15, 2026 09:01

All the points from this review have been resolved.

@barjin
barjin merged commit 9235254 into master Oct 15, 2026
12 checks passed
@barjin
barjin deleted the feat/track-child-runs branch October 15, 2026 09:01
vdusek added a commit to apify/apify-sdk-python that referenced this pull request Oct 15, 2026
Follow-up to #1149 and #1151 that aligns named child runs with the final
version of apify/apify-sdk-js#759.

Changes:

- A different Actor, task or input under an existing `run_name` raises a
`ValueError` only when the recorded run would be reused (reattached,
returned as `SUCCEEDED`, or resurrected). When the recorded run `FAILED`
or is gone, a new run starts with the new request, the old run moves to
`history`, and the record takes the new checksum ([agreed
here](apify/apify-sdk-js#759 (comment))).
- The client of a named run is stored only after the run is started or
reused.
- The `run_name` docstrings describe the new rule.

No action on [B4nan's
comment](apify/apify-sdk-js#759 (comment))
about loading the child-run record lazily instead of in Actor init. JS
kept the load in `init()`, because the synchronous `Actor.childRuns`
getter needs the records, and Python already loads them the same way in
`__aenter__`.

The docs in #1160 still say a name is bound to its first input. That
will be fixed there.

*✍️ Drafted by Claude Code*
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.

Expose tracked child runs as a readable API Named child runs that survive migration and resurrection

5 participants