This last lesson will wrap up our "fundamentals series" on working with actors, and it ends with a critical concept: actor life cycle.
Actors have a well-defined life cycle. Actors are created and started, and then they spend most of their lives receiving messages. In the event that you no longer need an actor, you can terminate or "stop" an actor.
There are 5 stages of the actor life cycle in Akka.NET:
Starting
Receiving
Stopping
Terminated
, orRestarting
Let's take them in turn.
This is the initial state of the actor, when it is being initialized by the ActorSystem
.
The actor is now available to process messages. Its Mailbox
(more on that later) will begin delivering messages into the OnReceive
method of the actor for processing.
During this phase, the actor is cleaning up its state. What happens during this phase depends on whether the actor is being terminated or restarted.
If the actor is being restarted, it's common to save state or messages during this phase to be processed once the actor is back in its Receiving state after the restart.
If the actor is being terminated, all the messages in its Mailbox
will be sent to the DeadLetters
mailbox of the ActorSystem
. DeadLetters
is a store of undeliverable messages, usually because an actor is dead.
The actor is dead. Any messages sent to its former IActorRef
will now go to DeadLetters
instead. The actor cannot be restarted, but a new actor can be created at its former address (which will have a new IActorRef
but an identical ActorPath
).
The actor is about to restart and go back into a Starting
state.
So, how can you link into the actor life cycle? Here are the 4 places you can hook in.
PreStart
logic gets run before the actor can begin receiving messages and is a good place to put initialization logic. Gets called during restarts too.
If your actor fails (i.e. throws an unhandled Exception) the actor's parent will restart the actor. PreRestart
is where you can hook in to do cleanup before the actor restarts, or to save the current message for reprocessing later.
PostStop
is called once the actor has stopped and is no longer receiving messages. This is a good place to include clean-up logic. PostStop also gets called during PreRestart
, but you can override PreRestart
and simply not call base.PreRestart
if you want to avoid this behavior during restarts.
DeathWatch
is also when an actor notifies any other actors that have subscribed to be alerted when it terminates. DeathWatch
is just a pub/sub system built into framework for any actor to be alerted to the termination of any other actor.
PostRestart
is called during restarts after PreRestart but before PreStart. This is a good place to do any additional reporting or diagnosis on the error that caused the actor to crash, beyond what Akka.NET already does for you.
Here's where the hook methods fit into the stages of the life cycle:
To hook in, you just override the method you want to hook into, like this:
/// <summary>
/// Initialization logic for actor
/// </summary>
protected override void PreStart()
{
// do whatever you need to here
}
PreStart
is far and away the most common hook method used. It is used to set up initial state for the actor and run any custom initialization logic your actor needs.
The second most common place to hook into the life cycle is in PostStop
, to do custom cleanup logic. For example, you may want to make sure your actor releases file system handles or any other resources it is consuming from the system before it terminates.
PreRestart
is in a distant third to the above methods, but you will occasionally use it. What you use it for is highly dependent on what the actor does, but one common case is to stash a message or otherwise take steps to get it back for reprocessing once the actor restarts.
In the event that an actor accidentally crashes (i.e. throws an unhandled Exception,) the actor's supervisor will automatically restart the actor's lifecycle from scratch - without losing any of the remaining messages still in the actor's mailbox.
As we covered in lesson 4 on the actor hierarchy/supervision, what occurs in the case of an unhandled error is determined by the SupervisionDirective
of the parent. That parent can instruct the child to terminate, restart, or ignore the error and pick up where it left off. The default is to restart, so that any bad state is blown away and the actor starts clean. Restarts are cheap.
This final exercise is very short, as our system is already complete. We're just going to use it to optimize the initialization and shutdown of TailActor
.
See all this in the constructor of TailActor
?
// TailActor.cs constructor
// start watching file for changes
_observer = new FileObserver(Self, Path.GetFullPath(_filePath));
_observer.Start();
// open the file stream with shared read/write permissions
// (so file can be written to while open)
_fileStream = new FileStream(Path.GetFullPath(_filePath), FileMode.Open,
FileAccess.Read, FileShare.ReadWrite);
_fileStreamReader = new StreamReader(_fileStream, Encoding.UTF8);
// read the initial contents of the file and send it to console as first message
var text = _fileStreamReader.ReadToEnd();
Self.Tell(new InitialRead(_filePath, text));
While it works, initialization logic really belongs in the PreStart()
method.
Time to use your first life cycle method!
Pull all of the above initialization logic out of the TailActor
constructor and move it into PreStart()
. We'll also need to change _observer
, _fileStream
, and _fileStreamReader
to non-readonly fields since they're moving out of the constructor.
The top of TailActor.cs
should now look like this
// TailActor.cs
private string _filePath;
private IActorRef _reporterActor;
private FileObserver _observer;
private Stream _fileStream;
private StreamReader _fileStreamReader;
public TailActor(IActorRef reporterActor, string filePath)
{
_reporterActor = reporterActor;
_filePath = filePath;
}
// we moved all the initialization logic from the constructor
// down below to PreStart!
/// <summary>
/// Initialization logic for actor that will tail changes to a file.
/// </summary>
protected override void PreStart()
{
// start watching file for changes
_observer = new FileObserver(Self, Path.GetFullPath(_filePath));
_observer.Start();
// open the file stream with shared read/write permissions
// (so file can be written to while open)
_fileStream = new FileStream(Path.GetFullPath(_filePath),
FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
_fileStreamReader = new StreamReader(_fileStream, Encoding.UTF8);
// read the initial contents of the file and send it to console as first message
var text = _fileStreamReader.ReadToEnd();
Self.Tell(new InitialRead(_filePath, text));
}
Much better! Okay, what's next?
TailActor
instances are each storing OS handles in _fileStreamReader
and FileObserver
. Let's use PostStop()
to make sure those handles are cleaned up and we are releasing all our resources back to the OS.
Add this to TailActor
:
// TailActor.cs
/// <summary>
/// Cleanup OS handles for <see cref="_fileStreamReader"/>
/// and <see cref="FileObserver"/>.
/// </summary>
protected override void PostStop()
{
_observer.Dispose();
_observer = null;
_fileStreamReader.Close();
_fileStreamReader.Dispose();
base.PostStop();
}
That's it! Hit F5
to run the solution and it should work exactly the same as before, albeit a little more optimized. :)
Compare your code to the solution in the Completed folder to see what the instructors included in their samples.
Ready for more? Start Unit 2 now.
Come ask any questions you have, big or small, in this ongoing Bootcamp chat with the Petabridge & Akka.NET teams.
If there is a problem with the code running, or something else that needs to be fixed in this lesson, please create an issue and we'll get right on it. This will benefit everyone going through Bootcamp.