116116#: mechanism exists and an operator opts in with their own number. The cell stays `partial` on the
117117#: shipped default and the record says why; that is the honest outcome, not a disappointing one.
118118DEFAULT_MAX_MESSAGES_PER_SECOND : float | None = None
119+
120+ #: Seconds between operator-facing pacing reports on ONE pacer (BACKLOG #290). Pacing is silent by
121+ #: construction — it never drops, NAKs or errors — so without a report an operator cannot tell a
122+ #: paced interface from a slow one. A pacer in deficit is consulted on every read, so the report is
123+ #: throttled to this window; see :meth:`_MessagePacer._note_paced`.
124+ _PACING_REPORT_SECONDS = 60.0
119125# On stop()/reload, established clients are closed and their handlers given this long to finish an
120126# in-flight commit before the connection tasks are cancelled — bounds shutdown so a peer holding a
121127# connection open can't hang it (review H-2).
@@ -1395,16 +1401,36 @@ class _MessagePacer:
13951401 and drives it with :meth:`deficit` / :meth:`charge`. There is no flag and no branch in here.
13961402 """
13971403
1398- __slots__ = ("_capacity" , "_last" , "_pending_wait" , "_rate" , "_tokens" )
1399-
1400- def __init__ (self , rate : float , burst : float , * , now : float ) -> None :
1404+ __slots__ = (
1405+ "_capacity" ,
1406+ "_last" ,
1407+ "_name" ,
1408+ "_paced_count" ,
1409+ "_paced_seconds" ,
1410+ "_pending_wait" ,
1411+ "_rate" ,
1412+ "_report_at" ,
1413+ "_tokens" ,
1414+ )
1415+
1416+ def __init__ (self , rate : float , burst : float , * , now : float , name : str = "" ) -> None :
14011417 self ._rate = rate
14021418 self ._capacity = max (burst , 1.0 )
14031419 self ._tokens = self ._capacity
14041420 self ._last = now
14051421 #: Debt owed before the next read, in seconds. PRIVATE, and settled only through pace() —
14061422 #: a consult-then-clear a caller performs by hand is an invariant restated once per loop.
14071423 self ._pending_wait = 0.0
1424+ #: The declaring inbound's name, carried only so a pacing report can NAME the connection an
1425+ #: operator has to go and look at (the :attr:`Source.name` precedent). "" when unwired.
1426+ self ._name = name
1427+ #: Applied-delay tally since the last report, reset by each report.
1428+ self ._paced_count = 0
1429+ self ._paced_seconds = 0.0
1430+ #: Next monotonic stamp a report may be emitted at. Starts at ``now`` so the FIRST time
1431+ #: pacing engages is reported immediately — that transition is the event an operator most
1432+ #: needs, and holding it back for a window would hide it behind the throttle.
1433+ self ._report_at = now
14081434
14091435 def charge (self , messages : int , * , now : float ) -> float :
14101436 """Charge ``messages`` and return the seconds to wait before reading again (0.0 if none).
@@ -1428,6 +1454,7 @@ async def pace(self) -> None:
14281454 """
14291455 if (wait := self ._pending_wait ) > 0.0 :
14301456 self ._pending_wait = 0.0
1457+ self ._note_paced (wait )
14311458 await asyncio .sleep (wait )
14321459
14331460 def settle (self , messages : int ) -> None :
@@ -1446,16 +1473,61 @@ def deficit(self, *, now: float) -> float:
14461473 connections has to recompute it at read time instead. Delegates to :meth:`charge` rather
14471474 than re-deriving the arithmetic, so the two can never disagree.
14481475 """
1449- return self .charge (0 , now = now )
1476+ owed = self .charge (0 , now = now )
1477+ if owed > 0.0 :
1478+ self ._note_paced (owed , now = now )
1479+ return owed
1480+
1481+ def _note_paced (self , seconds : float , * , now : float | None = None ) -> None :
1482+ """Tally one APPLIED read delay and report it to the operator, throttled.
1483+
1484+ Called from the two places a wait is actually acted on — :meth:`pace` for the stream pair and
1485+ :meth:`deficit` for the listener pair — never from :meth:`charge`, which both of those route
1486+ through and which ``deficit`` consults on every read. Counting in ``charge`` would tally the
1487+ same outstanding debt once per consult and report a number that is not a count of anything.
1488+
1489+ **Why pacing needs a voice at all.** The control is otherwise entirely invisible: it never
1490+ drops, NAKs, refuses or errors, so a paced interface looks to the operator exactly like a
1491+ slow one, and nothing is written anywhere. The 2026-08-11 ruling ships
1492+ :data:`DEFAULT_MAX_MESSAGES_PER_SECOND` OFF *because* a safe number can only come from a
1493+ site's own feed profile — and a site cannot tune a number it has no way to watch engage.
1494+ Reporting is the half that makes the opt-in posture usable; it changes no default and paces
1495+ nothing differently.
1496+
1497+ Throttled to one line per :data:`_PACING_REPORT_SECONDS` because a pacer that is in deficit
1498+ is consulted on every read, and an unthrottled line would restate one fact thousands of
1499+ times. WARNING rather than INFO: a clinical interface being held back is an operator-facing
1500+ condition, not routine chatter.
1501+
1502+ **Metadata only.** The connection name, a count, a duration and the configured rate — never a
1503+ frame, a peer address, or a byte of the body being paced (PHI.md; CLAUDE.md section 9).
1504+ """
1505+ self ._paced_count += 1
1506+ self ._paced_seconds += seconds
1507+ stamp = time .monotonic () if now is None else now
1508+ if stamp < self ._report_at :
1509+ return
1510+ logger .warning (
1511+ "inbound message pacing engaged on %s: %d read delay(s) totalling %.3fs "
1512+ "(max_messages_per_second=%g). The sender is being held back, not refused — no message "
1513+ "is dropped. Raise the rate if this feed is legitimate." ,
1514+ self ._name or "<unnamed inbound>" ,
1515+ self ._paced_count ,
1516+ self ._paced_seconds ,
1517+ self ._rate ,
1518+ )
1519+ self ._paced_count = 0
1520+ self ._paced_seconds = 0.0
1521+ self ._report_at = stamp + _PACING_REPORT_SECONDS
14501522
14511523 @classmethod
1452- def for_rate (cls , rate : float | None , burst : float ) -> _MessagePacer | None :
1524+ def for_rate (cls , rate : float | None , burst : float , * , name : str = "" ) -> _MessagePacer | None :
14531525 """Build a pacer, or ``None`` when no rate is configured — the shipped default.
14541526
14551527 The single place the off-default is honoured, so the four intakes that pace cannot drift
14561528 apart on what "unset" means. See :data:`DEFAULT_MAX_MESSAGES_PER_SECOND` for why off.
14571529 """
1458- return cls (rate , burst , now = time .monotonic ()) if rate else None
1530+ return cls (rate , burst , now = time .monotonic (), name = name ) if rate else None
14591531
14601532
14611533def _pacing_settings (settings : Mapping [str , Any ]) -> tuple [float | None , float ]:
@@ -1494,6 +1566,8 @@ def __init__(self, config: Source) -> None:
14941566 # Message-rate pacing. Absent -> OFF, unlike the caps above; see _pacing_settings and
14951567 # DEFAULT_MAX_MESSAGES_PER_SECOND for why that deviation is deliberate and ruled.
14961568 self .max_messages_per_second , self .message_burst = _pacing_settings (s )
1569+ # Carried only so a pacing report can name this connection (BACKLOG #290).
1570+ self ._pacing_name = config .name or ""
14971571 # Per-connection peer-IP allowlist (Tier 4 operability): when set, a connecting peer whose IP
14981572 # is not listed is refused at accept time. Absent/empty = no restriction.
14991573 sa = s .get ("source_ip_allowlist" )
@@ -1629,7 +1703,9 @@ async def _on_client(self, reader: asyncio.StreamReader, writer: asyncio.StreamW
16291703 await self ._emit_event ("established" , peer_host = peer_host )
16301704 try :
16311705 decoder = MLLPDecoder (max_frame_bytes = self .max_frame_bytes )
1632- pacer = _MessagePacer .for_rate (self .max_messages_per_second , self .message_burst )
1706+ pacer = _MessagePacer .for_rate (
1707+ self .max_messages_per_second , self .message_burst , name = self ._pacing_name
1708+ )
16331709 while True :
16341710 # ASVS 2.4.1 / 15.2.2 — the wait is BEFORE the read, never around the handler.
16351711 if pacer is not None :
0 commit comments