Skip to content

Commit 0948935

Browse files
committed
fix: reserve the limit of a resurrected child run once
1 parent ea0c9f4 commit 0948935

2 files changed

Lines changed: 32 additions & 3 deletions

File tree

‎src/apify/_child_runs.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -508,8 +508,8 @@ async def _budget(
508508
async with charging_manager.charge_lock():
509509
available = charging_manager.calculate_remaining_budget()
510510
record = (await self._load()).get(name)
511-
if replaces_current and record is not None:
512-
available += self._current_charge(name, record)
511+
current_charge = self._current_charge(name, record) if replaces_current and record is not None else 0
512+
available += current_charge
513513
if available <= 0:
514514
raise RuntimeError(
515515
f'Child run "{name}" was not started, since the budget of this Actor run is spent or reserved for '
@@ -521,7 +521,8 @@ async def _budget(
521521
f'Lowering the charge limit of child run "{name}" to {limit} USD, the budget left for it',
522522
extra={'requested_usd': str(max_total_charge_usd)},
523523
)
524-
self._reserving[name] = limit
524+
# A resurrected run's current charge is reserved by its record already.
525+
self._reserving[name] = max(limit - current_charge, Decimal(0))
525526

526527
try:
527528
yield limit, limit

‎tests/unit/actor/test_actor_child_runs.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,6 +1303,34 @@ def resurrect(run_client: Any, **_: Any) -> Run:
13031303
assert charge_result.charged_count == 0
13041304

13051305

1306+
async def test_resurrection_in_flight_reserves_its_limit_once(
1307+
parent_budget: dict[str, Run], apify_client_async_patcher: ApifyClientAsyncPatcher
1308+
) -> None:
1309+
"""While a resurrection is in flight, its limit is reserved once, including the charge its run made before."""
1310+
parent_budget['old-run'] = finish(make_run('old-run', 'RUNNING'), 'ABORTED', 1, finished_ago=timedelta(minutes=5))
1311+
started = asyncio.Event()
1312+
release = asyncio.Event()
1313+
1314+
async def resurrect(run_client: Any, **_: Any) -> Run:
1315+
started.set()
1316+
await release.wait()
1317+
return parent_budget[run_client._resource_id].model_copy(update={'status': 'RUNNING', 'finished_at': None})
1318+
1319+
async with Actor:
1320+
await seed_budget_record('child', 'old-run', maxTotalChargeUsd='6')
1321+
1322+
apify_client_async_patcher.patch('run', 'resurrect', replacement_method=resurrect, is_async=True)
1323+
1324+
async with _ActorType() as actor:
1325+
start_task = asyncio.create_task(actor.start('some-actor', name='child', max_total_charge_usd=Decimal(4)))
1326+
await started.wait()
1327+
charge_result = await actor.charge('some-event', count=10)
1328+
release.set()
1329+
await start_task
1330+
1331+
assert charge_result.charged_count == 6
1332+
1333+
13061334
async def test_failed_resurrection_leaves_the_charge_of_its_run_to_settle(
13071335
parent_budget: dict[str, Run], apify_client_async_patcher: ApifyClientAsyncPatcher, monkeypatch: pytest.MonkeyPatch
13081336
) -> None:

0 commit comments

Comments
 (0)