Skip to content

fix: persist the charged event counts across container restarts - #843

Open
Pijukatel wants to merge 3 commits into
masterfrom
claude/focused-archimedes-yoql5b
Open

Pijukatel wants to merge 3 commits into
masterfrom
claude/focused-archimedes-yoql5b

Conversation

@Pijukatel

@Pijukatel Pijukatel commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

After a reboot, migration or restart, the platform seeds the new container with the charged event counts recorded when the run started, so the charging manager began from a fresh maxTotalChargeUsd budget. The SDK now carries its own counts across in the default key-value store.

  • The charged event counts are persisted under __ACTOR_CHARGING_STATE, written on every PERSIST_STATE event, which Actor.reboot() and a migration dispatch before the container stops, and once more on exit. The write uses direct storage access, so a reboot from inside a request handler, where a storage transaction is open, still reaches the store.
  • A pay-per-event run on the platform reads the record at start and takes the persisted count of each event where it is higher than the env var's. Local runs keep the counts in memory only.
  • The per-event totals are derived from the counts and the prices instead of being stored.

Parity with the Python SDK fix in apify/apify-sdk-python#1222, which fixes apify/apify-sdk-python#1209.

After a reboot, migration or restart the platform seeds the new container
with the `APIFY_CHARGED_ACTOR_EVENT_COUNTS` recorded when the run started,
so the charging manager began from zero charged events and allowed a fresh
`maxTotalChargeUsd` budget.

The charging manager now keeps its charged event counts in a Crawlee
`RecoverableState` stored in the default key-value store under
`__ACTOR_CHARGING_STATE`. It is written on every `PERSIST_STATE` event,
which `Actor.reboot()` and a migration dispatch before the container stops,
and once more on exit. A pay-per-event run on the platform reads it at
start and takes the persisted count of each event where it is higher than
the env var's. Local runs keep their counts in memory only. The per-event
totals are derived from the counts and the prices instead of being stored.

Same fix as apify/apify-sdk-python#1222.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017ocuAKQTqReMDTjXCUTnKP
@github-actions github-actions Bot added the t-tooling Issues with this label are in the ownership of the tooling team. label Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 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/42202593178#summary-127841278079

The charging manager owns the persistence of its charged event counts and
writes them with direct storage access, so a `PERSIST_STATE` dispatched
inside a request handler's storage transaction, as `Actor.reboot()` does
when called from a handler, reaches the store instead of being buffered
into a transaction that never commits.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017ocuAKQTqReMDTjXCUTnKP
The counts are serialized when a write starts, so a periodic write still
in flight when a reboot persists could land after the reboot's write and
leave older counts in the store. Each write now waits for the previous
one. The persist listener is a private field, as the Actor's own are.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017ocuAKQTqReMDTjXCUTnKP
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

⚠️ There are broken links in the documentation.

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

@Pijukatel
Pijukatel requested a review from janbuchar November 6, 2026 12:31
@Pijukatel
Pijukatel marked this pull request as ready for review November 6, 2026 12:32
@apify-service-account apify-service-account added the tested Temporary label used only programatically for some analytics. label Nov 6, 2026
@Pijukatel Pijukatel added the adhoc Ad-hoc unplanned task added during the sprint. label Nov 6, 2026

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

adhoc Ad-hoc unplanned task added during the sprint. 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.

ChargingManager reads charged counts and total as 0 after Actor.reboot(), so maxTotalChargeUsd allows a fresh budget

3 participants