{"id":50924736,"url":"https://github.com/wy-z/gisolate","last_synced_at":"2026-06-16T21:30:32.089Z","repository":{"id":342545073,"uuid":"1174276498","full_name":"wy-z/gisolate","owner":"wy-z","description":"Process isolation for gevent applications — run any object in a clean subprocess, call methods transparently via ZMQ IPC.","archived":false,"fork":false,"pushed_at":"2026-05-20T04:19:12.000Z","size":117,"stargazers_count":160,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-20T07:55:23.501Z","etag":null,"topics":["gevent","ipc","zeromq","zmq"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wy-z.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-06T09:02:31.000Z","updated_at":"2026-05-20T04:19:17.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/wy-z/gisolate","commit_stats":null,"previous_names":["wy-z/gisolate"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/wy-z/gisolate","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wy-z%2Fgisolate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wy-z%2Fgisolate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wy-z%2Fgisolate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wy-z%2Fgisolate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wy-z","download_url":"https://codeload.github.com/wy-z/gisolate/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wy-z%2Fgisolate/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34425020,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-16T02:00:06.860Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["gevent","ipc","zeromq","zmq"],"created_at":"2026-06-16T21:30:29.781Z","updated_at":"2026-06-16T21:30:32.084Z","avatar_url":"https://github.com/wy-z.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# gisolate\n\n\u003e *Gevent has tormented me a thousand times, yet I keep coming back for more. This library is proof of that love.*\n\nProcess isolation for gevent applications. Run any object in a clean subprocess, call its methods transparently via ZMQ IPC.\n\n## Why\n\ngevent's `monkey.patch_all()` replaces stdlib modules globally. Some libraries (database drivers, native async frameworks, etc.) break under monkey-patching. **gisolate** spawns a clean child process — no monkey-patching — and proxies method calls over ZMQ, so incompatible code runs in isolation while your gevent app stays cooperative.\n\n## Install\n\n```bash\npip install gisolate\n```\n\nRequires Python 3.12+.\n\n## Quick Start\n\n### ProcessProxy — persistent child process\n\nProxy method calls to an object living in an isolated subprocess:\n\n```python\nimport gevent.monkey\ngevent.monkey.patch_all()\n\nfrom gisolate import ProcessProxy\n\n# Define a factory (must be importable / picklable)\ndef create_client():\n    from some_native_lib import Client\n    return Client(host=\"localhost\")\n\n# Option 1: inline\nproxy = ProcessProxy.create(create_client, timeout=30)\nresult = proxy.query(\"SELECT 1\")  # runs in child process\nproxy.shutdown()\n\n# Option 2: subclass\nclass ClientProxy(ProcessProxy):\n    client_factory = staticmethod(create_client)\n    timeout = 30\n\nwith ClientProxy() as proxy:\n    result = proxy.query(\"SELECT 1\")\n```\n\n### run_in_subprocess — one-shot call\n\nRun a single function in a subprocess and get the result:\n\n```python\nfrom gisolate import run_in_subprocess\n\ndef heavy_compute(n):\n    return sum(range(n))\n\nresult = run_in_subprocess(heavy_compute, args=(10_000_000,), timeout=60)\n```\n\n### ProcessBridge — cross-process RPC\n\nZMQ-based RPC bridge for server/client architectures. Server side uses gevent, client side uses asyncio:\n\n```python\nfrom gisolate import ProcessBridge\n\n# Server (gevent side)\nserver = ProcessBridge(\"ipc:///tmp/rpc.sock\", mode=ProcessBridge.Mode.SERVER)\nserver.start()\n\n# Client (asyncio side)\nimport asyncio\n\nasync def main():\n    client = ProcessBridge(\"ipc:///tmp/rpc.sock\", mode=ProcessBridge.Mode.CLIENT)\n    result = await client.call(lambda x, y: x + y, 3, 4, timeout=5)\n    print(result)  # 7\n    client.close()\n\nasyncio.run(main())\nserver.close()\n```\n\n### ProcessPublisher / ProcessSubscriber — one-way fan-out\n\nZMQ 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.\n\nBoth ends take a `runtime=` kwarg (a `PubSubRuntime` enum, also accepts the strings `\"gevent\"` / `\"asyncio\"`) selecting the concurrency backend:\n\n| Class | Default runtime | `publish` / `close` |\n|-------|-----------------|---------------------|\n| `ProcessPublisher` | `PubSubRuntime.GEVENT` | sync in GEVENT, awaitable in ASYNC |\n| `ProcessSubscriber` | `PubSubRuntime.ASYNC` | `close` sync in GEVENT, awaitable in ASYNC; handlers must be sync in GEVENT and `async def` in ASYNC |\n\nThe wire format is identical across runtimes, so a gevent publisher pairs with an asyncio subscriber (and vice versa) without any adapter.\n\n```python\n# Producer (gevent side — default runtime)\nfrom gisolate import ProcessPublisher\n\npub = ProcessPublisher(\"ipc:///tmp/stream.sock\").start()\npub.publish(\"v1.snapshot.AAPL\", {\"price\": 150.0})\npub.publish(\"v1.heartbeat.gevent\", {\"ts_ns\": 1234567890})\npub.close()\n\n# Consumer (asyncio side — default runtime)\nimport asyncio\nfrom gisolate import ProcessSubscriber\n\nasync def main():\n    sub = ProcessSubscriber(\"ipc:///tmp/stream.sock\")\n\n    async def on_snapshot(topic, payload):\n        print(topic, payload)\n\n    async def on_heartbeat(topic, payload):\n        print(\"heartbeat\", payload)\n\n    sub.subscribe(\"v1.snapshot.\", on_snapshot)\n    sub.subscribe(\"v1.heartbeat.\", on_heartbeat)\n    sub.start()\n    await asyncio.sleep(10)\n    await sub.close()\n\nasyncio.run(main())\n```\n\nAsyncio publisher / gevent subscriber — same wire format, just flip the `runtime=`:\n\n```python\n# Producer (asyncio side)\nfrom gisolate import ProcessPublisher, PubSubRuntime\n\nasync def producer():\n    async with ProcessPublisher(addr, runtime=PubSubRuntime.ASYNC) as pub:\n        await pub.publish(\"v1.tick.AAPL\", {\"price\": 150.0})\n\n# Consumer (gevent side) — handlers are sync\nfrom gisolate import ProcessSubscriber, PubSubRuntime\n\ndef on_tick(topic, payload):  # sync, not async def\n    print(topic, payload)\n\nwith ProcessSubscriber(addr, runtime=PubSubRuntime.GEVENT) as sub:\n    sub.subscribe(\"v1.tick.\", on_tick)\n    gevent.sleep(10)\n```\n\nNotes:\n- **Runtime must match the host loop** — `start()` requires a running asyncio loop in ASYNC mode and a greenlet context in GEVENT mode. Subsequent `subscribe` / `unsubscribe` / `publish` / `close` calls must stay on that same loop/hub; ZMQ sockets are not thread-safe.\n- **Handler signature follows the subscriber's runtime, not the publisher's** — a gevent subscriber consuming from an asyncio publisher still uses sync handlers.\n- **Context managers** — `with` for GEVENT, `async with` for ASYNC; using the wrong form raises `RuntimeError`. `start()` and `close()` are idempotent.\n- **Topic prefix matching** — `sub.subscribe(\"v1.snapshot.\", h)` receives every topic starting with that prefix. Multiple handlers may share a prefix; in ASYNC mode they run via `asyncio.gather`, in GEVENT mode each is spawned in its own greenlet. An exception in one handler is logged and does not kill the reader.\n- **`close()` from inside a handler is safe** — the reader is not joined in that case (would self-deadlock); sibling handlers in the current dispatch are allowed to finish.\n- **Lossy by design** — `publish` is non-blocking; messages are dropped when the send queue is full (slow subscriber). Tune via `sndhwm=` on the publisher.\n- **Late joiners miss history** — PUB/SUB has no replay; a subscriber that connects after a message was published will not see it. Treat published state as a stream, not a store.\n- **IPC cleanup** — `close()` unlinks the socket file for `ipc://` addresses on the publisher side. Relying on `__del__` is best-effort only; call `close()` (or use a context manager) for deterministic teardown.\n- **Pluggable serializer** — defaults to `SmartPickle` (pickle, falling back to dill). Pass any object implementing the `Serializer` protocol (`dumps`/`loads`) to use msgpack, JSON, etc. Publisher and subscriber must agree.\n\n### ThreadLocalProxy — per-thread instances\n\nThread-local proxy using unpatched `threading.local` for true isolation in `gevent.threadpool`:\n\n```python\nfrom gisolate import ThreadLocalProxy\n\nproxy = ThreadLocalProxy(create_client)\nproxy.query(\"SELECT 1\")  # each real OS thread gets its own instance\n```\n\n## Child Process Modes\n\n| `patch_kwargs`  | Child process runtime |\n|-----------------|----------------------|\n| `None` (default) | asyncio event loop   |\n| `dict`          | gevent with `patch_all(**patch_kwargs)` |\n\n```python\n# Child uses asyncio (default)\nproxy = ProcessProxy.create(factory)\n\n# Child uses gevent with selective patching\nproxy = ProcessProxy.create(factory, patch_kwargs={\"thread\": False, \"os\": False})\n```\n\n## API Reference\n\n### `ProcessProxy`\n\n- **`ProcessProxy.create(factory, *, timeout=24, mp_context=None, patch_kwargs=None)`** — create a proxy without subclassing\n- **`proxy.\u003cmethod\u003e(*args, **kwargs)`** — transparently call any method on the remote object\n- **`proxy.restart_process()`** — kill and restart child process\n- **`proxy.shutdown()`** — gracefully stop child process\n- Supports context manager (`with` statement)\n- Thread-safe: usable from greenlets and native threads\n\n### `run_in_subprocess(target, args=(), kwargs=None, *, timeout=3600, mp_context=None)`\n\nRun a function in an isolated subprocess. Blocks with gevent-safe polling.\n\n### `ProcessBridge(address, mode)`\n\n- **`bridge.start()`** — start the bridge (idempotent, returns self)\n- **`bridge.address`** — IPC address\n- **`await bridge.call(func, *args, timeout=60, **kwargs)`** — async RPC call (client mode)\n- **`bridge.close()`** — cleanup resources\n\n### `ProcessPublisher(address, *, runtime=PubSubRuntime.GEVENT, serializer=SmartPickle, sndhwm=1000)`\n\n- **`pub.start()`** — bind the PUB socket (idempotent, returns self). In ASYNC mode requires a running asyncio loop.\n- **`pub.publish(topic, payload)`** — non-blocking publish; drops on slow consumers. Returns `None` in GEVENT mode, a coroutine in ASYNC mode (must `await`).\n- **`pub.close()`** — cleanup (idempotent). Returns `None` in GEVENT mode, a coroutine in ASYNC mode.\n- **`pub.address`** / **`pub.runtime`** — read-only properties.\n- Context manager: `with` for GEVENT, `async with` for ASYNC. Using the wrong form raises `RuntimeError`.\n\n### `ProcessSubscriber(address, *, runtime=PubSubRuntime.ASYNC, serializer=SmartPickle)`\n\n- **`sub.subscribe(topic_prefix, handler)`** — register a handler for a topic prefix. Handler must be sync (`def`) in GEVENT mode and `async def` (or returning an awaitable) in ASYNC mode. Safe to call before or after `start()`.\n- **`sub.unsubscribe(topic_prefix, handler=None)`** — remove a specific handler or all handlers for a prefix. When the last handler is removed, the ZMQ-level subscription is dropped.\n- **`sub.start()`** — connect and spawn the reader (idempotent, returns self). In ASYNC mode requires a running asyncio loop; in GEVENT mode must be called from a greenlet context.\n- **`sub.close()`** — tear down the socket and join the reader (idempotent). Returns `None` in GEVENT mode, a coroutine in ASYNC mode. Safe to call from inside a handler — the reader is not joined in that case to avoid self-deadlock.\n- **`sub.address`** / **`sub.runtime`** — read-only properties.\n- Context manager: `with` for GEVENT, `async with` for ASYNC.\n\n### `PubSubRuntime` (StrEnum)\n\n- **`PubSubRuntime.GEVENT`** (`\"gevent\"`) — bind to the gevent hub; sync APIs and sync handlers.\n- **`PubSubRuntime.ASYNC`** (`\"asyncio\"`) — bind to the running asyncio loop; awaitable APIs and async handlers.\n\n### `Serializer` (Protocol)\n\nAnything with `dumps(obj) -\u003e bytes` and `loads(bytes) -\u003e obj` static methods can be used as a serializer for `ProcessPublisher` / `ProcessSubscriber`. Default is `SmartPickle` (pickle, falling back to dill). Publisher and subscriber must agree on the serializer.\n\n### `ThreadLocalProxy(factory)`\n\nTransparent proxy delegating attribute access to a per-thread instance.\n\n### `ensure_hub_started()`\n\nPre-start the internal gevent hub loop on demand. Idempotent and thread-safe. Called automatically by `ProcessProxy`, but can be invoked explicitly to control initialization timing.\n\n### `spawn_on_main_hub(func, *args, **kwargs)`\n\nSchedule a function on the main gevent hub without waiting. Thread-safe, fire-and-forget.\n\n### `ProcessError`\n\nRaised when a child process dies or communication fails.\n\n### `RemoteError`\n\nWrapper for exceptions from the child process that can't be pickled. Preserves the original exception type name and message.\n\n### `shutdown_hub()`\n\nExplicitly stop the internal gevent hub loop. Registered via `atexit` automatically.\n\n### `set_default_mp_context(ctx)` / `get_default_mp_context()`\n\nConfigure the default `multiprocessing` context for all proxies (default: `\"spawn\"`).\n\n## Note on `multiprocessing` and `__main__`\n\n`multiprocessing` spawn/forkserver children re-import the caller's `__main__` module. If your `main.py` has top-level side effects (e.g. `gevent.monkey.patch_all()`), these will re-execute in the child — causing double-patching warnings or import errors.\n\n**Best practice**: guard monkey-patching behind `__name__` and defer heavy imports:\n\n```python\n# main.py\nif __name__ == \"__main__\":\n    import gevent.monkey\n    gevent.monkey.patch_all()\n\n    import my_app\n    my_app.run()\n```\n\nSpawn children re-import `main.py` but skip the `__name__` block, avoiding side effects.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwy-z%2Fgisolate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwy-z%2Fgisolate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwy-z%2Fgisolate/lists"}