fix: per-call attempt counter so concurrent sends don't corrupt attempts
the attempt count used instance state (self._attempt_no), reset in _send_loop and incremented in _attempt; two concurrent send() calls on one Webhook interleaved, corrupting each other's WebhookResult.attempts. it is now a per-call mutable cell created in _send_loop and threaded into _attempt. verified under load: 400 concurrent sends, zero corrupted counts. Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
parent
bcf1f5511c
commit
cd93e4f44f
@ -48,7 +48,9 @@ Webhook(
|
||||
```
|
||||
|
||||
Inject a shared `session` for throughput (one session per process); without one, each
|
||||
send opens and closes its own.
|
||||
send opens and closes its own. A single `Webhook` instance is safe to drive from many
|
||||
concurrent `send()` calls — each call tracks its own attempt count, so concurrent sends
|
||||
don't corrupt each other's `WebhookResult.attempts`.
|
||||
|
||||
## WebhookResult
|
||||
|
||||
|
||||
@ -136,10 +136,10 @@ class Webhook:
|
||||
synthetic status-0). proxy rotation on connection errors lives inside the
|
||||
attempt and is capped separately.
|
||||
"""
|
||||
self._attempt_no = 0
|
||||
counter = [0]
|
||||
try:
|
||||
return await aretry(
|
||||
lambda: self._attempt(session, url, payload),
|
||||
lambda: self._attempt(session, url, payload, counter),
|
||||
attempts=self.max_retries + 1,
|
||||
on=(_Retryable,),
|
||||
)
|
||||
@ -147,7 +147,7 @@ class Webhook:
|
||||
return exhausted.result
|
||||
|
||||
async def _attempt(
|
||||
self, session: aiohttp.ClientSession, url: str, payload: Dict
|
||||
self, session: aiohttp.ClientSession, url: str, payload: Dict, counter: List[int]
|
||||
) -> WebhookResult:
|
||||
"""one logical send: proxy rotation + a single POST; may raise _Retryable
|
||||
|
||||
@ -155,14 +155,18 @@ class Webhook:
|
||||
aretry applies backoff; honors an explicit 429 retry_after by sleeping it
|
||||
before signalling. returns a final WebhookResult on success or a terminal
|
||||
(non-retryable) failure — never lets a provider/connection error escape.
|
||||
|
||||
`counter` is a per-call mutable cell ([0]) owned by the calling `_send_loop`,
|
||||
so the attempt count is local to one `send()` and concurrent sends on the
|
||||
same instance don't corrupt each other's tally.
|
||||
"""
|
||||
timeout = aiohttp.ClientTimeout(total=self.timeout)
|
||||
last_proxy: Optional[str] = None
|
||||
proxy_tries = 0
|
||||
|
||||
while True:
|
||||
self._attempt_no += 1
|
||||
attempts = self._attempt_no
|
||||
counter[0] += 1
|
||||
attempts = counter[0]
|
||||
proxy_url = None
|
||||
if self._proxies is not None:
|
||||
try:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user