@@ -363,14 +363,23 @@ async def _execute_record(
363363 ).load ()
364364 await asyncio .shield (self ._record (cancelled ))
365365 if charged :
366- await asyncio .shield (
367- self .budget_ledger .refund (
368- backend_id ,
369- request .evaluation_set ,
370- cost ,
371- principal ,
366+ try :
367+ await asyncio .shield (
368+ self .budget_ledger .refund (
369+ backend_id ,
370+ request .evaluation_set ,
371+ cost ,
372+ principal ,
373+ )
372374 )
373- )
375+ except Exception as refund_error :
376+ # The shield covers cancellation, not a refund that fails on
377+ # its own: an OSError from the ledger's write propagates here
378+ # in place of error, so the caller never sees the original
379+ # signal while the reservation stays charged -- the leak these
380+ # handlers exist to prevent. Chain it and re-raise the
381+ # original, as BudgetStore.refund already does.
382+ raise error from refund_error
374383 raise
375384 except EvaluationExecutionError as error :
376385 failure = EvaluationStore (
@@ -381,16 +390,25 @@ async def _execute_record(
381390 # refund and leak the reservation's budget.
382391 await asyncio .shield (self ._record (failure ))
383392 if charged :
384- await asyncio .shield (
385- self .budget_ledger .refund (
386- backend_id ,
387- request .evaluation_set ,
388- cost ,
389- principal ,
393+ try :
394+ await asyncio .shield (
395+ self .budget_ledger .refund (
396+ backend_id ,
397+ request .evaluation_set ,
398+ cost ,
399+ principal ,
400+ )
390401 )
391- )
402+ except Exception as refund_error :
403+ # The shield covers cancellation, not a refund that fails on
404+ # its own: an OSError from the ledger's write propagates here
405+ # in place of error, so the caller never sees the original
406+ # signal while the reservation stays charged -- the leak these
407+ # handlers exist to prevent. Chain it and re-raise the
408+ # original, as BudgetStore.refund already does.
409+ raise error from refund_error
392410 raise
393- except asyncio .CancelledError :
411+ except asyncio .CancelledError as cancellation :
394412 # Last line of defence for the reservation. The evaluator's own
395413 # handlers convert cancellation and failure into the two typed errors
396414 # above, but each of them has to await a persist before it can raise,
@@ -403,14 +421,23 @@ async def _execute_record(
403421 # evaluation id, so there is no record to load. The evaluator already
404422 # persisted one on its shielded paths.
405423 if charged :
406- await asyncio .shield (
407- self .budget_ledger .refund (
408- backend_id ,
409- request .evaluation_set ,
410- cost ,
411- principal ,
424+ try :
425+ await asyncio .shield (
426+ self .budget_ledger .refund (
427+ backend_id ,
428+ request .evaluation_set ,
429+ cost ,
430+ principal ,
431+ )
412432 )
413- )
433+ except Exception as refund_error :
434+ # The shield covers cancellation, not a refund that fails on
435+ # its own: an OSError from the ledger's write propagates here
436+ # in place of cancellation, so the caller never sees the original
437+ # signal while the reservation stays charged -- the leak these
438+ # handlers exist to prevent. Chain it and re-raise the
439+ # original, as BudgetStore.refund already does.
440+ raise cancellation from refund_error
414441 raise
415442 # Shielded for the same reason as the two handlers above, and one more:
416443 # the budget was charged for an evaluation that has now actually run, so
0 commit comments