Files
scadalink-design/AkkaDotNet/01-Actors.md
Joseph Doherty de636b908b Add Akka.NET reference documentation
Notes and documentation covering actors, remoting, clustering, persistence,
streams, serialization, hosting, testing, and best practices for the Akka.NET
framework used throughout the ScadaLink system.
2026-03-16 09:08:17 -04:00

189 lines
7.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 01 — Actors (Akka Core Library)
## Overview
Actors are the foundational building block of the entire SCADA system. Every machine, protocol adapter, alarm handler, and command dispatcher is modeled as an actor. Actors encapsulate state and behavior, communicate exclusively through asynchronous message passing, and form supervision hierarchies that provide automatic fault recovery.
In our system, the actor model provides three critical capabilities: an actor-per-device model where each of the 50500 machines is represented by its own actor instance, a protocol abstraction hierarchy where a common base actor defines the machine comms API and derived actors implement OPC-UA or custom protocol specifics, and supervision trees that isolate failures in individual device connections from the rest of the system.
## When to Use
- Every machine/device should be represented as an actor — this is the core modeling decision
- Protocol adapters (OPC-UA, custom) should be actors that own their connection lifecycle
- Command dispatchers, alarm aggregators, and tag subscription managers are all actors
- Any component that needs isolated state, failure recovery, or concurrent operation
## When Not to Use
- Simple data transformations with no state — use plain functions or Streams stages instead
- One-off request/response patterns with external HTTP APIs — consider using standard async/await unless the call needs supervision or retry logic
- Database access layers — wrap in actors only if you need supervision; otherwise inject repository services via DI
## Design Decisions for the SCADA System
### Actor-Per-Device Model
Each physical machine gets its own actor instance. The actor holds the current known state of the device (tag values, connection status, last command sent, pending command acknowledgments). This provides natural isolation — if communication with Machine #47 fails, only that actor is affected.
```
/user
/device-manager
/machine-001 (OpcUaDeviceActor)
/machine-002 (CustomProtocolDeviceActor)
/machine-003 (OpcUaDeviceActor)
...
```
### Protocol Abstraction Hierarchy
Define a common message protocol (interface) that all device actors respond to, regardless of the underlying communication protocol. The actor hierarchy looks like:
```
IDeviceActor (message contract)
├── OpcUaDeviceActor — implements comms via OPC-UA, subscribes to tags
└── CustomDeviceActor — implements comms via legacy custom protocol
```
Both actor types handle the same inbound messages (`SubscribeToTag`, `SendCommand`, `GetDeviceState`, `ConnectionLost`, etc.) but implement the underlying transport differently. The rest of the system only interacts with the message contract, never with protocol specifics.
### Supervision Strategy
The device manager actor supervises all device actors with a `OneForOneStrategy`:
- `CommunicationException` → Restart the device actor (re-establishes connection)
- `ConfigurationException` → Stop the actor (broken config won't fix itself on restart)
- `Exception` (general) → Restart with backoff (use exponential backoff to avoid reconnection storms)
- Escalate only if the device manager itself has an unrecoverable error
### Message Design
Messages should be immutable records. Use C# `record` types:
```csharp
public record SubscribeToTag(string TagName, IActorRef Subscriber);
public record TagValueChanged(string TagName, object Value, DateTime Timestamp);
public record SendCommand(string CommandId, string TagName, object Value);
public record CommandAcknowledged(string CommandId, bool Success, string? Error);
public record GetDeviceState();
public record DeviceState(string DeviceId, ConnectionStatus Status, IReadOnlyDictionary<string, TagValue> Tags);
```
## Common Patterns
### Stash for Connection Lifecycle
Device actors may receive commands before the connection is established. Use `IWithStash` to buffer messages during the connecting phase, then unstash when the connection is ready:
```csharp
public class OpcUaDeviceActor : ReceiveActor, IWithStash
{
public IStash Stash { get; set; }
public OpcUaDeviceActor(DeviceConfig config)
{
Connecting();
}
private void Connecting()
{
Receive<Connected>(msg => {
UnstashAll();
Become(Online);
});
Receive<SendCommand>(msg => Stash.Stash());
}
private void Online()
{
Receive<SendCommand>(HandleCommand);
Receive<ConnectionLost>(msg => Become(Connecting));
}
}
```
### Become/Unbecome for State Machines
Device actors naturally have states: `Connecting`, `Online`, `Faulted`, `Maintenance`. Use `Become()` to switch message handlers cleanly rather than littering `if/else` checks throughout.
### Ask Pattern — Use Sparingly
Prefer `Tell` (fire-and-forget with reply-to) over `Ask` (request-response). `Ask` creates temporary actors and timeouts. Use `Ask` only at system boundaries (e.g., when an external API needs a synchronous response from a device actor).
## Anti-Patterns
### Sharing Mutable State Between Actors
Never pass mutable objects in messages. Each actor's state is private. If two actors need the same data, send immutable copies via messages. This is especially critical in the SCADA context — two device actors should never share a connection object or a mutable tag cache.
### God Actor
Avoid creating a single "SCADA Manager" actor that handles all devices, alarms, commands, and reporting. Decompose into a hierarchy: device manager → device actors, alarm manager → alarm processors, command queue → command handlers.
### Blocking Inside Actors
Never block an actor's message processing thread with synchronous I/O (e.g., `Thread.Sleep`, synchronous socket reads, `.Result` on tasks). This stalls the actor's mailbox and can cascade into thread pool starvation. Use `ReceiveAsync` or `PipeTo` for async operations:
```csharp
// WRONG
Receive<ReadTag>(msg => {
var value = opcClient.ReadValueSync(msg.TagName); // blocks!
Sender.Tell(new TagValue(value));
});
// RIGHT
ReceiveAsync<ReadTag>(async msg => {
var value = await opcClient.ReadValueAsync(msg.TagName);
Sender.Tell(new TagValue(value));
});
```
### Over-Granular Actors
Not everything needs to be an actor. A device actor can internally use plain objects to manage tag dictionaries, parse protocol frames, or compute alarm thresholds. Only promote to a child actor if the component needs its own mailbox, lifecycle, or supervision.
## Configuration Guidance
### Dispatcher Tuning
For 500 device actors doing I/O-bound work (network communication with equipment), the default dispatcher is usually sufficient. If profiling shows contention, consider a dedicated dispatcher for device actors:
```hocon
device-dispatcher {
type = Dispatcher
throughput = 10
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 4
parallelism-factor = 1.0
parallelism-max = 16
}
}
```
### Mailbox Configuration
Default unbounded mailbox is fine for most SCADA scenarios. If tag subscription updates are very high frequency (thousands per second per device), consider a bounded mailbox with a drop-oldest strategy to prevent memory growth from stale updates:
```hocon
bounded-device-mailbox {
mailbox-type = "Akka.Dispatch.BoundedMailbox, Akka"
mailbox-capacity = 1000
mailbox-push-timeout-time = 0s
}
```
### Dead Letter Monitoring
In a SCADA system, dead letters often indicate a device actor has been stopped but something is still trying to send to it. Subscribe to dead letters for monitoring:
```csharp
system.EventStream.Subscribe<DeadLetter>(monitoringActor);
```
## References
- Official Documentation: <https://getakka.net/articles/intro/what-is-akka.html>
- Actor Concepts: <https://getakka.net/articles/concepts/actors.html>
- Supervision: <https://getakka.net/articles/concepts/supervision.html>
- Dispatchers: <https://getakka.net/articles/actors/dispatchers.html>