moxzi

Ray

This host runs the actor-host contract, version 2, on top of a named Ray actor per key (@ray.remote(max_concurrency=1), name moxzi/Type/id): Ray's own actor mailbox semantics — one method call in flight at a time on a synchronous actor — are the whole one-step-at-a- time guarantee, no lock of this host's own. It is a faithful Python port of the shared Go host kit (moxzi/adapters/hostkit-go/hostkit.go) that the Proto.Actor reference host was extracted from, for Ray + Postgres. The code is moxzi/adapters/ray (moxzi-ray-host, Python, Ray 2.58), built by moxzi/conformance/hosts/ray.json and driven by HOST=ray scripts/host_matrix_gate.sh.

What Ray owns, what Motoko owns#

OwnsWhoWhat
Identity / one-step-at-a-time per keyRay, a named actor per keyKeyActor, max_concurrency=1, name moxzi/{key}; Ray's own mailbox is the serialisation mechanism
Routingthis process, outside any Ray actorevery parked await becomes a durable route row before it is routed (a _routes table in Postgres); a restart re-drives every non-held route
Timersthis processa _timers table polled every 200ms; a claimed row (DELETE … RETURNING) fires exactly once
Placement / held / pending callsthis process, in Postgres_placement, _held tables behind PUT /_moxzi/placement and POST /_moxzi/hold
The HTTP surfacethis processserver.py on ThreadingHTTPServer — different keys' turns run concurrently in different request threads; a single key's steps still serialise one layer down, inside its Ray actor
Motoko execution, the heap, upgradesmoxzi-actor-host (Rust)everything past /v2/* — the Ray actor only proxies one JSON POST per step

Routing runs outside the Ray actor, not inside KeyActor.do_post: if a key's actor blocked on a routed call until the callee answered, the key would be held for the whole await, and two actors awaiting each other would deadlock. So the actor does one HTTP POST and returns; the shared hostkit.Host drives the resulting call chain in the HTTP front's own request thread. Ray has neither durable actor state (a KeyActor's Python object lives in RAM only) nor a durable timer of its own, so placement, held keys, the route journal and armed timers all live in Postgres, tables <ns>_placement, <ns>_held, <ns>_routes, <ns>_timers — the same schema shape as Proto.Actor's and Pekko's.

Prerequisites#

Verified on this machine:

Walk through it#

/tmp/moxzi-tutorial-ray/ is the scratch directory for this walk-through. ray_host.py starts its own private local Ray cluster (ray.init(address="local", ...)) — it never attaches to a cluster already running on the machine.

Write and build the two actors#

Same two actors as every other host page — Customer awaits Bank on chargeVia; both simplified from moxzi/conformance/fixtures/{customer,bank}.mo:

// customer.mo
import Prim "mo:⛔";
persistent actor {
  type Order = { id : Nat; total : Nat };
  var balance : Nat = 0;
  var orders : [Order] = [];
  var timerFired : Nat = 0;

  public func addOrder(order : Order) : async Nat {
    orders := Prim.Array_tabulate<Order>(orders.size() + 1, func (i : Nat) : Order {
      if (i < orders.size()) orders[i] else order
    });
    balance += order.total;
    orders.size()
  };
  public query func getBalance() : async Nat { balance };
  public query func orderCount() : async Nat { orders.size() };

  public func chargeVia(bank : Text, amount : Nat) : async Nat {
    let b : actor { debit : (Nat) -> async Nat } = actor (bank);
    let v = await b.debit(amount);
    balance += v;
    balance
  };

  public func armTimer(delayNs : Nat64) : async () {
    ignore Prim.setTimer<system>(delayNs, false, func () : async () { timerFired += 1 });
  };
  public query func timerCount() : async Nat { timerFired };
};
// bank.mo
persistent actor {
  var debited : Nat = 0;
  public func debit(n : Nat) : async Nat { debited += n; n };
  public query func total() : async Nat { debited };
};
MOXZI_COMPILER=bin/compiler-canister/compiler.wasm MOXZI_LINKER=bin/linker-canister/linker.wasm \
  moxzi/target/release/moxzi build customer.mo -o customer.wasm
# customer.mo -> customer.wasm + customer.did (272261 bytes, 1 file(s), 65 steps, 0.79s)
MOXZI_COMPILER=bin/compiler-canister/compiler.wasm MOXZI_LINKER=bin/linker-canister/linker.wasm \
  moxzi/target/release/moxzi build bank.mo -o bank.wasm

Start Postgres and the Ray host#

Pick a Postgres host port nothing else on the box is using (docker ps first):

docker run -d --name moxzi-tutorial-pg -e POSTGRES_PASSWORD=moxzi -e POSTGRES_DB=moxzi \
  -p 25901:5432 postgres:16

cd /tmp/moxzi-tutorial-ray
DATABASE_URL="postgres://postgres:moxzi@127.0.0.1:25901/moxzi?sslmode=disable" \
  MOXZI_HOST_NS=tutorial python3 moxzi/adapters/ray/ray_host.py --listen 127.0.0.1:29500 &
# ray: private local cluster up, namespace=tutorial
# moxzi-ray-host: contract moxzi-actor-host/2, listening on 127.0.0.1:29500

curl -s http://127.0.0.1:29500/healthz
# ok

Start the moxzi-actor-host services#

moxzi/target/release/moxzi-actor-host --listen 127.0.0.1:29521 --state ./state-customer \
  --namespace tutorial --deployment crm --actor Customer=./customer.wasm --known Bank/1
# moxzi-actor-host: Bank/1 is 6lkaw-mxxfa-ayudl-lyjba-qiou3-hjvqo-deil4-f3r6l-apodg-jbfpw-uae

moxzi/target/release/moxzi-actor-host --listen 127.0.0.1:29522 --state ./state-bank \
  --namespace tutorial --deployment crm --actor Bank=./bank.wasm

Point placement at the services, and call it#

curl -X PUT http://127.0.0.1:29500/_moxzi/placement -H 'content-type: application/json' \
  -d '{"types":{"Customer":["http://127.0.0.1:29521"],"Bank":["http://127.0.0.1:29522"]}}'
# HTTP 204

curl -X POST http://127.0.0.1:29500/actors/Customer/1/method/addOrder \
  -H 'content-type: application/json' -d '[{"id":1,"total":30}]'
# [1]  HTTP 200

A cross-actor await#

curl -X POST http://127.0.0.1:29500/actors/Customer/1/method/chargeVia \
  -H 'content-type: application/json' \
  -d '["6lkaw-mxxfa-ayudl-lyjba-qiou3-hjvqo-deil4-f3r6l-apodg-jbfpw-uae", 5]'
# [35]  HTTP 200

curl -X POST http://127.0.0.1:29500/actors/Bank/1/method/total -H 'content-type: application/json' -d '[]'
# [5]

Customer's KeyActor did one HTTP POST and returned; the HTTP front's own thread drove the routed call to whichever service owns Bank/1 and resumed Customer with the answer — getBalance now reads 30 + 5 = 35.

Arm a timer, and watch it fire#

curl -X POST http://127.0.0.1:29500/actors/Customer/1/method/armTimer \
  -H 'content-type: application/json' -d '[3000000000]'
sleep 6
curl -X POST http://127.0.0.1:29500/actors/Customer/1/method/timerCount -H 'content-type: application/json' -d '[]'
# [1]

Kill it and bring it back#

A restart of ray_host.py is a genuine restart of Ray, not a resume. ray.init(address= "local", ...) spawns raylet, gcs_server, the plasma store, and idle workers as children of the driver process — confirmed on this machine:

ps aux | grep -E "ray::|raylet|gcs_server"
# ray::DashboardAgent, ray::IDLE (x6), raylet, ray::KeyActor (x2: Customer, Bank), gcs_server

Killing only the driver brings every one of those down:

kill -9 <ray_host.py pid>
sleep 4
ps aux | grep -E "ray::|raylet|gcs_server"    # nothing
nc -z 127.0.0.1 29500 || echo "port free"     # port free

So a restart here has no durable Ray state to lose — every KeyActor and the whole local cluster are gone — and everything that must survive already lives in Postgres and the moxzi-actor-host services' own disks:

cd /tmp/moxzi-tutorial-ray
DATABASE_URL="postgres://postgres:moxzi@127.0.0.1:25901/moxzi?sslmode=disable" \
  MOXZI_HOST_NS=tutorial python3 moxzi/adapters/ray/ray_host.py --listen 127.0.0.1:29500 &

curl -X POST http://127.0.0.1:29500/actors/Customer/1/method/getBalance -H 'content-type: application/json' -d '[]'
# [35]  — placement and the route journal came back from Postgres, no re-PUT needed
curl -X POST http://127.0.0.1:29500/actors/Customer/1/method/timerCount -H 'content-type: application/json' -d '[]'
# [1]

Drain, then upgrade#

curl -X POST http://127.0.0.1:29500/actors/Customer/1/admin/drain -d '{}'
# {"state": "Quiescent"}
curl -X POST http://127.0.0.1:29500/actors/Customer/1/admin/upgrade \
  -H 'content-type: application/json' \
  -d "{\"deploymentId\":\"crm-v2\",\"wasm\":\"$(base64 -i customer.wasm | tr -d '\n')\"}"
# {"state": "Active"}
curl -X POST http://127.0.0.1:29500/actors/Customer/1/method/getBalance -H 'content-type: application/json' -d '[]'
# [35]

Operating notes#

These notes are for the service form. In process, the actor's image lives in Postgres and nothing is written to a service's disk — see In process below.

In process#

The same actor also runs inside Ray's own process through moxzi-rts, with Ray keeping its image: see /docs/hosts/in-process/ for the contract.

In process on Ray
Bindingctypes, inside each key's Ray actor
The actor's image lives inPostgres
Update latency vs Ray's own durable actor (p50, macOS)1.86x
ConformanceHOST=ray-inproc scripts/host_matrix_gate.sh

From moxzi/adapters/ray/ (the exact commands and environment are in moxzi/conformance/hosts/ray-inproc.json):

(cd ../../ && cargo build --release -p moxzi-rts)   # nothing else to build
DATABASE_URL=… MOXZI_IN_PROCESS=1 MOXZI_RTS_LIB=../../target/release/libmoxzi_rts.dylib \
  python3 ray_host.py --listen 127.0.0.1:8090     # libmoxzi_rts.so on Linux

The conformance matrix#

Ray's column of the spec §61 matrix, real Ray cluster, real process kills (moxzi/adapters/ray/README.md, measured 2026-09-23 against compiler b7577666):

§61 scenarioRay
PersistencePASS
Process FailurePASS
Trap RollbackPASS
Actor IsolationPASS
Actor ParallelismPASS 1.09–1.10×
Duplicate InvocationPASS
AwaitPASS
Await Across RestartPASS
Upgrade With No AwaitsPASS
Upgrade With Outstanding AwaitPASS
Permanently Unresolved AwaitPASS
Timer Across PassivationPASS
Timer Across Process FailurePASS
Tiny Heap Mutation (206 B on an 8 MB heap)PASS
ICRC-72PASS
Upgrade CompatibilityPASS
total16/16

Run twice by the lead (both 16/16, no scenario needed a SKIP): after each run, no moxzi-host-matrix-* Docker container and no ray::*/raylet/gcs_server/ray_host.py/ moxzi-actor-host process survived.

Reproduce it (needs moxzi/target/release/{moxzi,moxzi-actor-host} built, and Docker running — the gate starts its own dedicated, disposable Postgres container for this run and removes it on exit):

HOST=ray scripts/host_matrix_gate.sh
On this pageWhat Ray owns, what Motoko ownsPrerequisitesWalk through itWrite and build the two actorsStart Postgres and the Ray hostStart the moxzi-actor-host servicesPoint placement at the services, and call itA cross-actor awaitArm a timer, and watch it fireKill it and bring it backDrain, then upgradeOperating notesIn processThe conformance matrix