ted.neward@newardassociates.com | Blog: http://blogs.newardassociates.com | Github: tedneward | LinkedIn: tedneward
Explore what a "virtual actor" is and how it works
See how Orleans implements these virtual actors
Talk about Orleans-centric distributed system design
Code: https://github.com/tedneward/Demo-Orleans
Slides: http://www.newardassociates.com/presentations/BusyDotNetDevs/Orleans.html
actors model for CLR technology stacks
"grains"
stateless or persistent
virtual actors: "always alive" semantics
open-source
https://github.com/microsoft/orleans
used on some sizable projects
Halo 4, Halo 5
a form of inter-process communication (IPC)
extend concept of procedure calls across the network
library/infrastructure handles the transmission details
so it all looks "just like local" to developers
two processes want to communicate
specifically we want:
clients to initiate the request
clients block while server is processing
well-defined inputs/outputs
as much infrastructure to "disappear" as possible
enter remote procedure calls
server writes a function/method/endpoint
exposes/advertises it
either for ad-hoc discovery
or to a well-known endpoint (host/port/etc)
client calls a local function
magic happens
server executes call, returns results
more magic
client gets return value, carries on
client calls a local function
client marshals call information into a wire format
function name (and param types, if overloadable)
parameters
(ORPC only) remote object instance identifier
client connects to server, sends wire formatted request
server receives wire formatted request
unpacks call info, dispatches it locally
server executes call, returns results
server marshals results (return value or exception)
sends wire formatted response back to client
client receives wire formatted response
client unmarshals response and returns from local call
either returning return value
or throwing exception
based on whichever happened on server
transport: over what channel do we communicate?
TCP
UDP
HTTP
protocol: what does the wire format look like?
binary/proprietary
XML
JSON
dispatcher: how does infrastructure map to network?
identifying endpoints/calls
(sometimes) threading support
(sometimes) discoverability
RPC is sometimes characterized as "messaging"
message-out matched by message-in
RPC can be/is often built on top of messaging technologies
any messaging system can be used for RPC
so long as request can be tied to response
and client blocks until response is received
... and RPC can be used to build messaging systems
target queue: server
senders, receivers: clients
this can include other queues!
clients block until the server responds
the network is "hidden" from the caller
what happens if something goes wrong?
easy to make fatal mistakes and be too "chatty"
more difficult to version
less flexibility in some languages
requires some kind of "contract" between clients/servers
types (code) must be shared across both sides
how do we handle callbacks?
suggested in 1973 by Carl Hewitt
later, 1974: Sir Tony Hoare, Communicating Sequential Processes (CSP)
inspired by physics
as well as by Lisp, Simula, and early Smalltalk
implemented natively in Erlang
"... model of concurrent computation that treats the actor as the universal primitive of concurrent computation." (Wikipedia)
lightweight entities (actors)
on the surface, similar in concept to objects
below the surface, some very different nuances
also known in some circles as Communicating Sequential Processes (CSP)
an actor is an atomic unit of computation
actors can have local (non-shared!) state
actors communicate (only) by sending messages
upon receipt of a message, an actor can:
create more actors
send messages to other actors
designate what to do on the next message (eg, mutate internal state)
an actor will only ever be processing a single message at a time
each actor has a "mailbox" by which to receive messages
uniquely identifiable for each actor (eg, "actor:mailbox")
can either be implicit or explicit
can be either local-only or remote-accessible
share no state directly with other actors
often operate as the sole design mechanic
that is, "everything is an actor"
we don't mix actors and objects at the same conceptual level
asynchronously delivered; no blocking
any message can be sent to any actor
unknown/unrecognized messages are ignored
may be delivered out of any particular order
may or may not generate a response
Pro:
zero blocking; greatly reduced concerns around thread safety/concurrency
state management is often more encapsulated/convient, therefore easier to reason
Con:
different programming (and mental) model
more complex to implement than a "traditional" object
Microsoft .NET (.NET 10.0)
Assemblies:
Microsoft.Orleans.Sdk
Microsoft.Orleans.Server
Microsoft.Orleans.Client
... and a network
... and a database (optional)
https://github.com/tedneward/Demo-Orleans
HelloWorld subdirectory
four projects:
interfaces (IDL)
implementations
client
server/host
any CLR language (most often C#)
CLI: dotnet new --install Orleans.Contrib.UniversalSilo.Templates
brings in four CLI templates
Silo-and-Client (orleans-silo-and-client)
Standalone Client (orleans-client)
Standalone Silo (orleans-silo)
WebApi Direct Client (orleans-webapi)
BUT these are old (Orleans 3.6.0)!
HIGHLY advised to not make use of these
they were useful back in 2022, but not anymore
ClassLibrary
Reference: Microsoft.Orleans.Sdk
IHelloWorld
namespace GrainInterfaces;
using System.Threading.Tasks;
public interface IHelloWorld : Orleans.IGrainWithIntegerKey
{
Task<string> SayHello(string name);
}
ClassLibrary
depends on Interfaces project
Reference: Microsoft.Orleans.Sdk
HelloWorld
namespace Grains;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using GrainInterfaces;
public class HelloWorld : Orleans.Grain, IHelloWorld
{
private readonly ILogger _logger;
public HelloWorld(ILogger<HelloWorld> logger) { _logger = logger; }
public Task<string> SayHello(string name)
{
_logger.LogInformation("SayHello: name = '{name}', ", name);
return Task.FromResult($"Hello, {name}, welcome to Orleans");
}
}
Console
depends on Interfaces and Grains project
Reference: Microsoft.Orleans.Server
Program (host setup)
static async Task<IHost> StartSiloAsync(string[] args)
{
var builder = new HostBuilder()
.UseOrleans(c =>
{
c.UseLocalhostClustering()
.Configure<ClusterOptions>(options =>
{
options.ClusterId = "dev";
options.ServiceId = "HelloWorld";
})
.ConfigureLogging(logging => logging.AddConsole())
.AddDashboard();
});
var host = builder.Build();
await host.StartAsync();
return host;
}
Program (main)
try
{
var host = await StartSiloAsync(args);
Console.WriteLine("\n\n Press Enter to terminate...\n\n");
Console.ReadLine();
await host.StopAsync();
return 0;
}
catch (Exception ex)
{
Console.WriteLine(ex);
return 1;
}
Console
depends on Interfaces project
Reference: Microsoft.Oreleans.Client
Program.cs - main
try
{
var client = await ConnectClientAsync();
await DoClientWorkAsync(client);
Console.ReadKey();
return 0;
}
catch (Exception e)
{
Console.WriteLine($"\nException while trying to run client: {e.Message}");
Console.WriteLine("Make sure the silo the client is trying to connect to is running.");
Console.WriteLine("\nPress any key to exit.");
Console.ReadKey();
return 1;
}
Connecting to host cluster
static async Task<IClusterClient> ConnectClientAsync()
{
var host = new HostBuilder()
.UseOrleansClient(c => {
c.UseLocalhostClustering()
.Configure<ClusterOptions>(options =>
{
options.ClusterId = "dev";
options.ServiceId = "HelloWorld";
});
})
.ConfigureLogging(logging => logging.AddConsole())
.Build();
await host.StartAsync();
Console.WriteLine("Client successfully connected to silo host \n");
return host.Services.GetRequiredService<IClusterClient>();
}
Calling the grain
static async Task DoClientWorkAsync(IClusterClient client)
{
var friend = client.GetGrain<IHelloWorld>(0);
var response = await friend.SayHello("Good morning, HelloGrain!");
Console.WriteLine($"\n\n{response}\n\n");
}
Grain
Silo
Cluster
Host
Perpetual existence:
purely logical entities that always exist
cannot be created or destroyed
its virtual existence is unaffected by the failure of a server
Automatic instantiation: "activation"
Location transparency: clients are unaware of physical location
Automatic scale-out
"Virtual Actor": Identity + Logic + State
"activation"s create instances
defined observable/"hook"able lifecycle
single-threaded, serial message receipt
Orleans grains must have "long-lived" identity (to support virtuality)
Orleans uses one of five options for identity:
long - IGrainWithIntegerKey
GUID - IGrainWithGuidKey
string - IGrainWithStringKey
GUID + string (compound)
long + string (compound)
grain interface extends the identity interface
Grains can be entirely stateless
singletons, in essence
can allow multiple activations
Grains can hold state
state is held for the lifetime of the grain (e.g., forever!)
persist to external storage to preserve in face of crashes
Describes the messaging surface (API)
uses CLR interfaces and async idioms
only type visible to clients
"IDL" for Orleans
creates a strongly-typed dependency between clients and server
bundle of data passed across the wire
binary format; serialized (deep copy)
actual runtime type passed (not declared type)
Orleans generates "serializers" as part of build
can be invoked await (preserves order) or async (no ordering)
server instance/process, containing grains
manage allocation/lifecycle of grains
provides location transparency
forces "remote-first" mentality
designed to work together in a cluster
defined observable/"hook"able lifecycle
collection of silos
responsible for grain placement in Silos
random
local-to-caller (server)
hash-based
activation-count-based
configurable and customizable
process which contains the running server(s)
most often a Console, Service, or ASP.NET app
heavy use of Microsoft Hosting classes
A Host fires up, and establishes or joins a Cluster
The Cluster(s) establishes a "membership table" for Silos
usually externally, in an RDBMS or NoSQL database
a la DynamoDb, Cassandra, Azure Cosmos DB, or Redis
Any Silos are established/reestablished in the table
It is ready to receive requests
A Client fires up, and connects to a Cluster
The Client requests a grain (grain reference)
Client connects to the Cluster
... which determines an appropriate Silo
the grain may not exist yet
if it does, the silo populates a grain instance with stored state
if it doesn't, the silo creates a new C# object
that is to say, the silo "activates" the grain
The Silo returns a grain reference to the client
The Client makes a local call on the grain reference
This is turned into a Message which is sent to the Silo
The Silo determines if that grain is active
if it is not, it activates the grain
The Message is extracted into the method call on the grain, and invoked
The Silo returns the result to the client, and the client receives it
Grains always exist
runtime activates them on demand
runtime deactivates them "later" or on request
Grains fail independently
Everything is a Task or Task-of-T
enforcing/requiring asynchronicity
never do anything thread-blocking within a grain
Orleans isn't for every project
"we're focused on the 80% case"
Focus is on scalability and availability
"Contract-first": design the interfaces/API first
".NET-only": there's no cross-language/-platform play here
"Actors-first": grains are virtual actors
"Asynchronous-first": asynchronous is the default
begin by designing the interfaces for the grains
these are compiled into an assembly
which must be shared between client and server
ahead of time
versioning/evolution will need to be managed
if it doesn't run on the CLR, it can't use Orleans
at least as of Orleans 10.0 (but probably forever)
if you need cross-platform, go with POSH: plain old JSON/HTTP
Grains are not objects; they are actors
make them loosely-coupled, isolated, and primmarily independent
embrace the "let it crash" mindset
be quick to throw the instance away
don't preserve state until you're good
avoid "bottleneck" grains (registries, monitors, etc)
"virtual actors" give us some location independence and resiliency
no blocking code in your grain implementations
use Task and async/await
or IAsyncEnumerable for multi-element single-call returns
use Orleans Streaming for pub/sub scenarios
grains are non-reentrant and serialized by default
Orleans Dashboard
Persistent grains
Observers
Interceptors
Timers and reminders
.NET Aspire integration/hosting
Streams (pub/sub)
Evolving/versioning Grains
Event sourcing
Transactions
virtual actor system
built on top of CLR
providing easy language-to-messaging syntax/semantics
capable of supporting high scale/load
open-source
"Orleans: Distributed Virtual Actors for Programmability and Scalability"
https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/Orleans-MSR-TR-2014-41.pdf
this is the one that kinda kicked off the whole project
Orleans OSS repository
https://github.com/dotnet/orleans
Official docs
https://learn.microsoft.com/en-us/dotnet/orleans/
OrleansContrib
https://github.com/orgs/OrleansContrib
Awesome-Orleans
https://github.com/OrleansContrib/Awesome-Orleans
all from 2022
"Introducing Microsoft Orleans"
https://www.amazon.com/Introducing-Microsoft-Orleans-Implementing-Cloud-Native/dp/148428013X
"Microsoft Orleans for Developers"
https://www.amazon.com/Microsoft-Orleans-Developers-Cloud-Native-Distributed/dp/1484281667
"Distributed .NET w/Microsoft Orleans"
https://www.amazon.com/Distributed-NET-Microsoft-Orleans-applications-ebook/dp/B09NPBDQSL
Architect, Engineering Manager/Leader, "force multiplier"
http://www.newardassociates.com
http://blogs.newardassociates.com
Books
Developer Relations Activity Patterns (w/Woodruff, et al; APress, 2026)
Professional F# 2.0 (w/Erickson, et al; Wrox, 2010)
Effective Enterprise Java (Addison-Wesley, 2004)
SSCLI Essentials (w/Stutz, et al; OReilly, 2003)
Server-Based Java Programming (Manning, 2000)