fix(issue-5): record an accepted intent, not a refused one
The subscriber marked a notification intent as seen before the monotonic sequence guard ran. A late packet was then dropped by that guard with its intent already burned, so the genuine retry of a notification this subscriber had never delivered was mistaken for a replay and dropped too. Identity comparison stays ahead of the sequence guard -- a duplicate must not consume a sequence number -- but the bookkeeping write moves behind it, so "seen" means "accepted" rather than "arrived". An intent that cannot yield a stable identity (not an object, or holding an unhashable value) is now refused instead of raising out of the callback: a paho callback that raises takes the network loop down for every job that subscriber is watching. Six regressions; three of them fail on the unfixed tree, and one drives a real notifier retry through the real consumer with no hand-built payload. Full suite: 732 passed, no skips. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd
This commit is contained in:
1 parent
9a651ccc71
commit
8d7e010f9f
4 files changed
+162
-5
No files matched your search
@@ -140,20 +140,43 @@ class _Watcher:
|
||||
if seq is None or not isinstance(seq, int):
|
||||
logger.warning("drop event for job %s: missing or invalid seq", jid)
|
||||
return
|
||||
intent = (payload.get("data") or {}).get("notification_intent")
|
||||
if isinstance(intent, dict):
|
||||
data = payload.get("data")
|
||||
intent = data.get("notification_intent") if isinstance(data, dict) else None
|
||||
identity = None
|
||||
if intent is not None:
|
||||
# Present but not an object: it cannot yield a stable identity, so the
|
||||
# replay guard below could not protect it. Refuse rather than admit a
|
||||
# notification we would be unable to recognise on its retry.
|
||||
if not isinstance(intent, dict):
|
||||
logger.warning("drop event for job %s: notification_intent is %s, not an object",
|
||||
jid, type(intent).__name__)
|
||||
return
|
||||
identity = (intent.get("seq"), intent.get("event"), intent.get("timestamp"))
|
||||
if identity in self.seen_intents.setdefault(jid, set()):
|
||||
try:
|
||||
already_seen = identity in self.seen_intents.setdefault(jid, set())
|
||||
except TypeError:
|
||||
# A malformed sender can put an unhashable value in a field. That
|
||||
# must not escape defensive parsing as an exception: a callback
|
||||
# raising here would take down the network loop for every job.
|
||||
logger.warning("drop event for job %s: notification intent %r has no stable identity",
|
||||
jid, intent)
|
||||
return
|
||||
if already_seen:
|
||||
logger.info("drop event for job %s: notification intent %s already accepted",
|
||||
jid, identity)
|
||||
return
|
||||
self.seen_intents[jid].add(identity)
|
||||
|
||||
if seq <= self.last_seq.get(jid, 0):
|
||||
logger.warning("drop event for job %s: seq %d is not monotonically increasing (last %d)",
|
||||
jid, seq, self.last_seq.get(jid, 0))
|
||||
return
|
||||
self.last_seq[jid] = seq
|
||||
# Recorded only once the event is actually accepted. A late packet that the
|
||||
# sequence guard refuses must not mark its intent as seen, or the genuine
|
||||
# retry that follows would be mistaken for the replay of an event this
|
||||
# subscriber never delivered.
|
||||
if identity is not None:
|
||||
self.seen_intents[jid].add(identity)
|
||||
# Persistent audit log from the *subscriber's* vantage point: every event
|
||||
# that survives defensive parsing is recorded here, including ones a
|
||||
# different host published. This is the external-observer record that
|
||||
|
||||
Reference in new issue
Block a user