Skip to content

ActorStop

devlaam edited this page Jun 7, 2023 · 7 revisions

Actor.Stop: Different ways to stop an actor

Text compliant with Leucine version 0.5.x

Introduction

Starting an actor is very easy, just instantiate the class that you derived from one of the actor types. Like this:

class MyActor(name: String) extends AcceptActor(MyActor,name) :
 ...

val myActor1 = MyActor("one")
val myActor2 = MyActor("two")
...

When the actor is created:

  • it is automatically put in the ActorGuard or parent of a family if applicable,
  • the monitor will start taking samples if MonitorAid is mixed in
  • it will start by itself,
  • you may directly send messages to it by send.

In short, it is 'fully functional' directly after its creation. However, stopping an actor can be done in many different ways.

How to stop an actor

Although there are many ways an actor can stop, there is only one call to do so. That is:

/**
 * Stopping of an actor is organized in levels of severity. The lowest level (Direct) terminates directly, the
 * highest level never terminates. The latter is the default. Levels can always be decreased, increase is only
 * possible if the stop action was not yet initiated. Direct and Finish start immediately, and cannot be retracted. */
final def stop(way: Stop): Unit

This call is defined on the Actor type itself and can be used from the inside and outside of an actor, at any moment. What actually happens after this call depends on the stop value. Stop is an enumerated type defined in the object Actor with the following fields:

enum Stop extends EnumOrder[Stop] :
  /** Stop the actor asap, but complete the running letter. Subsequently stop all children likewise. Terminate afterwards. */
  case Direct
  /** The mailbox is finished, but no more letters are accepted. Subsequently finish all children likewise. Terminate afterwards. */
  case Finish
  /** Wait until all children have stopped, if present, and finish directly afterwards. */
  case Barren
  /** Wait until the actor and all children, if present, are silent for some time, then stop directly. */
  case Silent
  /** This actor may be terminated by the system if all other non final actors are terminated by themselves or the user. */
  case Final
  /** This actor never stops (initial state). */
  case Never

If you call stop from the outside, it is usually not completely clear which letter is processed, and what the current state of the mailbox is. So you do not know where stop(Direct) 'makes the cut'. But from the inside the difference is clear: stop(Direct) only completes the current message, and then terminates, and stop(Finish) completes all messages in the mailbox, and those on the Stash if they are flushed in the mean time. Even timers that expire during this procedure are handled. But new letters are not accepted. So the queue will eventually be depleted after which the actor is terminated. Likewise, if you call stop(Finish) from within an other actor, which is the sole source of letters for this actor, you have deterministic control over what happens at the terminated actor: all letters send before the call, which are know to you, will be processed.

The stop(Barren) call comes in handy in families where you create a parent on the fly, just for distributing work. After all worker children of the parent are done, the parent will terminate itself automatically. If an actor has no children (or is not part of a family) then this call acts like a stop(Finish).

The stop(Silent) call can be used to ditch actors which are doing nothing. Sometimes you want a service to be present, but free up its resources when no one makes use of it, to recreate only when asked for. Note that this only works for an actor system with termination polling by ActorGuard.watch. See Lifetime management of all actors for more information.

The stop(Final) call is there for actors that only depend on other actors but have no use on their own. A good example is the Logger. You want this service to be around, as long as there is any other activity, but if there is none, this actor should not keep the system from terminating as a whole. But since all other activities are gone, who is there to stop the Logger? In this case this is done by the ActorGuard, which stops all actors in Final stop mode.

Lastly we have the mode Never. Calling stop(Never) is useful if you need to change an earlier stop call. Note that you cannot cancel a stop(Direct) or stop(Finish) with it, but the other stop modes can be cancelled as long as they have not become active.

The started() and stopped(...) callbacks

After construction of the actor, the first call will be to started(). This is handled in a different thread than the constructor runs in, to make sure the constructing actor in not overloaded (if you not do heavy work in the constructor itself).

In the tear down procedure of the actor, after all messages that were supposed to be handled are handled, stopped(...) will be called.

In between the messages, if any, will be handled.

Their signatures are:

/**
 * Called after actor construction and guaranteed before the first message is processed. Use this to
 * perform work to initialize the actor. Apart from a few instructions, work should not be done in
 * the constructor itself since this effectively runs in the thread of the actor that constructed this
 * actor. The method started() runs in its own thread. Override this with your own implementation. */
protected def started(): Unit

/**
 * Called before actor deactivation and guaranteed after the last message is processed. If there were
 * any unprocessed messages in this actor at tear down, complete is false. These could be in the normal
 * mailbox or on the stash, if present. Cause returns the last stop mode, so the cause of stopping
 * this actor is known. In case of a actorContext shutdown this is NOT called, for this disruptively
 * terminates all processing loops. It is however called when stop(...) is used, or when the actor
 * is shutdown by a parent. The actor may still be around after this method is called, but will never
 * accept new messages. The parent is still defined, when stopped() is executed (but may already
 * stopped processing messages) but all the children will already be removed from the list, and their
 * stopped() methods have already been called. Apart from the situation described above, you can rely
 * on started() and stopped() to always come in pairs, even when no messages are processed at all. */
  protected def stopped(cause: Actor.Stop, complete: Boolean): Unit

They need to be overridden, for their implementations are blank.

Internals of an actor

Clone this wiki locally