Explorar o código

Use SingletonSlot for thread-safe chattolib client

- Add AsyncSingletonSlot class for thread-safe async client creation
- Update _get_chatto_client() to use AsyncSingletonSlot with double-checked locking
- Maintain self._chatto_client cache for backward compatibility
- Clear client cache on disconnect and relogin

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
Liv Klumpp hai 1 semana
pai
achega
b7840b7074
Modificáronse 1 ficheiros con 62 adicións e 16 borrados
  1. 62 16
      adapter.py

+ 62 - 16
adapter.py

@@ -35,6 +35,7 @@ import hashlib
 import logging
 import logging
 import mimetypes
 import mimetypes
 import os
 import os
+import threading
 from collections import OrderedDict
 from collections import OrderedDict
 from datetime import datetime, timezone
 from datetime import datetime, timezone
 from typing import Any, Dict, List, Optional
 from typing import Any, Dict, List, Optional
@@ -54,6 +55,35 @@ from gateway.config import Platform
 # Chattolib imports (lazy loaded)
 # Chattolib imports (lazy loaded)
 from tools.lazy_deps import lazy_import
 from tools.lazy_deps import lazy_import
 
 
+# Thread-safe singleton helper
+from plugins.plugin_utils import SingletonSlot
+
+
+class AsyncSingletonSlot:
+    """Thread-safe async singleton helper (extends SingletonSlot pattern for async factories)."""
+    def __init__(self):
+        self._instance = None
+        self._lock = threading.Lock()
+        self._future = None
+
+    async def get(self, factory):
+        """Get or create instance using async factory. Thread-safe with double-checked locking."""
+        if self._instance is not None:
+            return self._instance
+
+        with self._lock:
+            if self._instance is None:
+                if self._future is None:
+                    self._future = asyncio.ensure_future(factory())
+                return await self._future
+            return self._instance
+
+    def reset(self):
+        """Reset the singleton instance."""
+        with self._lock:
+            self._instance = None
+            self._future = None
+
 ChattoClient = lazy_import("chattolib", "ChattoClient")
 ChattoClient = lazy_import("chattolib", "ChattoClient")
 ChattoError = lazy_import("chattolib", "ChattoError")
 ChattoError = lazy_import("chattolib", "ChattoError")
 ChattoAuthError = lazy_import("chattolib", "ChattoAuthError")
 ChattoAuthError = lazy_import("chattolib", "ChattoAuthError")
@@ -169,8 +199,9 @@ class ChattoAdapter(BasePlatformAdapter):
                 str(c) for c in extra.get("free_response_channels", []) if str(c).strip()
                 str(c) for c in extra.get("free_response_channels", []) if str(c).strip()
             )
             )
 
 
-        # --- Chattolib client ---
-        self._chatto_client: Optional[ChattoClient] = None
+        # --- Chattolib client (thread-safe lazy singleton) ---
+        self._client_slot: AsyncSingletonSlot = AsyncSingletonSlot()
+        self._chatto_client: Optional[ChattoClient] = None  # Cached client instance
 
 
         # --- Runtime state ---
         # --- Runtime state ---
         self._token: Optional[str] = None
         self._token: Optional[str] = None
@@ -205,7 +236,8 @@ class ChattoAdapter(BasePlatformAdapter):
     # ------------------------------------------------------------------ #
     # ------------------------------------------------------------------ #
 
 
     async def _get_chatto_client(self) -> Optional[ChattoClient]:
     async def _get_chatto_client(self) -> Optional[ChattoClient]:
-        """Get or create a ChattoClient instance."""
+        """Get or create a ChattoClient instance using thread-safe async singleton."""
+        # Fast path: return cached client if available
         if self._chatto_client is not None:
         if self._chatto_client is not None:
             return self._chatto_client
             return self._chatto_client
         
         
@@ -214,13 +246,16 @@ class ChattoAdapter(BasePlatformAdapter):
             return None
             return None
         
         
         try:
         try:
-            self._chatto_client = await ChattoClient.login(
-                self._login,
-                self._password,
-                base_url=self._base_url,
+            client = await self._client_slot.get(
+                lambda: ChattoClient.login(
+                    self._login,
+                    self._password,
+                    base_url=self._base_url,
+                )
             )
             )
+            self._chatto_client = client  # Cache for direct access
             logger.info("Chatto: logged in as %s via chattolib", self._login)
             logger.info("Chatto: logged in as %s via chattolib", self._login)
-            return self._chatto_client
+            return client
         except ChattoAuthError as e:
         except ChattoAuthError as e:
             logger.error("Chatto: authentication failed: %s", e)
             logger.error("Chatto: authentication failed: %s", e)
             self._set_fatal_error("auth_failed", str(e), retryable=True)
             self._set_fatal_error("auth_failed", str(e), retryable=True)
@@ -230,6 +265,10 @@ class ChattoAdapter(BasePlatformAdapter):
             self._set_fatal_error("client_error", str(e), retryable=True)
             self._set_fatal_error("client_error", str(e), retryable=True)
             return None
             return None
 
 
