Akka.NET
The Akka.NET library brings the classic Actor model to .NET. You create the Actors yourself, in a tree of parents and children. Each parent supervises its children, and when one of them fails, it decides what to do with it.
Author: bezzad
The problem: the warehouse on a sale day
On a sale day, thousands of reservation requests reach the shop’s warehouse in one second. Each product has its own stock. We have two problems:
- Concurrency. Two requests must not reserve the last item at the same time.
- Failure. If the code for one product fails (for example, because of bad data), the whole warehouse must not stop working.
In the Actor model lesson, we saw that one Actor for each product solves the first problem. Akka.NET also solves the second problem with a simple idea: each Actor has a parent that looks after it.
The main idea: a tree of Actors
In Akka.NET, each Actor is created by another Actor. So all Actors form a tree.
- The Actor system (ActorSystem) is the root of the tree. Usually we have one in each app.
- Each Actor creates its own children. The stock manager creates the Actor for each product.
- Each Actor has an address. Other code has only a reference (IActorRef), not the object itself. So it cannot touch its state.
- Each parent supervises its children. This is the most important idea in Akka.
Code: a simple Actor
Messages are immutable records:
public sealed record Reserve(string OrderId, string ProductId, int Quantity);
public sealed record Reserved(string OrderId);
public sealed record OutOfStock(string OrderId);
The Actor for each product. With Receive, we say what to do for each message type:
using Akka.Actor;
public sealed class ProductStockActor : ReceiveActor
{
private int _available;
public ProductStockActor(string productId, IStockRepository stock)
{
_available = stock.GetAvailable(productId); // runs again after a restart
Receive<Reserve>(msg =>
{
if (msg.Quantity > _available)
{
Sender.Tell(new OutOfStock(msg.OrderId));
return;
}
_available -= msg.Quantity;
Sender.Tell(new Reserved(msg.OrderId));
if (_available == 0) Become(SoldOut); // change behavior for next messages
});
}
private void SoldOut() =>
Receive<Reserve>(msg => Sender.Tell(new OutOfStock(msg.OrderId)));
}
Two points in this code:
- The Sender property is the Actor that sent the current message. We send the reply to it.
- The Become method changes the Actor’s behavior for the next messages. After the stock runs out, the Actor does not even check the number any more.
The stock manager creates the child for each product the first time, and gives the message to it:
public sealed class StockManagerActor : ReceiveActor
{
public StockManagerActor(IStockRepository stock)
{
Receive<Reserve>(msg =>
{
var child = Context.Child(msg.ProductId);
if (child.Equals(ActorRefs.Nobody))
child = Context.ActorOf(
Props.Create(() => new ProductStockActor(msg.ProductId, stock)),
msg.ProductId);
child.Forward(msg); // keep the original sender, so the child replies to it
});
}
}
And using it from outside, for example in an API:
var system = ActorSystem.Create("shop");
var manager = system.ActorOf(Props.Create(() => new StockManagerActor(repository)), "stock");
// Tell: fire and forget.
manager.Tell(new Reserve("o-1", "p-7", 1));
// Ask: wait for one reply, with a timeout.
var reply = await manager.Ask<object>(new Reserve("o-2", "p-7", 1), TimeSpan.FromSeconds(3));
Supervision: let it crash
The Akka idea comes from Erlang: instead of try/catch everywhere, let the Actor crash and let its parent decide.
- The child fails. Its current message is left half done.
- Its mailbox is paused, and the parent gets the failure notice.
- The parent makes one of four decisions. Continue, create again, stop, or pass it to the parent above.
- The rest of the warehouse keeps working. A failure in the headphones Actor has no effect on the phone Actor.
public sealed class StockManagerActor : ReceiveActor
{
// ... the same constructor as before ...
protected override SupervisorStrategy SupervisorStrategy() =>
new OneForOneStrategy(
maxNrOfRetries: 3,
withinTimeRange: TimeSpan.FromMinutes(1),
localOnlyDecider: ex => ex switch
{
InvalidDataException => Directive.Restart, // reload clean state
TimeoutException => Directive.Resume, // state is still fine
_ => Directive.Escalate
});
}
The OneForOne strategy means the decision is applied only to the child that failed. Another strategy, named AllForOne, applies the decision to all the children.
Important rules
- Message order is guaranteed only between two specific Actors. If A sends two messages to B, B gets them in the same order. But there is no order between messages from A and from C.
- Message delivery is “at most once” by default. If a message is lost between two servers, Akka does not send it again by itself. For important work, ask for a reply and set a Timeout.
- Do not wait for long work inside an Actor. If you have a long await, the mailbox waits behind it. Start the work, and send the result back to the Actor itself as a new message with PipeTo.
- Do not give out the state. Send only immutable messages.
Akka.NET or Orleans?
Both are Actor models, but they have different philosophies:
| Akka.NET | Orleans | |
|---|---|---|
| Creating Actors | You create and stop them yourself. | Automatic, with the first message. |
| Structure | A tree of parents and children. | No tree. Only an ID. |
| Error handling | The parent, with a supervision strategy. | The error goes back to the caller, like a normal method. |
| Communication | Messages with Tell, and Ask when needed. | Calling an async method on an interface. |
| Spreading over several servers | The Cluster and Cluster Sharding packages. | Built in and on by default. |
| Learning curve | Steeper, more concepts. | Gentler for a .NET developer. |
| Good for | Complex message flows, precise control over lifecycle and errors. | A large number of independent entities, like a cart or a player. |
Common mistakes
| Mistake | Result | Right way |
|---|---|---|
| Using Ask between all Actors | Actors wait for each other, and Timeouts grow. | Communicate with Tell, and reply as a message. |
| try/catch for all errors inside the Actor | The real error is hidden, and the state stays broken. | Leave the error to the parent and write a supervision strategy. |
| Thinking the state stays after a Restart | The stock becomes zero or wrong. | Save important state and read it again. |
| Sending a mutable object in a message | Two Actors touch the same data. | Only immutable records. |
| One Actor for all products | It becomes a bottleneck. | One Actor for each product. |
Summary in six lines
- In Akka.NET you create the Actors yourself, and they form a tree.
- Each Actor defines its behavior for each message type with Receive, and changes its behavior with Become.
- The main communication is with Tell. Ask is for the border with normal code.
- Each parent supervises its children: continue, create again, stop, or pass it up.
- After a Restart, the memory state is cleared. Save important state.
- The Orleans library is simpler and more automatic. The Akka.NET library gives more control.