|
16 | 16 | import os |
17 | 17 | import re |
18 | 18 | import time |
| 19 | +from collections import OrderedDict |
19 | 20 | from collections.abc import AsyncIterable, Awaitable, Callable |
20 | 21 | from contextvars import ContextVar |
21 | 22 | from datetime import UTC, datetime |
@@ -197,8 +198,9 @@ def __init__(self, config: SlackAdapterConfig | None = None) -> None: |
197 | 198 | # Channel external/shared cache |
198 | 199 | self._external_channels: set[str] = set() |
199 | 200 |
|
200 | | - # Cache of AsyncWebClient instances keyed by bot token |
201 | | - self._client_cache: dict[str, Any] = {} |
| 201 | + # Cache of AsyncWebClient instances keyed by bot token (LRU-bounded) |
| 202 | + self._client_cache: OrderedDict[str, Any] = OrderedDict() |
| 203 | + self._client_cache_max = 100 # max cached clients |
202 | 204 |
|
203 | 205 | # Multi-workspace OAuth fields |
204 | 206 | self._client_id: str | None = config.client_id or (os.environ.get("SLACK_CLIENT_ID") if zero_config else None) |
@@ -264,18 +266,35 @@ def _get_client(self, token: str | None = None) -> Any: |
264 | 266 | Clients are cached by token so we avoid creating a new instance on |
265 | 267 | every request. The import is deferred so that ``slack_sdk`` is only |
266 | 268 | required at call-time. |
| 269 | +
|
| 270 | + When *token* is explicitly passed (even as ``""``) it is used as-is; |
| 271 | + only when *token* is ``None`` do we fall back to ``_get_token()``. |
267 | 272 | """ |
268 | | - resolved_token = token or self._get_token() |
269 | | - cached = self._client_cache.get(resolved_token) |
270 | | - if cached is not None: |
271 | | - return cached |
| 273 | + resolved_token = self._get_token() if token is None else token |
| 274 | + |
| 275 | + if resolved_token in self._client_cache: |
| 276 | + self._client_cache.move_to_end(resolved_token) |
| 277 | + return self._client_cache[resolved_token] |
272 | 278 |
|
273 | 279 | from slack_sdk.web.async_client import AsyncWebClient |
274 | 280 |
|
275 | 281 | client = AsyncWebClient(token=resolved_token) |
276 | 282 | self._client_cache[resolved_token] = client |
| 283 | + if len(self._client_cache) > self._client_cache_max: |
| 284 | + # Evict oldest (LRU) |
| 285 | + evicted_token, evicted_client = self._client_cache.popitem(last=False) |
| 286 | + # Close the evicted client's session if possible |
| 287 | + try: |
| 288 | + if hasattr(evicted_client, "session") and evicted_client.session: |
| 289 | + asyncio.get_running_loop().create_task(evicted_client.session.close()) |
| 290 | + except RuntimeError: |
| 291 | + pass |
277 | 292 | return client |
278 | 293 |
|
| 294 | + def _invalidate_client(self, token: str) -> None: |
| 295 | + """Remove a cached client (e.g., on token revocation).""" |
| 296 | + self._client_cache.pop(token, None) |
| 297 | + |
279 | 298 | # ------------------------------------------------------------------ |
280 | 299 | # Initialization |
281 | 300 | # ------------------------------------------------------------------ |
@@ -2670,17 +2689,24 @@ def _handle_slack_error(self, error: Any) -> None: |
2670 | 2689 | Never returns (always raises). |
2671 | 2690 | """ |
2672 | 2691 | slack_error = error |
2673 | | - code = getattr(slack_error, "response", {}) |
2674 | | - if isinstance(code, dict): |
2675 | | - code.get("error") |
2676 | | - else: |
2677 | | - getattr(getattr(slack_error, "response", None), "get", lambda *a: None)("error") |
| 2692 | + resp = getattr(slack_error, "response", None) |
| 2693 | + error_code: str | None = None |
| 2694 | + if isinstance(resp, dict): |
| 2695 | + error_code = resp.get("error") |
| 2696 | + elif resp is not None: |
| 2697 | + error_code = getattr(resp, "get", lambda *a: None)("error") |
| 2698 | + |
| 2699 | + # Invalidate cached client on auth errors (token revocation / invalid_auth) |
| 2700 | + if error_code in ("invalid_auth", "token_revoked", "account_inactive"): |
| 2701 | + try: |
| 2702 | + token = self._get_token() |
| 2703 | + self._invalidate_client(token) |
| 2704 | + except AuthenticationError: |
| 2705 | + pass |
2678 | 2706 |
|
2679 | 2707 | # Check for rate limiting |
2680 | | - if hasattr(slack_error, "response"): |
2681 | | - resp = slack_error.response |
2682 | | - if isinstance(resp, dict) and resp.get("error") == "ratelimited": |
2683 | | - raise AdapterRateLimitError("slack") from error |
| 2708 | + if isinstance(resp, dict) and error_code == "ratelimited": |
| 2709 | + raise AdapterRateLimitError("slack") from error |
2684 | 2710 |
|
2685 | 2711 | raise error # type: ignore[misc] |
2686 | 2712 |
|
|
0 commit comments