@@ -114,51 +114,58 @@ class CircuitState(enum.Enum):
114114class _RollingWindow :
115115 """Time-bucketed success/failure counters over a rolling window.
116116
117- `window_seconds` is split into `_BUCKET_COUNT` buckets. Each bucket holds
118- [successes, failures] tagged with the integer time-slot it represents; a
119- bucket whose slot is stale is reset on write, and `totals` filters to the
120- live slot range so data older than the window never counts. Every method is
121- synchronous and reads `now` from its caller (so the breaker's critical
122- section owns the clock read).
117+ `window_seconds` is split into `_BUCKET_COUNT` buckets, one per integer time-slot; the
118+ window is the current slot and the `_BUCKET_COUNT - 1` before it. Running totals are
119+ kept beside the buckets, so recording and reading are constant-time. Every method is
120+ synchronous and reads `now` from its caller (so the breaker's critical section owns
121+ the clock read).
123122 """
124123
125124 def __init__ (self , window_seconds : float ) -> None :
126125 self ._bucket_width = window_seconds / _BUCKET_COUNT
127- self ._slot = [- 1 ] * _BUCKET_COUNT
126+ self ._slot : float = - math .inf
127+ self ._index = 0
128128 self ._success = [0 ] * _BUCKET_COUNT
129129 self ._failure = [0 ] * _BUCKET_COUNT
130-
131- def _current_slot (self , now : float ) -> int :
132- return int (now // self ._bucket_width )
130+ self ._total = 0
131+ self ._failures = 0
132+
133+ def _advance (self , now : float ) -> None :
134+ slot = now // self ._bucket_width
135+ if slot <= self ._slot :
136+ return
137+ current = int (slot )
138+ if current - self ._slot >= _BUCKET_COUNT :
139+ self .clear ()
140+ else :
141+ for stale in range (int (self ._slot ) + 1 , current + 1 ):
142+ index = stale % _BUCKET_COUNT
143+ self ._total -= self ._success [index ] + self ._failure [index ]
144+ self ._failures -= self ._failure [index ]
145+ self ._success [index ] = 0
146+ self ._failure [index ] = 0
147+ self ._slot = current
148+ self ._index = current % _BUCKET_COUNT
133149
134150 def record (self , now : float , * , failed : bool ) -> None :
135- slot = self ._current_slot (now )
136- index = slot % _BUCKET_COUNT
137- if self ._slot [index ] != slot : # bucket reused for a new slot — evict
138- self ._slot [index ] = slot
139- self ._success [index ] = 0
140- self ._failure [index ] = 0
151+ self ._advance (now )
141152 if failed :
142- self ._failure [index ] += 1
153+ self ._failure [self ._index ] += 1
154+ self ._failures += 1
143155 else :
144- self ._success [index ] += 1
156+ self ._success [self ._index ] += 1
157+ self ._total += 1
145158
146159 def totals (self , now : float ) -> tuple [int , int ]:
147160 """Return (total, failures) across buckets still inside the window at `now`."""
148- slot = self ._current_slot (now )
149- oldest = slot - _BUCKET_COUNT + 1
150- total = 0
151- failures = 0
152- for i in range (_BUCKET_COUNT ):
153- if oldest <= self ._slot [i ] <= slot :
154- total += self ._success [i ] + self ._failure [i ]
155- failures += self ._failure [i ]
156- return total , failures
161+ self ._advance (now )
162+ return self ._total , self ._failures
157163
158164 def clear (self ) -> None :
159- self ._slot = [- 1 ] * _BUCKET_COUNT
160165 self ._success = [0 ] * _BUCKET_COUNT
161166 self ._failure = [0 ] * _BUCKET_COUNT
167+ self ._total = 0
168+ self ._failures = 0
162169
163170
164171_Event = tuple [str , int , str , dict [str , typing .Any ]]
0 commit comments