Skip to content

ActorsInLeucine

devlaam edited this page Oct 19, 2026 · 7 revisions

Actors in Leucine

Leucine is an actor framework with many of the properties described in the "Actors in General" paragraph. However, to keep the design simple, some choices were made. So you get many of the benefits of the larger frameworks out there, but not all. First, we speak of letters and messages. The latter are formally the content of the former. However, most of the time this difference is irrelevant, and we will use both terms interchangeably. Now, let's review the features one by one.

Basic Features

  • Leucine is typed actor system in two aspects. First, the compiler can check if the letters can be handled by the actor. If not, the code will not compile. At the same time, if the letters are derived from a sealed trait, the compiler can also determine if all letters it promised to handle are actually handled. Second, the senders are also typed, and each actor will refuse letters at compile time if the sender is not a registered type. Both limitations help to prohibit unwanted messages being passed around.
  • Depending on the platform, actors run on one or more threads. The user does not need to care, and neither race conditions nor deadlocks can occur, also long as the user takes a few precautions. To prohibit race conditions mutable state from the actor should never be exposed to the outside, and this also holds true for methods that manipulate internal states. Immutable values like configuration can be exposed just fine. Deadlocks can be prohibited if all method calls stay either within the actor, or are guaranteed to be purely functional. Never make use of synchronized in the call chain somewhere. And, maybe most importantly, messages must be completely immutable.
  • Actors are classes you define and later instantiate. Depending on the needs this actor can use a simple or more complex base class. The latter also come with a larger footprint. The simple actors accept messages from all senders, the most advanced keep allow you to couple specific letters to specific senders. This limit the chance that letters are send to the wrong destination or are ignored when received.
  • All actors must have a name, which you can define or auto generate. This name should be unique, among the base actors, or among its siblings when part of a family.
  • In order to actually do something in an actor, it must receive a message (apart from some startup code perhaps). Messages are instances of letter classes defined in the companion object of the actor (best practice). Often these are just wrappers around some other data. Since messages stay on one machine, sending is fast, for only the object reference needs to be send around.
  • Letters accepted by the actor are put into the mailbox and are processed in the order of arrival. However, in general it is not clear what that order is, so your code must be able the handle the messages independent thereof. If, for some reason, a letter cannot be handled at a certain moment, it can be stashed away for later processing.
  • The number of letters that can be stored in the mailbox can be maximized. When the mailbox is full, new letters will be refused. By testing this, a system can build back pressure and prohibit overload before this becomes a problem. For example, refuse a connection on a busy server.
  • There are two situations where there are guarantees about the order. The first arises when two actors just communicate between each other. When the first actor sends two messages after each other, they will be processed on the receiving actor in the same order. Note that messages from other actors may come in between though. And if the first actor sends the last messages via an other actor, it still may arrive before the former message. The other guarantee arises from the stash. If you decide to unstash, all messages from the stash will be presented in the order they where stashed, and before messages from other actors. The only message that may come in between are event messages (from timers or I/O), if you defined them.
  • Actors can be told to stop in different ways, which can be done from the outside, and by anyone who holds a reference to the actor. The stop(Direct) call makes sure the actor completes the current message, and subsequently starts its tear down sequence. New messages are no longer accepted, and messages in the mailbox are ditched. Once such a call is made the actor cannot be used any more. Actors can also be asked to stop, but first finish whatever is in the mailbox or stash with stop(Finish). New messages are no longer accepted. Usually this is the more 'controlled' way to stop an actor. There three more modes of stopping, which stop the actor depending on the activity of the actor, so you can keep clean actor system.
  • The message handler catches exceptions being generated and directs them to a callback handler. In principle these can be ignored, and the actor proceeds to the next message. The state (if any) is unchanged. Errors on the other hand bubble up. If these need to be caught, custom code is needed.
  • Actor Termination also generates a callback, for example when some stop mode is used. This callback is guaranteed to be called before actor deactivation and after the last message is processed. If there were any unprocessed letters in this actor at tear down these are reported. In case of a actorContext shutdown the termination callbacks are not called.
  • Unhandled letters can be caught, collected and logged for example. The reason why the letter was not handled is also kept. Among others, this could be for example that the actor has stopped. New letters are subsequently refused and a direct stop on the actor may also cause left over letters.
  • There is logging transport mechanism present that is able to cooperate with the actor framework for reduce logging overhead. It is not a replacement for the usual logging frameworks which you can still use. It can be used as a simple stand alone logger, or coupled to the logging framework of your liking, or be completely ignored.
  • It runs on JVM, JS and Native. On the JVM/Native it makes use of a thread pool to distribute the tasks. Blocking (short) in a thread is not strictly forbidden, but discouraged. On JS there is only thread on which everything is scheduled. Blocking direct stops all other activity, so this is not possible. Despite these differences, there are no noticeable differences for the user. All features (like timers, waiting for I/O etc) run equally well on all platforms. So Leucine isolates you from the differences in treading implementation. For each platform there is also the possibility to run in a virtual main loop. This gives, when no timers are defined, a strictly deterministic program flow.

