From 6769b4da196c6452e8616cd9da395ef40c31e016 Mon Sep 17 00:00:00 2001 From: AndrewAct Date: Thu, 23 Jul 2026 15:17:53 -0500 Subject: [PATCH] Count no-op ticks so due == sent + no_op in worker logs _claim_and_send() had several "due but nothing to do" branches (already resolved, pending-not-stale, retries exhausted, claim race) that fell through without incrementing any counter, making due != sent + skipped look like unexplained missing sends in the Tick complete log. Co-Authored-By: Claude Sonnet 5 --- backend/TODO.md | 12 ------------ backend/apps/workers/horoscope_email.py | 13 ++++++++++--- .../tests/services/test_worker_horoscope_email.py | 3 ++- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/backend/TODO.md b/backend/TODO.md index 1fd8730..8d2f829 100644 --- a/backend/TODO.md +++ b/backend/TODO.md @@ -1,13 +1 @@ # Backend TODO - -## Worker: add a no-op counter to TickResult - -`apps/workers/horoscope_email.py` — `TickResult` currently tracks `due`, `sent`, `skipped`, -but `_claim_and_send()` has several "due but nothing to do" branches (already resolved -today, pending-not-yet-stale, failed-max-attempts, claim race lost) that fall through to -`return` at line ~141-144 without incrementing any counter. Result: `due != sent + skipped`, -which reads as unexplained missing sends in the `Tick complete` log even though it's benign. - -Fix: add a counter (e.g. `no_op`) to `TickResult`, increment it right before that `return`, -and include it in the `Tick complete` log line so `due == sent + no_op` (skipped stays a -separate, unrelated bucket — see the `_is_due()` vs `_claim_and_send()` distinction). diff --git a/backend/apps/workers/horoscope_email.py b/backend/apps/workers/horoscope_email.py index 391a830..33c3c46 100644 --- a/backend/apps/workers/horoscope_email.py +++ b/backend/apps/workers/horoscope_email.py @@ -37,6 +37,7 @@ class TickResult: due: int = 0 sent: int = 0 skipped: int = 0 + no_op: int = 0 @dataclass @@ -139,8 +140,10 @@ async def _claim_and_send( logger.debug("sub=%s date=%s already resolved (status=%s)", sub_id, local_date, status) if delivery_id is None: - # Retries exhausted, nothing stale yet, or claimed by another worker between our - # read and write — all fine, nothing to do. + # Retries exhausted, nothing stale yet, already resolved, or claimed by another + # worker between our read and write — all fine, nothing to do. Counted so + # due == sent + no_op (skipped is a disjoint bucket from _is_due, see above). + result.no_op += 1 return await deps.delivery_service.send_daily_horoscope(subscription, delivery_id, local_date) @@ -170,7 +173,11 @@ async def _run_tick_safely(deps: WorkerDeps) -> None: return if result.due: logger.info( - "Tick complete: due=%s sent=%s skipped=%s", result.due, result.sent, result.skipped + "Tick complete: due=%s sent=%s no_op=%s skipped=%s", + result.due, + result.sent, + result.no_op, + result.skipped, ) diff --git a/backend/tests/services/test_worker_horoscope_email.py b/backend/tests/services/test_worker_horoscope_email.py index b2263fa..9b13903 100644 --- a/backend/tests/services/test_worker_horoscope_email.py +++ b/backend/tests/services/test_worker_horoscope_email.py @@ -75,7 +75,7 @@ async def test_tick_skips_when_already_resolved_today(): result = await tick(deps) - assert result == TickResult(due=1, sent=0, skipped=0) + assert result == TickResult(due=1, sent=0, skipped=0, no_op=1) deps.delivery_service.send_daily_horoscope.assert_not_awaited() deps.repository.claim_delivery.assert_not_awaited() deps.repository.reclaim_failed_delivery.assert_not_awaited() @@ -132,6 +132,7 @@ async def test_tick_does_not_reclaim_when_nothing_is_stale(): result = await tick(deps) assert result.sent == 0 + assert result.no_op == 1 deps.delivery_service.send_daily_horoscope.assert_not_awaited()