fix: only codes 10015/50027 mark a webhook dead; serialize cold creates
T1-1: _is_dead_webhook treated every NotFound/401/404 as dead, so a 404 for another resource (10003 Unknown Channel from a deleted thread=) deleted the picked webhook, possibly a third party's. key on discord's error code only. T2-5: concurrent cold get_or_create calls each created a webhook (8 sends -> 8 webhooks, 7 orphaned against the cap). a cache miss now takes a lock and re-checks the cache before restore-or-create. Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
1 parent
f43ce2d8f1
commit
c76f5286d9
1 file changed
+9
-8
@@ -60,6 +60,7 @@ through :meth:`clear` as usual.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import io
|
||||
import logging
|
||||
import random
|
||||
@@ -71,7 +72,6 @@ log = logging.getLogger(__name__)
|
||||
|
||||
MAX_WEBHOOKS_PER_CHANNEL = 10
|
||||
|
||||
_DEAD_WEBHOOK_STATUSES = (401, 404)
|
||||
_UNKNOWN_WEBHOOK_CODE = 10015
|
||||
_INVALID_TOKEN_CODE = 50027
|
||||
|
||||
@@ -132,11 +132,8 @@ class InMemoryWebhookStore:
|
||||
|
||||
def _is_dead_webhook(exc: discord.HTTPException) -> bool:
|
||||
"""true when an HTTPException signals a webhook that no longer exists or has an invalid
|
||||
token (the recover-and-recreate trigger)"""
|
||||
if isinstance(exc, discord.NotFound):
|
||||
return True
|
||||
if exc.status in _DEAD_WEBHOOK_STATUSES:
|
||||
return True
|
||||
token (the recover-and-recreate trigger); keyed on discord's error code only - a bare
|
||||
404/401 can be another resource (e.g. 10003 Unknown Channel for a deleted thread=)"""
|
||||
return exc.code in (_UNKNOWN_WEBHOOK_CODE, _INVALID_TOKEN_CODE)
|
||||
|
||||
|
||||
@@ -203,6 +200,7 @@ class DPYWebhooks:
|
||||
self.default_name = default_name
|
||||
self._cache: dict[int, discord.Webhook] = {}
|
||||
self._rr: dict[int, dict] = {}
|
||||
self._create_lock = asyncio.Lock()
|
||||
|
||||
async def get_or_create(
|
||||
self,
|
||||
@@ -212,15 +210,18 @@ class DPYWebhooks:
|
||||
) -> discord.Webhook:
|
||||
"""return the channel's managed webhook, restoring from store or creating if absent;
|
||||
raises WebhookCapacityError at the cap unless evict_oldest, and propagates Forbidden
|
||||
when Manage Webhooks is missing"""
|
||||
when Manage Webhooks is missing; concurrent cold calls share one create"""
|
||||
cached = self._cache.get(channel.id)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
async with self._create_lock:
|
||||
cached = self._cache.get(channel.id)
|
||||
if cached is not None:
|
||||
return cached
|
||||
restored = await self._restore_from_store(channel)
|
||||
if restored is not None:
|
||||
return restored
|
||||
|
||||
return await self._create(channel, evict_oldest=evict_oldest)
|
||||
|
||||
async def get(self, channel: discord.TextChannel) -> "Optional[discord.Webhook]":
|
||||
|
||||
Reference in new issue
Block a user