You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The actor model deliberately avoids distributed transactions — they conflict with the "actor is the atomic unit" principle. But some domains genuinely need cross-actor ACID:
Bank transfer — debit account A + credit account B; either both or neither.
Inventory + Order — reserve item + create order; either both or neither.
Multi-resource workflow — book hotel + flight + car as a unit.
Single actor owns all state — collapses A + B into one actor. Limits concurrency, doesn't scale.
External transaction coordinator (e.g., a database transaction) — bypasses the actor model.
Orleans Transactions add a fourth option: opt-in 2-phase commit (2PC) across actors, with the coordinator + transaction-log built into the framework. Use only when you really need ACID; otherwise use the saga.
This is the most ambitious item in the catalog — a major addition that touches state-store contracts, message-dispatch, and supervision. Hence XL + dedicated design phase.
Reference: what Orleans does
publicinterfaceIAccountGrain:IGrainWithGuidKey{[Transaction(TransactionOption.Join)]TaskDebit(decimalamount);[Transaction(TransactionOption.Join)]TaskCredit(decimalamount);[Transaction(TransactionOption.CreateOrJoin)]Task<decimal>GetBalance();}publicclassAccountGrain:Grain,IAccountGrain{privatereadonlyITransactionalState<Balance>balance;publicTaskDebit(decimalamount){returnbalance.PerformUpdate(b =>{if(b.Value<amount)thrownewInsufficientFundsException();b.Value-=amount;});}// ... Credit similar ...}// Caller:[Transaction(TransactionOption.Create)]publicclassTransferGrain:Grain,ITransferGrain{publicasyncTaskTransfer(Guidfrom,Guidto,decimalamount){awaitaccountFrom.Debit(amount);awaitaccountTo.Credit(amount);// 2PC commit at end of this method; debit+credit either both apply or both rollback.}}
Orleans's runtime:
Generates a TransactionId on Transaction(Create).
Propagates it through every call inside the transaction scope.
Each touched grain joins the transaction; their state-store records the "tentative" change.
On scope-exit: coordinator runs Prepare phase (all participants vote), then Commit (or Abort if any voted no).
Failure recovery: coordinator can resume after crash via the transaction log.
This is a sketch of the SHAPE, not a complete design. The implementation needs its own multi-page design doc.
// src/transactions/TransactionalState.ts (new)exportinterfaceTransactionalState<T>{/** * Read the current value inside the active transaction. * Throws if no transaction is active. */read(): Promise<T>;/** * Stage an update inside the active transaction. * Throws if no transaction is active. */update(updater: (current: T)=>T): Promise<void>;}exportinterfaceTransactionScope{/** Run `fn` inside a transaction; commit on resolve, abort on reject. */run<R>(fn: ()=>Promise<R>): Promise<R>;}// Cluster-singleton coordinator:exportclassTransactionCoordinator{/** Begin a new transaction; returns a TransactionScope. */begin(options?: {readonlytimeoutMs?: number}): TransactionScope;}
Transaction-log store — durable per-cluster log of TX states (open / preparing / committed / aborted).
TX-context propagation — every tell / ask carries the active TX id (via AsyncLocalStorage + envelope field).
Per-actor TX participation — actor's state-store maintains tentative + committed versions; on Prepare vote, lock the actor (block non-tx messages); on Commit, atomicify.
Coordinator failover — singleton with recovery: new coordinator reads TX log, drives undecided TXs to completion.
Deadlock detection — TX A holds actor X, waits on Y; TX B holds Y, waits on X. Detect + abort one.
Timeout & abort — each TX has a deadline; coordinator aborts expired TXs.
Read isolation — what does a non-TX read see during another TX's tentative writes? Repeatable-read? Read-committed? Snapshot? Recommend snapshot.
Out of scope / non-goals
Cross-cluster transactions — local cluster only.
Transactions over external resources (DB, message broker) — only actor state.
Nested transactions — phase 1: one TX at a time per call chain. Phase 2: nested with savepoints (maybe).
Optimistic concurrency — Orleans uses pessimistic locking. Same here for phase 1.
Inferring transactions from method signatures — no decorators in TS; explicit tx.run(...) only.
Open design questions (many)
Locking granularity: per-actor (sketch) vs per-TransactionalState. Per-actor is simpler + matches actor-as-unit. Recommend per-actor.
Isolation level: Read-Committed vs Snapshot vs Serializable. Recommend Snapshot (best practical concurrency).
Coordinator placement: cluster singleton vs distributed (Raft-style). Singleton simpler; distributed scales better. Recommend singleton with persistence-based failover.
TX propagation: AsyncLocalStorage (sketch) vs explicit TX id parameter on every tell. ALS is cleaner; loses TS type-tracking. Recommend ALS.
Failure handling: actor crashes mid-TX → abort the whole TX (sketch) vs let the supervisor decide. Recommend abort.
Compensation vs rollback: 2PC rolls back tentative state; users with side effects (e.g. sent an email) need to handle compensation themselves. Document explicitly.
Phase-1 simplifications: in-memory TX log? No deadlock detection? Just timeouts? Defer features.
Performance: 2PC is slow (typically 2-3 cluster round-trips per TX). Document expected throughput (~100-1000 TX/sec, not 100K).
Test plan (sketch)
Bank transfer happy path — debit + credit; both commit; balances reflect.
Size / Priority
Rationale
The actor model deliberately avoids distributed transactions — they conflict with the "actor is the atomic unit" principle. But some domains genuinely need cross-actor ACID:
Today's options:
Orleans Transactions add a fourth option: opt-in 2-phase commit (2PC) across actors, with the coordinator + transaction-log built into the framework. Use only when you really need ACID; otherwise use the saga.
This is the most ambitious item in the catalog — a major addition that touches state-store contracts, message-dispatch, and supervision. Hence XL + dedicated design phase.
Reference: what Orleans does
Orleans's runtime:
TransactionIdonTransaction(Create).Design sketch — actor-ts equivalent (high-level only)
This is a sketch of the SHAPE, not a complete design. The implementation needs its own multi-page design doc.
Caller:
Actor:
Required infrastructure (huge)
tell/askcarries the active TX id (via AsyncLocalStorage + envelope field).Out of scope / non-goals
tx.run(...)only.Open design questions (many)
Test plan (sketch)
Acceptance criteria (rough)
TransactionalState<T>interface + implementations for journal/snapshot/object-storage backends.TransactionScope+TransactionCoordinator.Pre-implementation checklist (mandatory)