diff --git a/tractor/runtime/_portal.py b/tractor/runtime/_portal.py index 5ef82659..e0577d69 100644 --- a/tractor/runtime/_portal.py +++ b/tractor/runtime/_portal.py @@ -334,8 +334,11 @@ class Portal: # `move_on_after` fired — peer didn't ack within # bounded window. Behaviour depends on # `raise_on_timeout`: - assert cs.cancelled_caught - if raise_on_timeout: + if ( + cs.cancelled_caught + and + raise_on_timeout + ): raise ActorTooSlowError( f'Peer {peer_id} did not ack `Actor.cancel()`' f'-RPC within bounded wait of ' @@ -344,6 +347,11 @@ class Portal: # legacy fire-and-forget path: log + return False so # the caller can decide whether to escalate. + # + # NOTE, we also land here in the (unexpected) case where + # the shielded `move_on_after` block exits WITHOUT + # `return True` and WITHOUT the deadline firing — prefer + # a soft `False` over an `assert`-crash mid-teardown. log.debug( f'May have failed to cancel peer?\n' f'\n' diff --git a/tractor/runtime/_supervise.py b/tractor/runtime/_supervise.py index 5e66fa8f..d488ca0c 100644 --- a/tractor/runtime/_supervise.py +++ b/tractor/runtime/_supervise.py @@ -151,7 +151,20 @@ async def _try_cancel_then_kill( f' reason: {too_slow}\n' f'-> escalating to `proc.terminate()` (hard-kill)\n' ) - proc.terminate() + # XXX, the `subint` backend stores an `int` interp-id in the + # `proc` slot (not a `Process`), so it has no `.terminate()`. + # Guard here so a cancel-ack timeout doesn't `AttributeError` + # once that backend lands; its hard-kill path is a TODO. + if hasattr(proc, 'terminate'): + proc.terminate() + else: + log.error( + f'Cannot hard-kill sub-actor — backend proc-handle ' + f'{proc!r} ({type(proc).__name__!r}) has no ' + f'`.terminate()`!\n' + f' uid: {subactor.aid.reprol()!r}\n' + f'TODO: per-backend cancel-escalation.\n' + ) class ActorNursery: