diff --git a/faust/agents/agent.py b/faust/agents/agent.py index 036b0c46d..7ba604e5b 100644 --- a/faust/agents/agent.py +++ b/faust/agents/agent.py @@ -235,7 +235,22 @@ def on_init_dependencies(self) -> Iterable[ServiceT]: return [] def actor_tracebacks(self) -> List[str]: - return [actor.traceback() for actor in self._actors] + tracebacks: List[str] = [] + for actor in self._actors: + try: + tracebacks.append(actor.traceback()) + except RuntimeError as exc: + # A coroutine/async generator that has finished naturally has + # no stack frame (``cr_frame``/``ag_frame`` is ``None``), and + # ``mode`` raises "cannot find stack of coroutine" when asked + # to format it. This happens during ``wait_empty`` around + # shutdown and rebalances, where tracebacks are collected + # purely for logging -- so a missing frame must not crash the + # whole collection. See issue #105. + tracebacks.append( + f"Could not extract traceback for actor {actor!r}: {exc}" + ) + return tracebacks async def _start_one( self, diff --git a/tests/unit/agents/test_agent.py b/tests/unit/agents/test_agent.py index 25f033ac6..da0157c0a 100644 --- a/tests/unit/agents/test_agent.py +++ b/tests/unit/agents/test_agent.py @@ -239,6 +239,29 @@ def test_cancel(self, *, agent): actor1.cancel.assert_called_once_with() actor2.cancel.assert_called_once_with() + def test_actor_tracebacks(self, *, agent): + actor1 = Mock(name="actor1") + actor1.traceback.return_value = "traceback for actor1" + agent._actors = [actor1] + assert agent.actor_tracebacks() == ["traceback for actor1"] + + def test_actor_tracebacks__missing_stack(self, *, agent): + # A finished coroutine has no stack frame, and mode raises + # RuntimeError("cannot find stack of coroutine"). Collecting + # tracebacks (for logging around shutdown/rebalance) must not + # let that error escape. See issue #105. + good = Mock(name="good_actor") + good.traceback.return_value = "traceback for good" + bad = Mock(name="bad_actor") + bad.traceback.side_effect = RuntimeError("cannot find stack of coroutine") + agent._actors = [good, bad] + + tracebacks = agent.actor_tracebacks() + + assert tracebacks[0] == "traceback for good" + assert "Could not extract traceback" in tracebacks[1] + assert "cannot find stack of coroutine" in tracebacks[1] + @pytest.mark.asyncio async def test_on_partitions_revoked(self, *, agent): revoked = {TP("foo", 0)}