diff --git a/README.md b/README.md index 6181554..332e47c 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,9 @@ with ConfigClient("localhost:9090", subject="myapp") as client: print(f"Fee changed: {old} -> {new}") ``` +> **Fork safety:** gRPC channels are not fork-safe. Create `ConfigClient` (and start any watcher) +> *after* forking — not before. See [Fork safety](sdk/docs/watching.md#fork-safety) for details. + ## Async ```python diff --git a/sdk/docs/watching.md b/sdk/docs/watching.md index d735c2e..6541ab7 100644 --- a/sdk/docs/watching.md +++ b/sdk/docs/watching.md @@ -120,6 +120,41 @@ with ConfigClient("localhost:9090", subject="myapp") as client: print(fee_a.value, fee_b.value) ``` +## Fork safety + +gRPC channels are **not fork-safe**. Do not create a `ConfigClient` (or `AsyncConfigClient`) before +calling `os.fork()` — this includes implicit forks from `multiprocessing.Pool`, Gunicorn workers, +and similar process-spawning frameworks. + +After a fork, the child inherits the open gRPC channel. The channel's internal threads and file +descriptors are in an undefined state, which can cause hangs, crashes, or silent data corruption. + +**Fix: create the client inside the worker, not before forking.** + +```python +from multiprocessing import Pool +from opendecree import ConfigClient + +def worker(tenant_id: str) -> str: + # Safe — client created after fork + with ConfigClient("localhost:9090", subject="myapp") as client: + return client.get(tenant_id, "payments.fee") + +with Pool(4) as pool: + results = pool.map(worker, ["tenant-a", "tenant-b"]) +``` + +If you must use `multiprocessing`, prefer the `spawn` start method (default on macOS and Windows) +over `fork` — it avoids inheriting the parent's file descriptors entirely: + +```python +import multiprocessing +multiprocessing.set_start_method("spawn") +``` + +The same restriction applies to `ConfigWatcher`: the background thread does not survive a fork. +Stop the watcher before forking, or start it inside the child process. + ## Next steps - [Async Usage](async.md) — async watcher with `async for` iteration