openvidia 2.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,93 @@
1
+ """Optional cross-node state sync via Redis pub/sub.
2
+
3
+ Multi-node setups (several proxy instances behind the same pool of keys)
4
+ share cooldowns and circuit state so node A does not hammer a key that node
5
+ B just cooled down. Redis is an OPTIONAL extra: without it the proxy runs
6
+ standalone, in-memory, exactly as before.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import json
13
+ import uuid
14
+
15
+ try:
16
+ import redis.asyncio as aioredis
17
+ except ImportError: # redis extra not installed
18
+ aioredis = None
19
+
20
+ _CHANNEL = "openvidia:events"
21
+
22
+
23
+ class RedisSync:
24
+ """Publish local state events and apply remote ones via on_remote."""
25
+
26
+ def __init__(self, url: str) -> None:
27
+ self.url = url
28
+ self.node_id = uuid.uuid4().hex[:8]
29
+ self.on_remote = None
30
+ self._client = aioredis.from_url(url) if aioredis else None
31
+ self._queue: asyncio.Queue[dict] = asyncio.Queue()
32
+ self._tasks: list[asyncio.Task] = []
33
+ self._pubsub = None
34
+
35
+ @property
36
+ def enabled(self) -> bool:
37
+ return self._client is not None
38
+
39
+ def broadcast(self, payload: dict) -> None:
40
+ """Fire-and-forget publish; the queue keeps it non-blocking."""
41
+ if not self.enabled:
42
+ return
43
+ try:
44
+ self._queue.put_nowait(payload)
45
+ except Exception:
46
+ pass
47
+
48
+ async def start(self) -> None:
49
+ if not self.enabled:
50
+ return
51
+ self._tasks.append(asyncio.create_task(self._publish_loop()))
52
+ self._tasks.append(asyncio.create_task(self._listen_loop()))
53
+
54
+ async def _publish_loop(self) -> None:
55
+ while True:
56
+ payload = await self._queue.get()
57
+ try:
58
+ await self._client.publish(_CHANNEL, json.dumps(payload))
59
+ except Exception:
60
+ pass
61
+
62
+ async def _listen_loop(self) -> None:
63
+ try:
64
+ self._pubsub = self._client.pubsub()
65
+ await self._pubsub.subscribe(_CHANNEL)
66
+ async for message in self._pubsub.listen():
67
+ if message.get("type") != "message":
68
+ continue
69
+ try:
70
+ event = json.loads(message["data"])
71
+ except (TypeError, ValueError, KeyError):
72
+ continue
73
+ if event.get("node_id") == self.node_id:
74
+ # Our own message comes back from the channel: it must not be
75
+ # re-applied (infinite loop otherwise).
76
+ continue
77
+ callback = self.on_remote
78
+ if callback is not None:
79
+ await callback(event)
80
+ except asyncio.CancelledError:
81
+ raise
82
+ except Exception:
83
+ pass
84
+
85
+ async def close(self) -> None:
86
+ for task in self._tasks:
87
+ task.cancel()
88
+ if self._tasks:
89
+ await asyncio.gather(*self._tasks, return_exceptions=True)
90
+ self._tasks.clear()
91
+ if self._client is not None:
92
+ await self._client.aclose()
93
+ self._client = None