Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions sdk/docs/watching.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down