From 9114fb5fe2aa6703254bdfd6b3af2250373c9d76 Mon Sep 17 00:00:00 2001 From: Jan Date: Sat, 3 Oct 2026 08:46:13 +0200 Subject: [PATCH] fix(gamedata): log fio transport errors at warning in refresh tasks - planet and user FIO refresh log httpx.TransportError (timeouts, dropped connections) as a warning without traceback; retry bookkeeping unchanged - tests cover a ReadTimeout in both tasks, incl. the planet retry state - CLAUDE.md logging rule updated to match Closes #75 Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 5 +++-- backend/gamedata/tasks.py | 6 ++++++ backend/tests/gamedata/test_tasks.py | 8 +++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index bbdfcfa..ceae42f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -124,8 +124,9 @@ shipped by Vector (`vector.toml`) to Axiom, where they feed dashboards. outage. Beat has to run with `--scheduler core.beat:QueueDepthScheduler`. - Refresh failures: `fio_refresh_failed`, `planet_refresh_failed`, `planet_infrastructure_refresh_failed`, `cxpc_refresh_failed`, - `exchanges_refresh_failed`. A FIO error status is a warning with - `status_code`, anything else `logger.exception`. + `exchanges_refresh_failed`. A FIO error status (with `status_code`) or + transport error (timeout, dropped connection) is a warning, anything else + `logger.exception`. - Levels: `info` for a business event, `warning` for an expected failure (bad user FIO key, invalid webhook payload), `logger.exception` inside `except` for what needs a look. Log a failure once, where it's handled. diff --git a/backend/gamedata/tasks.py b/backend/gamedata/tasks.py index aaec18e..12ef009 100644 --- a/backend/gamedata/tasks.py +++ b/backend/gamedata/tasks.py @@ -80,6 +80,9 @@ def gamedata_refresh_planet() -> bool: planet_natural_id=to_update.planet_natural_id, status_code=exc.response.status_code, ) + elif isinstance(exc, httpx.TransportError): + # FIO slow or unreachable (timeout, dropped connection): expected, fio_request_failed has the error + logger.warning('planet_refresh_failed', planet_natural_id=to_update.planet_natural_id) else: logger.exception('planet_refresh_failed', planet_natural_id=to_update.planet_natural_id) to_update.update_refresh_result(error=exc) @@ -269,6 +272,9 @@ def gamedata_refresh_user_fiodata(user_id: int, *_legacy_args: str) -> bool: # retrying won't fix a rejected key: fail now; new credentials reset the row (user/signals.py) to_update.fio_status_code = 401 to_update.automation_error_count = GameFIOPlayerData.MAX_RETRIES - 1 + elif isinstance(exc, httpx.TransportError): + # FIO slow or unreachable: expected, fio_request_failed has the error + log.warning('fio_refresh_failed') else: log.exception('fio_refresh_failed') to_update.update_refresh_result(error=exc, commit=False) diff --git a/backend/tests/gamedata/test_tasks.py b/backend/tests/gamedata/test_tasks.py index 1bdb467..86c81a8 100644 --- a/backend/tests/gamedata/test_tasks.py +++ b/backend/tests/gamedata/test_tasks.py @@ -63,6 +63,7 @@ def test_refresh_planet(self, scenario): 'error, level', [ (httpx.HTTPStatusError('503', request=MagicMock(), response=MagicMock(status_code=503)), 'WARNING'), + (httpx.ReadTimeout('The read operation timed out'), 'WARNING'), (ValueError('bug'), 'ERROR'), ], ) @@ -83,8 +84,12 @@ def test_refresh_planet_logs_its_failure( if isinstance(r.msg, dict) and r.msg['event'] == 'planet_refresh_failed' ] assert [(lvl, msg['planet_natural_id']) for lvl, msg in failed] == [(level, 'M')] - # a traceback only for what is not an HTTP error status + # a traceback only for what is not an HTTP error status or transport failure assert ('exception' in failed[0][1]) is (level == 'ERROR') + # the failure is recorded for a retry either way + planet = GamePlanet.objects.get(planet_natural_id='M') + assert (planet.automation_refresh_status, planet.automation_error_count) == ('retrying', 1) + assert planet.automation_next_retry_at is not None @pytest.mark.parametrize('scenario', ['missing', 'fio_fail', 'success']) @patch('gamedata.tasks.get_fio_service') @@ -100,6 +105,7 @@ def test_refresh_user_fiodata(self, mock_get_fio, scenario): 'error, level', [ (httpx.HTTPStatusError('401', request=MagicMock(), response=MagicMock(status_code=401)), 'WARNING'), + (httpx.ReadTimeout('The read operation timed out'), 'WARNING'), (ValueError('bug'), 'ERROR'), ], )