Skip to main content

A virtual actor framework for Python, inspired by Microsoft Orleans

Project description

PyOrleans

PyPI Python License: MIT

PyOrleans is a virtual-actor (“grain”) framework for Python, modeled after the programming model of Microsoft 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 (no GIL) where available (PEP 703).

Quick start

Install and run a minimal stateful grain in-process:

pip install pyorleans
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), 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.

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 and the 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) 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, 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

pip install pyorleans

Requires Python 3.12+. Optional extras:

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

Development

Clone github.com/antunsz/pyorleans and run tests from the repository root:

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:

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: 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)
PyOrleans (GIL) CPython with GIL, ASGI served with Granian
PyOrleans (NoGIL) tdciwasaki/python-nogil (3.14-slim-trixie): free-threaded CPython + Granian multi-thread runtime (free-threading, PEP 703)

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):

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

Or manually:

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

References

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyorleans-0.2.0.tar.gz (29.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyorleans-0.2.0-py3-none-any.whl (35.1 kB view details)

Uploaded Python 3

File details

Details for the file pyorleans-0.2.0.tar.gz.

File metadata

  • Download URL: pyorleans-0.2.0.tar.gz
  • Upload date:
  • Size: 29.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for pyorleans-0.2.0.tar.gz
Algorithm Hash digest
SHA256 7b208fb250db001de750fb8b1eb751be3040073a5044a460cbd53a3a2689ef4d
MD5 d0e40e4aa40709f679ad718c66d9a40b
BLAKE2b-256 3fa2d1847b3c0f6ce09f50b83a70b5f36b7ec309d03e96b53ca221dc5abf3209

See more details on using hashes here.

File details

Details for the file pyorleans-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: pyorleans-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 35.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for pyorleans-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 319a2055d2e9a3a8f7522ea8da3b50a98bad245590ea206c085f5ce5efd821f6
MD5 56f7cf9d812f568ac913de6c3cd5c6a0
BLAKE2b-256 ab29a1602503fa7b9af44012496e89aaac126c4590a52b05a0bc12907cb2a1dd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page