Metadata-Version: 2.4
Name: pyorleans
Version: 0.2.0
Summary: A virtual actor framework for Python, inspired by Microsoft Orleans
Project-URL: Homepage, https://github.com/antunsz/pyorleans
Project-URL: Documentation, https://github.com/antunsz/pyorleans#readme
Project-URL: Repository, https://github.com/antunsz/pyorleans
Project-URL: Issues, https://github.com/antunsz/pyorleans/issues
Project-URL: Changelog, https://github.com/antunsz/pyorleans/releases
Author-email: Carlos Antunes <c4rlo4@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: actor,asyncio,concurrency,free-threading,orleans,virtual-actor
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Classifier: Topic :: System :: Distributed Computing
Classifier: Typing :: Typed
Requires-Python: >=3.12
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Provides-Extra: redis
Requires-Dist: redis>=5.0; extra == 'redis'
Description-Content-Type: text/markdown

# PyOrleans

[![PyPI](https://img.shields.io/pypi/v/pyorleans.svg)](https://pypi.org/project/pyorleans/)
[![Python](https://img.shields.io/pypi/pyversions/pyorleans.svg)](https://pypi.org/project/pyorleans/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

**PyOrleans** is a virtual-actor (“grain”) framework for Python, modeled after the programming model of [Microsoft Orleans](https://learn.microsoft.com/en-us/dotnet/orleans/). It gives you always-addressable actors, turn-based concurrency per grain, pluggable storage with optimistic concurrency, timers, reminders, call filters, and placement hooks—so you can structure distributed-style logic in a single process (or extend toward multiple silos) without manually juggling locks for each entity.

The design goal is to bring the **Orleans grain semantics**—virtual actors, single-threaded turns, and explicit concurrency attributes—to idiomatic **async Python**, including the option to exploit [free-threaded CPython](https://docs.python.org/3/howto/free-threading-python.html) (no GIL) where available ([PEP 703](https://peps.python.org/pep-0703/)).

## Quick start

Install and run a minimal stateful grain in-process:

```bash
pip install pyorleans
```

```python
import asyncio
from dataclasses import dataclass

from pyorleans import Grain, GrainFactory, LocalMemoryStorage, Silo, stateful


@dataclass
class CounterState:
    n: int = 0


@stateful(CounterState)
class CounterGrain(Grain):
    def add(self, x: int) -> int:
        self.state.n += x
        return self.state.n


async def main() -> None:
    silo = Silo()
    silo.register_storage(LocalMemoryStorage())
    silo.register_grain_class(CounterGrain)
    await silo.start()

    factory = GrainFactory(silo)
    grain = factory.get_grain(CounterGrain, "my-counter")
    total = await grain.add(7)
    print(total)  # 7

    await silo.stop()


if __name__ == "__main__":
    asyncio.run(main())
```

For an HTTP front end (e.g. FastAPI + [Granian](https://github.com/emmett-framework/granian)), see `benchmarks/benchmark_apps/pyorleans_api/main.py` in this repository.

## Architecture (conceptual)

High-level flow for a **remote client** calling grains through HTTP (as in the benchmark). The C# path uses a real Orleans cluster client and Redis for membership; the Python path hosts `Silo` inside the same process as the ASGI app.

```mermaid
flowchart TB
    subgraph bench["Benchmark client"]
        BR["run_benchmark.py\n(aiohttp, POST /benchmark/{id})"]
    end

    subgraph csharp["Orleans C# stack (Docker)"]
        WEB["OrleansPoC.WebAPI\n:8081"]
        CLIENT["IClusterClient"]
        REDIS[("Redis\nclustering / directory")]
        SILO_CS["Orleans silo host\n(orleans-server)"]
        G_CS["Stock-style grains"]
        WEB --> CLIENT
        CLIENT <--> REDIS
        SILO_CS <--> REDIS
        CLIENT --> G_CS
    end

    subgraph pyo["PyOrleans stack (Docker)"]
        GR["Granian\n(ASGI)"]
        API["FastAPI app"]
        SILO_PY["PyOrleans Silo\n+ worker threads"]
        G_PY["StockBenchmarkGrain"]
        GR --> API --> SILO_PY --> G_PY
    end

    BR --> WEB
    BR --> GR
```

## Motivation

- **Orleans** popularized *virtual actors*: you address a grain by id and type; the runtime activates it on demand, serializes calls per activation, and manages lifecycle and persistence. See the [Orleans documentation](https://learn.microsoft.com/en-us/dotnet/orleans/) and the [dotnet/orleans](https://github.com/dotnet/orleans) repository for the original model and terminology.
- **Python** already excels at I/O-bound concurrency with `asyncio`. PyOrleans adds a **grain boundary**: one logical thread of execution per activation, explicit reentrancy and interleaving rules, and state helpers—similar in spirit to Orleans, adapted to Python’s async runtime.
- **Free-threaded Python** (GIL optional/disabled, [PEP 703](https://peps.python.org/pep-0703/)) allows CPU-bound work in multiple native threads for distinct grains; the benchmark contrasts **CPython with GIL** (3.12) against a **free-threaded** runtime in Docker ([tdciwasaki/python-nogil](https://hub.docker.com/r/tdciwasaki/python-nogil), Python 3.14) to illustrate throughput under load.

PyOrleans is **not** a wire-compatible port of Orleans; it is a **Python library** inspired by the same concepts.

## Features

- Grains (`Grain`), silo host (`Silo`), and factory (`GrainFactory`, `GrainReference`)
- Turn-style execution and concurrency attributes (`reentrant`, `read_only`, `stateless_worker`, …)
- Stateful grains with `StorageProvider`, `LocalMemoryStorage`, and ETag-style conflicts (`ETagMismatchError`)
- Timers and reminders, observers, call filters, placement strategies, in-memory directory

See package exports in `src/pyorleans/__init__.py` for the full public API.

## Installation

```bash
pip install pyorleans
```

Requires **Python 3.12+**. Optional extras:

```bash
pip install pyorleans[redis]   # Redis-related integrations when you use them
```

## Development

Clone [github.com/antunsz/pyorleans](https://github.com/antunsz/pyorleans) and run tests from the repository root:

```bash
pip install -e ".[dev]"   # requires a recent pip that supports PEP 660 editable installs
# or, without editable install:
PYTHONPATH=src python -m pytest
```

Build artifacts locally:

```bash
python -m pip install build
python -m build
```

## Benchmark (repository)

The `benchmarks/` directory is **not** part of the published wheel; it is for local experimentation.

### C# baseline and inspiration

The C# side follows the same **solution shape** as the public PoC [angelobelchior/OrleansPoC](https://github.com/angelobelchior/OrleansPoC): `Application/` with **WebAPI**, **silo server**, **contracts**, **Aspire `ServiceDefaults`**, and **Redis-backed Orleans clustering**. The HTTP workload updates a **stock-style grain** (name, value, trades, volume), matching the domain used in that PoC and mirrored in the PyOrleans FastAPI app (`StockBenchmarkGrain` in `benchmarks/benchmark_apps/pyorleans_api/main.py`). **`Dockerfile.orleans_server` and `Dockerfile.orleans_webapi` clone that repository at build time** (`git clone --depth 1`); override with build args `ORLEANS_POC_REPO` and `ORLEANS_POC_REF` (default `main`) if you use a fork or pinned branch/tag.

Orleans packages are restored from **nuget.org** inside the image (`benchmarks/docker-nuget.config`).

**Docker-only patches** (under `benchmarks/orleans_*_patch/`): the PoC normally relies on **.NET Aspire** (`OrleansPoC.AppHost`) to call `WithClustering(redis)` for both silo and client. In Compose we only set `ConnectionStrings__redis`, so the Dockerfiles overlay `Program.cs` files that call `UseRedisClustering` with that connection string. The WebAPI patch also registers `GET /` and `POST /benchmark/{id}` for `run_benchmark.py`. On the **silo** image, `UseDashboard()` is omitted: the dashboard would bind HTTP `:8080` and **collide with Kestrel** on the same port, so the silo never finished starting and the client logged “Could not find any gateway”.

### Targets compared

| Target | Role |
|--------|------|
| **Orleans C#** | Reference .NET Orleans stack ([Orleans overview](https://learn.microsoft.com/en-us/dotnet/orleans/)) |
| **PyOrleans (GIL)** | CPython with GIL, ASGI served with [Granian](https://github.com/emmett-framework/granian) |
| **PyOrleans (NoGIL)** | [tdciwasaki/python-nogil](https://hub.docker.com/r/tdciwasaki/python-nogil) (`3.14-slim-trixie`): free-threaded CPython + [Granian](https://github.com/emmett-framework/granian) multi-thread runtime ([free-threading](https://docs.python.org/3/howto/free-threading-python.html), [PEP 703](https://peps.python.org/pep-0703/)) |

The NoGIL service no longer compiles CPython from source; it uses the community image above (based on official Python-style builds with GIL disabled). To confirm in a container: `python -c "import sys; print(not sys.flags.gil)"` should print `True` (see the image README on Docker Hub).

### Methodology

The runner (`benchmarks/run_benchmark.py`) drives **HTTP POST** requests with **aiohttp**:

| Parameter | Value |
|-----------|--------|
| Concurrency | 200 workers |
| Warmup | 2 s per target |
| Measured window | 10 s per target |
| Multi-grain pool | 100 distinct grain keys (`STOCK-0000` …) |

**Scenarios**

1. **Single grain** — all requests use one id (`BENCH`): stresses **per-grain serialization** and turn throughput (no parallelism across grains).
2. **Multi grain** — rotating ids across the pool: stresses **many concurrent activations** (where free-threading and thread pools can help—subject to host limits and Docker CPU).

Results are **environment-specific** (CPU, thermal limits, Docker Desktop settings, .NET vs Python builds). Treat them as **one sample**, not a universal ranking. After changing interpreters (for example moving the NoGIL container to **Python 3.14** via `tdciwasaki/python-nogil`), **re-run** the benchmark before comparing to older tables.

### Example run (sample)

The table below is a **single local run** after `make benchmark` (all three services healthy). Re-run on your machine for comparable numbers.

**Scenario 1 — single grain (fully serialized on one activation)**

| Target | RPS | Avg ms | p50 ms | p95 ms | p99 ms |
|--------|-----|--------|--------|--------|--------|
| Orleans C# [1 grain] | 17,943.7 | 11.1 | 11.0 | 12.0 | 15.0 |
| PyOrleans (GIL) [1 grain] | 10,707.1 | 18.7 | 15.9 | 31.9 | 46.8 |
| PyOrleans (NoGIL) [1 grain] | 7,190.7 | 27.8 | 27.7 | 29.2 | 39.6 |

**Scenario 2 — multi grain (100 distinct grains)**

| Target | RPS | Avg ms | p50 ms | p95 ms | p99 ms |
|--------|-----|--------|--------|--------|--------|
| Orleans C# [100 grains] | 17,749.9 | 11.3 | 11.1 | 12.2 | 15.2 |
| PyOrleans (GIL) [100 grains] | 11,805.1 | 16.9 | 15.7 | 23.8 | 26.5 |
| PyOrleans (NoGIL) [100 grains] | 7,357.2 | 27.2 | 26.9 | 28.8 | 45.0 |

In this sample, **Orleans C#** had the highest RPS in both scenarios; **PyOrleans (GIL)** improved from single- to multi-grain (less head-of-line blocking across keys). **PyOrleans (NoGIL)** was slower here—often consistent with **extra threading / allocator / build** overhead in a small, allocation-heavy HTTP+grain path; interpret with profiling on your target hardware.

### How to run

From the repo root (Docker Compose v2):

```bash
make up        # builds and starts services in benchmarks/docker-compose.yml
make benchmark # runs the client under benchmarks/, then tears down
```

Or manually:

```bash
docker compose -f benchmarks/docker-compose.yml up -d --build
cd benchmarks && pip install aiohttp && python run_benchmark.py
```

### References

- [Microsoft Orleans docs](https://learn.microsoft.com/en-us/dotnet/orleans/), [dotnet/orleans](https://github.com/dotnet/orleans)
- [angelobelchior/OrleansPoC](https://github.com/angelobelchior/OrleansPoC) — PoC layout and stock-style grain scenario that the C# benchmark is aligned with
- [PEP 703](https://peps.python.org/pep-0703/), [Free-threading HOWTO](https://docs.python.org/3/howto/free-threading-python.html)
- [Granian](https://github.com/emmett-framework/granian)
- [tdciwasaki/python-nogil](https://hub.docker.com/r/tdciwasaki/python-nogil) (benchmark `Dockerfile.pyorleans.nogil` base image)

## License

MIT — see [`LICENSE`](LICENSE).