+    async def _client(self) -> ChattoClient:
+        """Helper to get the chattolib client. Thread-safe via AsyncSingletonSlot."""
+        return await self._get_chatto_client()
+
     async def _ensure_token(self) -> bool:
     async def _ensure_token(self) -> bool:
         """Login via chattolib."""
         """Login via chattolib."""
         if self._token:
         if self._token:
@@ -245,7 +284,8 @@ class ChattoAdapter(BasePlatformAdapter):
     async def _relogin(self) -> bool:
     async def _relogin(self) -> bool:
         """Force re-login (token expired)."""
         """Force re-login (token expired)."""
         self._token = None
         self._token = None
-        self._chatto_client = None  # Also clear chattolib client
+        self._chatto_client = None  # Clear cached client
+        self._client_slot.reset()  # Clear chattolib client singleton
         return await self._ensure_token()
         return await self._ensure_token()
 
 
 
 
@@ -258,7 +298,7 @@ class ChattoAdapter(BasePlatformAdapter):
         if not await self._ensure_token():
         if not await self._ensure_token():
             return False
             return False
 
 
-        client = self._chatto_client
+        client = await self._get_chatto_client()
         
         
         # Get our own user info
         # Get our own user info
         try:
         try:
@@ -391,6 +431,8 @@ class ChattoAdapter(BasePlatformAdapter):
                 pass
                 pass
             self._ws_task = None
             self._ws_task = None
         self._token = None
         self._token = None
+        self._chatto_client = None
+        self._client_slot.reset()
 
 
     # ------------------------------------------------------------------ #
     # ------------------------------------------------------------------ #
     # Liveness probe
     # Liveness probe
@@ -438,7 +480,8 @@ class ChattoAdapter(BasePlatformAdapter):
             if not self._running:
             if not self._running:
                 return
                 return
             try:
             try:
-                await self._chatto_client.get_viewer()
+                client = await self._get_chatto_client()
+                await client.get_viewer()
                 failures = 0
                 failures = 0
                 # Refresh presence to keep showing as online
                 # Refresh presence to keep showing as online
                 try:
                 try:
@@ -479,7 +522,8 @@ class ChattoAdapter(BasePlatformAdapter):
     async def _join_room(self, room_id: str) -> None:
     async def _join_room(self, room_id: str) -> None:
         """Join a room if not already a member."""
         """Join a room if not already a member."""
         try:
         try:
-            await self._chatto_client.join_room(room_id=room_id)
+            client = await self._get_chatto_client()
+            await client.join_room(room_id=room_id)
             logger.debug("Chatto: joined room %s (%s)", room_id, self._room_names.get(room_id, room_id))
             logger.debug("Chatto: joined room %s (%s)", room_id, self._room_names.get(room_id, room_id))
         except ChattoError as e:
         except ChattoError as e:
             if "permission_denied" in str(e).lower() or "403" in str(e):
             if "permission_denied" in str(e).lower() or "403" in str(e):
@@ -493,9 +537,10 @@ class ChattoAdapter(BasePlatformAdapter):
             room_service_pb2 = lazy_import("chattolib._pb.chatto.api.v1", "room_service_pb2")
             room_service_pb2 = lazy_import("chattolib._pb.chatto.api.v1", "room_service_pb2")
             pb_to_dict = lazy_import("chattolib._transport", "pb_to_dict")
             pb_to_dict = lazy_import("chattolib._transport", "pb_to_dict")
             
             
-            resp = await self._chatto_client.services.rooms.get_room_events(
+            client = await self._get_chatto_client()
+            resp = await client.services.rooms.get_room_events(
                 room_service_pb2.GetRoomEventsRequest(room_id=room_id),
                 room_service_pb2.GetRoomEventsRequest(room_id=room_id),
-                headers=self._chatto_client._headers(),
+                headers=client._headers(),
             )
             )
             data = pb_to_dict(resp)
             data = pb_to_dict(resp)
             events = data.get("page", {}).get("events", [])
             events = data.get("page", {}).get("events", [])
@@ -556,7 +601,7 @@ class ChattoAdapter(BasePlatformAdapter):
         This replaces the manual WebSocket loop with chattolib's high-level
         This replaces the manual WebSocket loop with chattolib's high-level
         stream_events() which provides pre-decoded RealtimeEvent objects.
         stream_events() which provides pre-decoded RealtimeEvent objects.
         """
         """
-        client = self._chatto_client
+        client = await self._get_chatto_client()
         
         
         backoff = _WS_RECONNECT_INITIAL_BACKOFF
         backoff = _WS_RECONNECT_INITIAL_BACKOFF
         
         
@@ -1331,7 +1376,8 @@ class ChattoAdapter(BasePlatformAdapter):
         """Add a reaction to a message via MessageService/AddReaction."""
         """Add a reaction to a message via MessageService/AddReaction."""
         shortcode = self._emoji_to_shortcode(emoji)
         shortcode = self._emoji_to_shortcode(emoji)
         try:
         try:
-            result = await self._chatto_client.add_reaction(
+            client = await self._client()
+            result = await client.add_reaction(
                 room_id=str(chat_id),
                 room_id=str(chat_id),
                 message_event_id=str(message_id),
                 message_event_id=str(message_id),
                 emoji=shortcode,
                 emoji=shortcode,