gisolate 0.2.14__tar.gz → 0.2.16.dev0__tar.gz
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.
- gisolate-0.2.16.dev0/.code-review-graph/.gitignore +3 -0
- gisolate-0.2.16.dev0/.code-review-graph/graph.db +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/PKG-INFO +61 -1
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/README.md +60 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/pyproject.toml +1 -1
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/src/gisolate/__init__.py +5 -1
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/src/gisolate/_internal.py +16 -2
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/src/gisolate/proxy.py +6 -6
- gisolate-0.2.16.dev0/src/gisolate/pubsub.py +425 -0
- gisolate-0.2.16.dev0/tests/test_pubsub.py +644 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/uv.lock +1 -1
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/.envrc +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/.gitignore +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/CLAUDE.md +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/LICENSE +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/lefthook.yml +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/src/gisolate/_workers.py +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/src/gisolate/bridge.py +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/src/gisolate/hub.py +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/src/gisolate/local.py +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/src/gisolate/subprocess.py +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/tests/__init__.py +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/tests/bdd/__init__.py +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/tests/bdd/features/process_proxy.feature +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/tests/bdd/features/serialization.feature +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/tests/bdd/features/subprocess_run.feature +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/tests/bdd/features/thread_local.feature +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/tests/bdd/test_proxy.py +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/tests/bdd/test_serialization.py +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/tests/bdd/test_subprocess.py +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/tests/bdd/test_thread_local.py +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/tests/conftest.py +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/tests/helpers.py +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/tests/test_bridge.py +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/tests/test_hub.py +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/tests/test_internal.py +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/tests/test_local.py +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/tests/test_proxy.py +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/tests/test_subprocess.py +0 -0
- {gisolate-0.2.14 → gisolate-0.2.16.dev0}/tests/test_workers.py +0 -0
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gisolate
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.16.dev0
|
|
4
4
|
Summary: Process isolation for gevent applications — run any object in a clean subprocess, call methods transparently via ZMQ IPC.
|
|
5
5
|
Project-URL: Repository, https://github.com/wy-z/gisolate
|
|
6
6
|
License-Expression: MIT
|
|
@@ -105,6 +105,47 @@ asyncio.run(main())
|
|
|
105
105
|
server.close()
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
+
### ProcessPublisher / ProcessSubscriber — one-way fan-out
|
|
109
|
+
|
|
110
|
+
ZMQ PUB/SUB for one-way data streaming (snapshots, signals, heartbeats). Use this when message loss is acceptable; use `ProcessBridge` when you need request/response with delivery guarantees.
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
# Producer (gevent side)
|
|
114
|
+
from gisolate import ProcessPublisher
|
|
115
|
+
|
|
116
|
+
pub = ProcessPublisher("ipc:///tmp/stream.sock").start()
|
|
117
|
+
pub.publish("v1.snapshot.AAPL", {"price": 150.0})
|
|
118
|
+
pub.publish("v1.heartbeat.gevent", {"ts_ns": 1234567890})
|
|
119
|
+
pub.close()
|
|
120
|
+
|
|
121
|
+
# Consumer (asyncio side)
|
|
122
|
+
import asyncio
|
|
123
|
+
from gisolate import ProcessSubscriber
|
|
124
|
+
|
|
125
|
+
async def main():
|
|
126
|
+
sub = ProcessSubscriber("ipc:///tmp/stream.sock")
|
|
127
|
+
|
|
128
|
+
async def on_snapshot(topic, payload):
|
|
129
|
+
print(topic, payload)
|
|
130
|
+
|
|
131
|
+
async def on_heartbeat(topic, payload):
|
|
132
|
+
print("heartbeat", payload)
|
|
133
|
+
|
|
134
|
+
sub.subscribe("v1.snapshot.", on_snapshot)
|
|
135
|
+
sub.subscribe("v1.heartbeat.", on_heartbeat)
|
|
136
|
+
sub.start()
|
|
137
|
+
await asyncio.sleep(10)
|
|
138
|
+
await sub.close()
|
|
139
|
+
|
|
140
|
+
asyncio.run(main())
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Notes:
|
|
144
|
+
- **Topic prefix matching** — `sub.subscribe("v1.snapshot.", h)` receives every topic starting with that prefix.
|
|
145
|
+
- **Multiple handlers per prefix** — invoked concurrently with `asyncio.gather`. Exceptions in one handler do not kill the reader task.
|
|
146
|
+
- **Lossy by design** — `publish` is non-blocking; messages are dropped when the send queue is full (slow subscriber). Set `sndhwm=` to tune.
|
|
147
|
+
- **Pluggable serializer** — defaults to `SmartPickle`. Pass any object implementing the `Serializer` protocol (`dumps`/`loads`) to use msgpack, JSON, etc.
|
|
148
|
+
|
|
108
149
|
### ThreadLocalProxy — per-thread instances
|
|
109
150
|
|
|
110
151
|
Thread-local proxy using unpatched `threading.local` for true isolation in `gevent.threadpool`:
|
|
@@ -153,6 +194,25 @@ Run a function in an isolated subprocess. Blocks with gevent-safe polling.
|
|
|
153
194
|
- **`await bridge.call(func, *args, timeout=60, **kwargs)`** — async RPC call (client mode)
|
|
154
195
|
- **`bridge.close()`** — cleanup resources
|
|
155
196
|
|
|
197
|
+
### `ProcessPublisher(address, *, serializer=SmartPickle, sndhwm=1000)`
|
|
198
|
+
|
|
199
|
+
- **`pub.start()`** — bind the PUB socket (idempotent, returns self)
|
|
200
|
+
- **`pub.publish(topic, payload)`** — non-blocking publish; drops on slow consumers
|
|
201
|
+
- **`pub.close()`** — cleanup (idempotent)
|
|
202
|
+
- Supports context manager (`with` statement)
|
|
203
|
+
|
|
204
|
+
### `ProcessSubscriber(address, *, serializer=SmartPickle)`
|
|
205
|
+
|
|
206
|
+
- **`sub.subscribe(topic_prefix, handler)`** — register an async handler for a topic prefix
|
|
207
|
+
- **`sub.unsubscribe(topic_prefix, handler=None)`** — remove a handler or all handlers for a prefix
|
|
208
|
+
- **`sub.start()`** — connect and spawn the reader task (idempotent, returns self)
|
|
209
|
+
- **`await sub.close()`** — cancel reader and cleanup (idempotent)
|
|
210
|
+
- Supports async context manager (`async with` statement)
|
|
211
|
+
|
|
212
|
+
### `Serializer` (Protocol)
|
|
213
|
+
|
|
214
|
+
Anything with `dumps(obj) -> bytes` and `loads(bytes) -> obj` static methods can be used as a serializer for `ProcessPublisher` / `ProcessSubscriber`. Default is `SmartPickle` (pickle, falling back to dill).
|
|
215
|
+
|
|
156
216
|
### `ThreadLocalProxy(factory)`
|
|
157
217
|
|
|
158
218
|
Transparent proxy delegating attribute access to a per-thread instance.
|
|
@@ -84,6 +84,47 @@ asyncio.run(main())
|
|
|
84
84
|
server.close()
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
### ProcessPublisher / ProcessSubscriber — one-way fan-out
|
|
88
|
+
|
|
89
|
+
ZMQ PUB/SUB for one-way data streaming (snapshots, signals, heartbeats). Use this when message loss is acceptable; use `ProcessBridge` when you need request/response with delivery guarantees.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# Producer (gevent side)
|
|
93
|
+
from gisolate import ProcessPublisher
|
|
94
|
+
|
|
95
|
+
pub = ProcessPublisher("ipc:///tmp/stream.sock").start()
|
|
96
|
+
pub.publish("v1.snapshot.AAPL", {"price": 150.0})
|
|
97
|
+
pub.publish("v1.heartbeat.gevent", {"ts_ns": 1234567890})
|
|
98
|
+
pub.close()
|
|
99
|
+
|
|
100
|
+
# Consumer (asyncio side)
|
|
101
|
+
import asyncio
|
|
102
|
+
from gisolate import ProcessSubscriber
|
|
103
|
+
|
|
104
|
+
async def main():
|
|
105
|
+
sub = ProcessSubscriber("ipc:///tmp/stream.sock")
|
|
106
|
+
|
|
107
|
+
async def on_snapshot(topic, payload):
|
|
108
|
+
print(topic, payload)
|
|
109
|
+
|
|
110
|
+
async def on_heartbeat(topic, payload):
|
|
111
|
+
print("heartbeat", payload)
|
|
112
|
+
|
|
113
|
+
sub.subscribe("v1.snapshot.", on_snapshot)
|
|
114
|
+
sub.subscribe("v1.heartbeat.", on_heartbeat)
|
|
115
|
+
sub.start()
|
|
116
|
+
await asyncio.sleep(10)
|
|
117
|
+
await sub.close()
|
|
118
|
+
|
|
119
|
+
asyncio.run(main())
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Notes:
|
|
123
|
+
- **Topic prefix matching** — `sub.subscribe("v1.snapshot.", h)` receives every topic starting with that prefix.
|
|
124
|
+
- **Multiple handlers per prefix** — invoked concurrently with `asyncio.gather`. Exceptions in one handler do not kill the reader task.
|
|
125
|
+
- **Lossy by design** — `publish` is non-blocking; messages are dropped when the send queue is full (slow subscriber). Set `sndhwm=` to tune.
|
|
126
|
+
- **Pluggable serializer** — defaults to `SmartPickle`. Pass any object implementing the `Serializer` protocol (`dumps`/`loads`) to use msgpack, JSON, etc.
|
|
127
|
+
|
|
87
128
|
### ThreadLocalProxy — per-thread instances
|
|
88
129
|
|
|
89
130
|
Thread-local proxy using unpatched `threading.local` for true isolation in `gevent.threadpool`:
|
|
@@ -132,6 +173,25 @@ Run a function in an isolated subprocess. Blocks with gevent-safe polling.
|
|
|
132
173
|
- **`await bridge.call(func, *args, timeout=60, **kwargs)`** — async RPC call (client mode)
|
|
133
174
|
- **`bridge.close()`** — cleanup resources
|
|
134
175
|
|
|
176
|
+
### `ProcessPublisher(address, *, serializer=SmartPickle, sndhwm=1000)`
|
|
177
|
+
|
|
178
|
+
- **`pub.start()`** — bind the PUB socket (idempotent, returns self)
|
|
179
|
+
- **`pub.publish(topic, payload)`** — non-blocking publish; drops on slow consumers
|
|
180
|
+
- **`pub.close()`** — cleanup (idempotent)
|
|
181
|
+
- Supports context manager (`with` statement)
|
|
182
|
+
|
|
183
|
+
### `ProcessSubscriber(address, *, serializer=SmartPickle)`
|
|
184
|
+
|
|
185
|
+
- **`sub.subscribe(topic_prefix, handler)`** — register an async handler for a topic prefix
|
|
186
|
+
- **`sub.unsubscribe(topic_prefix, handler=None)`** — remove a handler or all handlers for a prefix
|
|
187
|
+
- **`sub.start()`** — connect and spawn the reader task (idempotent, returns self)
|
|
188
|
+
- **`await sub.close()`** — cancel reader and cleanup (idempotent)
|
|
189
|
+
- Supports async context manager (`async with` statement)
|
|
190
|
+
|
|
191
|
+
### `Serializer` (Protocol)
|
|
192
|
+
|
|
193
|
+
Anything with `dumps(obj) -> bytes` and `loads(bytes) -> obj` static methods can be used as a serializer for `ProcessPublisher` / `ProcessSubscriber`. Default is `SmartPickle` (pickle, falling back to dill).
|
|
194
|
+
|
|
135
195
|
### `ThreadLocalProxy(factory)`
|
|
136
196
|
|
|
137
197
|
Transparent proxy delegating attribute access to a per-thread instance.
|
|
@@ -4,11 +4,12 @@ Run any object in a clean subprocess, call methods transparently via ZMQ IPC.
|
|
|
4
4
|
Isolates libraries incompatible with gevent monkey-patching.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from ._internal import ProcessError, RemoteError
|
|
7
|
+
from ._internal import ProcessError, RemoteError, Serializer
|
|
8
8
|
from .bridge import ProcessBridge
|
|
9
9
|
from .hub import ensure_hub_started, shutdown as shutdown_hub, spawn_on_main_hub
|
|
10
10
|
from .local import ThreadLocalProxy
|
|
11
11
|
from .proxy import ProcessProxy, get_default_mp_context, set_default_mp_context
|
|
12
|
+
from .pubsub import ProcessPublisher, ProcessSubscriber
|
|
12
13
|
from .subprocess import run_in_subprocess
|
|
13
14
|
|
|
14
15
|
# Pre-initialize threadpoolctl on main thread to cache library info.
|
|
@@ -24,7 +25,10 @@ __all__ = [
|
|
|
24
25
|
"ProcessBridge",
|
|
25
26
|
"ProcessError",
|
|
26
27
|
"ProcessProxy",
|
|
28
|
+
"ProcessPublisher",
|
|
29
|
+
"ProcessSubscriber",
|
|
27
30
|
"RemoteError",
|
|
31
|
+
"Serializer",
|
|
28
32
|
"ThreadLocalProxy",
|
|
29
33
|
"ensure_hub_started",
|
|
30
34
|
"get_default_mp_context",
|
|
@@ -4,7 +4,7 @@ import contextlib
|
|
|
4
4
|
import io
|
|
5
5
|
import logging
|
|
6
6
|
import pickle
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any, Protocol, runtime_checkable
|
|
8
8
|
|
|
9
9
|
import dill
|
|
10
10
|
import gevent.monkey
|
|
@@ -44,8 +44,22 @@ class RemoteError(RuntimeError):
|
|
|
44
44
|
# ---------------------------------------------------------------------------
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
@runtime_checkable
|
|
48
|
+
class Serializer(Protocol):
|
|
49
|
+
"""Pluggable serializer protocol: ``dumps(obj) -> bytes`` / ``loads(bytes) -> obj``."""
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def dumps(obj: Any) -> bytes: ...
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def loads(data: bytes) -> Any: ...
|
|
56
|
+
|
|
57
|
+
|
|
47
58
|
class SmartPickle:
|
|
48
|
-
"""Serializer preferring pickle, falling back to dill. Learns from failures.
|
|
59
|
+
"""Serializer preferring pickle, falling back to dill. Learns from failures.
|
|
60
|
+
|
|
61
|
+
Implements the :class:`Serializer` protocol.
|
|
62
|
+
"""
|
|
49
63
|
|
|
50
64
|
_PICKLE = b"P"
|
|
51
65
|
_DILL = b"D"
|
|
@@ -73,14 +73,14 @@ class ProcessProxy(abc.ABC):
|
|
|
73
73
|
- shutdown(): thread-safe (marshals to main hub if needed)
|
|
74
74
|
"""
|
|
75
75
|
|
|
76
|
-
AUTO_RESTART_THRESHOLD = 6
|
|
77
|
-
RESTART_COOLDOWN = 6.0
|
|
78
|
-
ALIVE_CHECK_INTERVAL = 10
|
|
79
76
|
mp_context: Any = None
|
|
80
77
|
patch_kwargs: dict | None = None
|
|
81
78
|
timeout: float = 24
|
|
82
79
|
max_concurrency: int | None = None
|
|
83
80
|
daemon: bool = True
|
|
81
|
+
auto_restart_threshold: int = 6
|
|
82
|
+
restart_cooldown: float = 6.0
|
|
83
|
+
alive_check_interval: int = 10
|
|
84
84
|
|
|
85
85
|
@staticmethod
|
|
86
86
|
@abc.abstractmethod
|
|
@@ -215,7 +215,7 @@ class ProcessProxy(abc.ABC):
|
|
|
215
215
|
return hub.run_on_main_hub(self.restart_process)
|
|
216
216
|
with self._lock:
|
|
217
217
|
now = time.monotonic()
|
|
218
|
-
if self._is_alive() and now - self._last_restart < self.
|
|
218
|
+
if self._is_alive() and now - self._last_restart < self.restart_cooldown:
|
|
219
219
|
log.warning("Restart skipped (cooldown)")
|
|
220
220
|
return
|
|
221
221
|
self._last_restart = now
|
|
@@ -260,7 +260,7 @@ class ProcessProxy(abc.ABC):
|
|
|
260
260
|
idle_cycles = 0
|
|
261
261
|
else:
|
|
262
262
|
idle_cycles += 1
|
|
263
|
-
if idle_cycles >= self.
|
|
263
|
+
if idle_cycles >= self.alive_check_interval and not self._is_alive():
|
|
264
264
|
break
|
|
265
265
|
if not self._shutdown:
|
|
266
266
|
log.warning("Child process died, stopping reader")
|
|
@@ -334,7 +334,7 @@ class ProcessProxy(abc.ABC):
|
|
|
334
334
|
except ProcessError:
|
|
335
335
|
with self._lock:
|
|
336
336
|
self._error_count += 1
|
|
337
|
-
should_restart = self._error_count >= self.
|
|
337
|
+
should_restart = self._error_count >= self.auto_restart_threshold
|
|
338
338
|
if should_restart:
|
|
339
339
|
log.warning(f"{self._error_count} consecutive errors, restarting")
|
|
340
340
|
self.restart_process()
|