Advanced features

  • It is possible to organize your actors in hierarchical structures called a Family. Also here you can follow the basic approach, where all actors of one family are the same (except for the root, which cannot have a parent) or you may define different actors at each level, each with its own letter and sender types. In a family the buildup and tear down of the actors takes place in an organized manner. Also, you can pass messages to one specific child, or relay them to some of the children. There can be multiple family root's, to separate your concerns.
  • Sometimes you cannot handle a message right away, and it is needed to postpone its processing. To that end, you can make use of a Stash. Any message put there is kept in FIFO order, and as soon as you flush the stash, the letters reappear before the other letters present in the mailbox. This makes handling predictable. You cannot only store them, but also change the letter or spoof its sender, if needed, before stashing.
  • Actors react on external events, the messages, but it is also possible to generate the messages internally, based on a timer or i/o event. This message is always from the actor to itself. In the receive method you may subsequently initiate other events and set external events into motion. Each timer or event is anchored (per default) on the actor itself, but you may define your own anchors and have multiple timers in parallel in one actor. Leucine guarantees that timers defined in order with non decreasing delay values will keep that order. Furthermore, if a timer is cancelled during message handling, that event will not generate a new message, even if the timer did already expire. There are no provisions for accurate time intervals or repeated timers, they are only correct by approximation, and repetition is reached by restarting the timer after it has expired.
  • There is a protector mixin that guards the number of letters in the mailbox and that calls an event when a threshold has been reached. Further letters are then still accepted (up to the overflow limit), but this enables the possibility of creating managed back pressure. When the mailbox is depleted, you will be informed again. Using this mechanism ensures no letters get lost, provided they are throttled upstream. This is better than to rely solely on the overflow protection, which keeps the mailbox filled to the limit.
  • There is a monitor mixin and separate class you can instantiate which enables you to get insight in the way the actors operate. This can be used to get insight in the inner working of your code for debugging or system supervision. This works by periodically sampling the actors and copy all their collected statistics. In its lightweight form you can monitor if the system is in good health, which may be useful for server systems that run continuously. With all functions enabled, it allows you to track every letter in the system, useful for debugging but to heavy for production operations.

Limitations

Compared to other actor systems (for example Akka) there may be a few properties lacking you may have expected:

  • It is not possible to disperse actors over machine boundaries. Of course, the user may roll its own cross machine boundary messaging implementation.
  • Also, Leucine provides only some guarantees about the message sequences (see above). In particular, messages can enter the mailbox in any order, and the user must be able to handle that.
  • There is no advanced life cycle management, the user is responsible to take care of crashed actors. In fact, we think crashing actors are a sign of a bug that needs solving, so it is best this is not being hidden by a silent restart.
  • Although care has been taken to make efficient use of resources, it may not be the most performant actor system on the planet. This still has to be tested and maybe optimized. And messages are passed from one actor to an other by synchronization. Although the code inside the synchronized sections is kept brief, it is there. The user can keep the impact low, by only passing precalculated immutable structures, so refrain from calculations inside the send methods or actor constructor bodies.

Conclusion

Leucine is just an other actor system with the main advantage being simple (but still feature rich), cross-platform and MIT licensed. And of course, it is completely stand alone, so not dependencies are forced upon the user. On the other pages we discuss all its possibilities in more depth with examples.

Internals of an actor

Clone this wiki locally