diff --git a/tests/test_trace_client.py b/tests/test_trace_client.py index 7d5d59f..e1150ea 100644 --- a/tests/test_trace_client.py +++ b/tests/test_trace_client.py @@ -247,6 +247,30 @@ def test_a_base_url_without_a_scheme_is_logged_not_a_dead_thread(self): % [r.getMessage() for r in self.log]) self.assertTrue(all(r.levelno == logging.DEBUG for r in self.log)) + def test_the_sender_thread_survives_an_unexpected_error_from_send(self): + crashes = [] + real_send = TraceClient._send + calls = [] + + def send_that_fails_once(client, body): + calls.append(body) + if len(calls) == 1: + raise RuntimeError("boom") + real_send(client, body) + + with mock.patch.object(threading, "excepthook", crashes.append), \ + mock.patch.object(TraceClient, "_send", send_that_fails_once): + client = TraceClient(self.base_url, "MyGame", "1.2.3", key="k") + client.report("first") + client.report("second") + self.assertTrue(self.capture.arrived.wait(5), "a later report is still delivered") + client.close() + self.assertEqual([], crashes, "the sender thread must not die with a traceback on stderr") + self.assertEqual(["second"], [json.loads(r["body"])["name"] for r in self.capture.requests]) + self.assertTrue(any("sender failed" in r.getMessage() and "boom" in r.getMessage() for r in self.log), + [r.getMessage() for r in self.log]) + self.assertTrue(all(r.levelno == logging.DEBUG for r in self.log)) + def test_constructor_rejects_a_missing_base_url_or_application(self): for base_url, application in ((None, "MyGame"), (" ", "MyGame"), ("http://x", None), ("http://x", "")): with self.assertRaises(ValueError): diff --git a/trace_client/trace_client.py b/trace_client/trace_client.py index 0954f67..2c3797f 100644 --- a/trace_client/trace_client.py +++ b/trace_client/trace_client.py @@ -198,7 +198,10 @@ def _drain(self) -> None: body = q.get() if body is None: return - self._send(body) + try: + self._send(body) + except Exception as failure: # noqa: BLE001 - a dead sender would leave the queue filling forever + _LOG.debug("[trace] sender failed on %s: %s", body, failure) def _send(self, body: bytes) -> None: try: