Repository navigation
feat: support named child runs that survive migration and resurrection - #759
Conversation
…Actor.childRuns()
|
See more at https://github.com/apify/apify-sdk-js/actions/runs/38527819404#summary-115732253368 |
barjin
left a comment
There was a problem hiding this comment.
A few ideas / comments where this implementation differs from the Orchestrator ⬇️
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.
|
Lets compare this with apify/apify-sdk-python#1149 |
|
Compared with the Python counterpart, which is split into apify/apify-sdk-python#1149 (named runs) and apify/apify-sdk-python#1151 (
Things we should settle across both SDKs:
|
|
Python PRs (apify/apify-sdk-python#1149, apify/apify-sdk-python#1151) have been changed to match JS behavior in two places:
I'm still thinking about the rest. |
| const trackedRun = tracked ? await client.run(tracked.runId).get() : undefined; | ||
|
|
||
| if (trackedRun && ['SUCCEEDED', 'READY', 'RUNNING'].includes(trackedRun.status)) { | ||
| await this.verifyRequest(runName, request); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
|
Compared with the Python counterpart (apify/apify-sdk-python#1149 for named runs, apify/apify-sdk-python#1151 for Full comparison table:
Edit: updated on 2026-10-14 ✍️ Drafted by Claude Code |
`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*
`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*
# Conflicts: # src/actor.ts
… them Runs that are still aborting or timing out are awaited first, as only finished runs can be resurrected. The resurrection gets the build, memory, run timeout and limits of the call.
A run started moments ago may not be on every API replica yet, so a single 404 could start a duplicate run.
…ns after a reboot
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.
|
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). |
janbuchar
left a comment
There was a problem hiding this comment.
LGTM, it seems to have convinced Vláďa's LLM assistant
All the points from this review have been resolved.
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*
Adds a
runNameoption toActor.start,startTask,callandcallTaskthat reattaches to the recorded child run after a restart, and theActor.childRunsgetter to read them.Closes #738, closes #740