CoolFace
Apppublic

Rhinox13/chatapi

sourceHugging Faceupdated 3mo agoView on Hugging Face
0likes
realtime.py323 linesDownload Raw Back to services
1from __future__ import annotations
2
3from dataclasses import dataclass, field
4import queue
5import threading
6import uuid
7from typing import Any, Callable
8
9from ..repositories import ConversationStore, UserStore
10
11
12@dataclass
13class RealtimeSubscription:
14    owner_id: str
15    events: queue.Queue[dict[str, Any]] = field(default_factory=lambda: queue.Queue(maxsize=100))
16    closed: threading.Event = field(default_factory=threading.Event)
17
18
19@dataclass
20class ActiveConnection:
21    connection_id: str
22    owner_id: str
23    kind: str
24    close_callback: Callable[[], None] | None = None
25
26
27@dataclass(frozen=True)
28class ConnectionLease:
29    connection_id: str
30    owner_id: str
31    kind: str
32
33
34class ConnectionLimitExceeded(RuntimeError):
35    pass
36
37
38class RealtimeBroker:
39    def __init__(self, store: ConversationStore, user_store: UserStore):
40        self._store = store
41        self._user_store = user_store
42        self._lock = threading.Lock()
43        self._subscriptions_by_owner: dict[str, list[RealtimeSubscription]] = {}
44        self._connections_by_owner: dict[str, list[ActiveConnection]] = {}
45
46    @staticmethod
47    def _normalize_limit(value: Any, default: int = 0) -> int:
48        try:
49            return max(0, int(value or default))
50        except (TypeError, ValueError):
51            return default
52
53    def acquire_connection(
54        self,
55        owner_id: str,
56        *,
57        kind: str,
58        max_connections: int = 0,
59        max_connections_per_user: int = 0,
60        close_callback: Callable[[], None] | None = None,
61    ) -> ConnectionLease:
62        connection = ActiveConnection(
63            connection_id=uuid.uuid4().hex,
64            owner_id=owner_id,
65            kind=kind,
66            close_callback=close_callback,
67        )
68        affected_owners: set[str] = set()
69        with self._lock:
70            affected_owners |= self._enforce_limits_locked(
71                owner_id,
72                max_connections=max_connections,
73                max_connections_per_user=max_connections_per_user,
74            )
75            if self._would_exceed_limits_locked(
76                owner_id,
77                max_connections=max_connections,
78                max_connections_per_user=max_connections_per_user,
79            ):
80                raise ConnectionLimitExceeded("connection limit exceeded")
81            self._connections_by_owner.setdefault(owner_id, []).append(connection)
82            affected_owners.add(owner_id)
83        self._publish_connection_counts(affected_owners)
84        return ConnectionLease(
85            connection_id=connection.connection_id,
86            owner_id=owner_id,
87            kind=kind,
88        )
89
90    def subscribe(
91        self,
92        owner_id: str,
93        *,
94        max_connections: int = 0,
95        max_connections_per_user: int = 0,
96        queue_size: int = 100,
97    ) -> RealtimeSubscription:
98        queue_size = max(1, self._normalize_limit(queue_size, 100))
99        subscription = RealtimeSubscription(
100            owner_id=owner_id,
101            events=queue.Queue(maxsize=queue_size),
102        )
103        lease = self.acquire_connection(
104            owner_id,
105            kind="websocket",
106            max_connections=max_connections,
107            max_connections_per_user=max_connections_per_user,
108            close_callback=subscription.closed.set,
109        )
110        setattr(subscription, "_connection_lease", lease)
111        with self._lock:
112            subscribers = self._subscriptions_by_owner.setdefault(owner_id, [])
113            subscribers.append(subscription)
114        return subscription
115
116    def _enforce_limits_locked(
117        self,
118        owner_id: str,
119        *,
120        max_connections: int,
121        max_connections_per_user: int,
122    ) -> set[str]:
123        affected_owners: set[str] = set()
124        max_connections = self._normalize_limit(max_connections)
125        max_connections_per_user = self._normalize_limit(max_connections_per_user)
126
127        if max_connections_per_user > 0:
128            while self._owner_connection_count_locked(owner_id) >= max_connections_per_user:
129                removed = self._drop_oldest_connection_locked(owner_id=owner_id)
130                if removed is None:
131                    break
132                affected_owners.add(removed.owner_id)
133
134        if max_connections <= 0:
135            return affected_owners
136        while self._connection_count_locked() >= max_connections:
137            removed = self._drop_oldest_connection_locked()
138            if removed is None:
139                return affected_owners
140            affected_owners.add(removed.owner_id)
141        return affected_owners
142
143    def _connection_count_locked(self) -> int:
144        return sum(len(items) for items in self._connections_by_owner.values())
145
146    def _owner_connection_count_locked(self, owner_id: str) -> int:
147        return len(self._connections_by_owner.get(owner_id, ()))
148
149    def _would_exceed_limits_locked(
150        self,
151        owner_id: str,
152        *,
153        max_connections: int,
154        max_connections_per_user: int,
155    ) -> bool:
156        max_connections = self._normalize_limit(max_connections)
157        max_connections_per_user = self._normalize_limit(max_connections_per_user)
158        if max_connections_per_user > 0 and self._owner_connection_count_locked(owner_id) >= max_connections_per_user:
159            return True
160        if max_connections > 0 and self._connection_count_locked() >= max_connections:
161            return True
162        return False
163
164    def _drop_oldest_connection_locked(self, owner_id: str | None = None) -> ActiveConnection | None:
165        candidate_owners = [owner_id] if owner_id is not None else list(self._connections_by_owner.keys())
166        for candidate_owner in candidate_owners:
167            connections = self._connections_by_owner.get(candidate_owner, [])
168            for index, connection in enumerate(connections):
169                if connection.close_callback is None:
170                    continue
171                connections.pop(index)
172                if connections:
173                    self._connections_by_owner[candidate_owner] = connections
174                else:
175                    self._connections_by_owner.pop(candidate_owner, None)
176                self._close_connection_locked(connection)
177                return connection
178        return None
179
180    def _close_connection_locked(self, connection: ActiveConnection) -> None:
181        self._remove_subscription_for_connection_locked(connection.connection_id)
182        if connection.close_callback is not None:
183            connection.close_callback()
184
185    def _remove_subscription_for_connection_locked(self, connection_id: str) -> None:
186        for owner_id, subscribers in list(self._subscriptions_by_owner.items()):
187            remaining = [
188                subscription
189                for subscription in subscribers
190                if getattr(getattr(subscription, "_connection_lease", None), "connection_id", None) != connection_id
191            ]
192            if len(remaining) == len(subscribers):
193                continue
194            if remaining:
195                self._subscriptions_by_owner[owner_id] = remaining
196            else:
197                self._subscriptions_by_owner.pop(owner_id, None)
198            return
199
200    def unsubscribe(self, subscription: RealtimeSubscription) -> None:
201        lease = getattr(subscription, "_connection_lease", None)
202        if isinstance(lease, ConnectionLease):
203            self.release_connection(lease)
204            return
205        with self._lock:
206            subscribers = self._subscriptions_by_owner.get(subscription.owner_id)
207            if not subscribers:
208                return
209            self._subscriptions_by_owner[subscription.owner_id] = [
210                item for item in subscribers if item is not subscription
211            ]
212            if not self._subscriptions_by_owner[subscription.owner_id]:
213                self._subscriptions_by_owner.pop(subscription.owner_id, None)
214
215    def release_connection(self, lease: ConnectionLease) -> None:
216        affected_owner: str | None = None
217        with self._lock:
218            connections = self._connections_by_owner.get(lease.owner_id)
219            if not connections:
220                return
221            remaining = [item for item in connections if item.connection_id != lease.connection_id]
222            if len(remaining) == len(connections):
223                return
224            affected_owner = lease.owner_id
225            if remaining:
226                self._connections_by_owner[lease.owner_id] = remaining
227            else:
228                self._connections_by_owner.pop(lease.owner_id, None)
229            self._remove_subscription_for_connection_locked(lease.connection_id)
230        if affected_owner is not None:
231            self._publish_connection_counts({affected_owner})
232
233    def count_owner_connections(self, owner_id: str) -> int:
234        with self._lock:
235            return self._owner_connection_count_locked(owner_id)
236
237    def count_connections(self) -> int:
238        with self._lock:
239            return self._connection_count_locked()
240
241    def publish_snapshot(self, owner_id: str) -> None:
242        self._publish(owner_id, self.build_snapshot(owner_id))
243
244    def publish_conversation_upsert(self, owner_id: str, conversation_id: str) -> None:
245        conversation = self._store.get_conversation(conversation_id, owner_id)
246        if conversation is None:
247            self.publish_conversation_delete(owner_id, conversation_id)
248            return
249        try:
250            messages = self._store.get_messages(conversation_id, owner_id)
251        except ValueError:
252            messages = []
253        self._publish(
254            owner_id,
255            {
256                "type": "conversation_upsert",
257                "conversation": conversation.to_dict(),
258                "messages": [message.to_dict() for message in messages],
259            },
260        )
261
262    def publish_conversation_delete(self, owner_id: str, conversation_id: str) -> None:
263        self._publish(
264            owner_id,
265            {
266                "type": "conversation_delete",
267                "conversation_id": conversation_id,
268            },
269        )
270
271    def _publish(self, owner_id: str, event: dict[str, Any]) -> None:
272        with self._lock:
273            subscribers = tuple(self._subscriptions_by_owner.get(owner_id, ()))
274        for subscription in subscribers:
275            self._offer_event(subscription, event)
276
277    def _publish_connection_counts(self, owner_ids: set[str]) -> None:
278        for owner_id in owner_ids:
279            self._publish(
280                owner_id,
281                {
282                    "type": "connection_count",
283                    "current_connection_count": self.count_owner_connections(owner_id),
284                },
285            )
286
287    @staticmethod
288    def _offer_event(subscription: RealtimeSubscription, event: dict[str, Any]) -> None:
289        try:
290            subscription.events.put_nowait(event)
291            return
292        except queue.Full:
293            pass
294        try:
295            subscription.events.get_nowait()
296        except queue.Empty:
297            pass
298        try:
299            subscription.events.put_nowait(event)
300        except queue.Full:
301            subscription.closed.set()
302
303    @staticmethod
304    def _force_event(subscription: RealtimeSubscription, event: dict[str, Any]) -> None:
305        while True:
306            try:
307                subscription.events.put_nowait(event)
308                return
309            except queue.Full:
310                try:
311                    subscription.events.get_nowait()
312                except queue.Empty:
313                    return
314
315    def build_snapshot(
316        self,
317        owner_id: str,
318    ) -> dict[str, Any]:
319        return {
320            "type": "snapshot",
321            "conversations": [item.to_dict() for item in self._store.list_conversations(owner_id)],
322        }
323