Skip to content

ActorGuard

devlaam edited this page Oct 19, 2026 · 8 revisions

ActorGuard: Lifetime management of all actors

Text compliant with Leucine version 0.7.x

Introduction

Up to now we made use of natural lifetime management of the actors. In other words, when there were no more messages, the actor stopped and garbage collector ditched the object. For a larger system, this is not adequate, and more control over this process is needed. Also, the termination behavior depends on the platform and runtime environment. With ActorGuard we automatically put all actors under ... guard. At the same time some extra capabilities become available as a result of that. Note that the actor guard is always present, regardless if it is being used or not.

Using ActorGuard.watch

The ActorGuard is an object, so there is only one in the whole system, and the watch facility is always located at the main entry point of your application as last statement. Depending on the platform it may block or not when you call watch.

The signature of watch is:

  /**
   * Start watching for actor system completion. This uses polling to see if all actors are done.
   * Do not set the pollInterval to low, for this calls all actors under guard. The minimum is 1
   * second. In practice this time defines the maximum time to wait for the application to terminate
   * after all the work is done. So for servers running days or more 10 seconds to 1 minute may be a
   * good value, for CLI apps, use 2 seconds. Use force if you want to terminate other processes as
   * well when the actors are all completed to shutdown. The function complete() is called after all
   * actors have stopped. Calling the watch method may be needed to start the actor system. */
def watch(force: Boolean, pollInterval: FiniteDuration, complete: () => Unit = () => ())(using context: ActorContext): Unit

To illustrate its use, less us look at the way this is used in the demos that come along with this library. The main entry point is defined as:

object Init extends LogInfo:
  import PlatformContext.Platform

  /* When you arrive here, you can be certain all actors are done */
  def complete(): Unit = println("Demo complete")

  @main
  def main(): Unit =
    /* Welcome message */
    println(s"Started Actor examples on the ${actorContext.platform} platform.")
    /* Open the console as an actor to get commands from the user. */
    val console = new Console
    /* Watch the actors for completion. Note that this blocks for the JVM and Native platforms
     * but not on JS. There blocking is not possible and this call returns immediately. Normally
     * you do not call watch on JS, for this does have not much added value. Except if you use
     * stop(Silent) somewhere. */
    ActorGuard.watch(false,3.seconds,complete)
    /* The application should exit when all work is done. */

We defined the polling for termination to be 3 seconds, so the user does not need to wait to long after completion.

Stopping inactive actors.

Making use of watch enables an other feature and that is to stop an actor when it has gone silent for a certain amount of time. The ActorGuard keeps a list of all actors in the Stop.Silent mode and terminates those that are inactive. This can be handy for services that are created on the fly but of which the termination cannot be clearly defined. With Stop.Silent and ActorGuard.watch they are terminated when no longer needed. Note that the reverse also holds true. If ActorGuard.watch is not used in your system, Stop.Silent is ineffective. It relies on the polling mechanism so that we do not need to define a timer for each actor separately.

Getting actors by their name.

The ActorGuard has one other public method with which you may obtain any actor of the system based on its path. It is much like the get on the FamilyActor but it works system wide. This facility does not depend on the use of ActorGuard.watch and is always at your disposal on the object ActorGuard.

/**
 * Get the actor with this path/name if it exists. It will recurse into the family tree if required
 * All actors you gave a name manually are indexed. Here is the primary search point. If you are
 * already inside a family actor, it is more efficient to search just that tree. */
def get(path: String)(using context: ActorContext): Option[Actor]

It will return the actor if it exists, but the type will be an generic Actor. With this interface you can stop it, and get its activity, but that's about it. If you need to send a letter to it, do a match on the expected type.

Catching unhandled letters.

You can register a function at ActorGuard.failed(..) with the signature:

/**
 * Here you register a handler for failed messages. This includes all messages that
 * are refused at sending, cause an exception that is not catched by the user, is
 * unmatched (manually added) or is left over as unprocessed when the actor stops
 * prematurely. The post handler can be called in any context, so the execution should
 * be very brief. It is for logging, or post processing by sending it to an other actor.
 * Nothing more. Define this handler at the beginning of your application and make sure
 * it is only defined once.  */
def failed(post: Actor.Post => Unit): Unit

to get a report on all messages that were not regularly processed. This could be routed to a logger at the start of the application for example:

ActorGuard.failed(post => logger.warn(post.full))

For more information on unhandled messages, see the page Catch unhandled messages.

Internals of an actor

Clone this wiki locally