Actors for Squeak Smalltalk

Smalltalk Balloon (by Bert Freudenberg) The Smalltalk-80 concurrency model is, at heart, threads with mutable shared state plus locks, semaphores, mutexes and so on.

This library adds an Erlang-style Actor model to Squeak.

The following code snippet creates an actor, and invokes one of its methods via synchronous (Promise-based) RPC:

h := HelloWorldActor spawn.
(h greet: 'Actor world') wait. "produces 'Hello, Actor world!'"

Quickstart

  1. Download a recent Squeak image and VM.
  2. Update to at least trunk update number 17768.
  3. (Installer squeaksource project: 'Actors') install: 'ConfigurationOfActors'
  4. (optional) Run the tests in package Actors-Tests.

Features

Installation

To install the package, you will need a recent Squeak. I have been using the 6.0-alpha trunk series: that is, the bleeding edge.

Quickstart

  1. Download a recent Squeak image and VM.
  2. Update to at least trunk update number 17768.
  3. (Installer squeaksource project: 'Actors') install: 'ConfigurationOfActors'
  4. (optional) Run the tests in package Actors-Tests.

Detailed instructions

Download Squeak

Download a recent 6.0-alpha trunk image and VM for your platform.

Update Squeak

Update your image. Execute the following in a workspace:

MCMcmUpdater updateFromServer

Alternatively, click the Squeak icon in the top left of the window, and choose “Update Squeak”:

Squeak update menu item

As of this writing, my updated image is at update number 17768. This includes all the Kernel changes necessary for supporting the Actors package.

Install the Actors package

Execute the following in a workspace

(Installer squeaksource project: 'Actors') install: 'ConfigurationOfActors'

Generally, the ConfigurationOfActors package is the latest release of the package. After it has been loaded, you can optionally update to the latest development version with

"Optional step: update to the latest development version."
(Installer squeaksource project: 'Actors') install: 'Actors'

Running the Tests

Open a Test Runner, either from the main World menu, or by executing

TestRunner open

in a Workspace. Select the Actors-Tests package from the left-hand-side list, and click “Run Selected”:

TestRunner on Actors-Tests

User Manual

This documentation is available as a single page for printing.

This library implements the Actor model for Smalltalk, drawing heavily on the design of Erlang and its standard library, OTP.

As such, it brings Smalltalk into the Process-based family of Actor-style languages1.

Processes and Behaviors

Each Actor is a Smalltalk Process with an ordinary Smalltalk object as its Behavior.

Messages, Proxies, RPC, Promises, Timeouts and error handling

Actors send each other messages via Proxies. Messages may be synchronous (RPC) or asynchronous (one-way). Request objects represent both cases. Synchronous calls can be made to block, or to return a Promise of a future reply. Exception handling and timeouts and timers are fully integrated.

Borrowing from Erlang/OTP, actors may be linked to each other. Failures (uncaught exceptions or other crashes) propagate along these links. This gives a robust approach to error handling in a concurrent setting.

Links (and the related monitors) are the foundation for Supervisors, another Erlang/OTP idea. A Supervisor manages the lifecycle of a collection of actors, starting and stopping them and restarting them according to a configurable policy when they fail.

Socket and GUI support

Erlang-inspired TCP/IP server and client socket support is included. Some support for Morphic-based GUI programming with Actors is also provided.

Tracing and debugging

A tracing facility is built in to the message-passing mechanism. An “event tracer” receives fine-grained notifications of important steps in the routing, queueing and delivery of requests, replies and exception messages. It also receives notifications of actor lifecycle events.

  1. The following paper is an excellent survey of the different families of the Actor model:

    “43 Years of Actors: a Taxonomy of Actor Models and Their Key Properties”, Joeri De Koster, Tom Van Cutsem, and Wolfgang De Meuter, Proc. AGERE Workshop, 2016. ACM metadata. Tech Report version (PDF).

    In addition, I have written an informal article on the history of the Actor model

Behaviors

Every instance of Actor has a behavior object associated with it.

Receiving and processing messages

Every user-level message sent to an Actor must be a request carrying a Message instance. The message is sent to the actor’s behavior object, and the reply is relayed to the calling actor.

Methods taking blocks or ActorProxy values often demand special treatment; see the section on the weaknesses of the library’s design for more information.

Ways of responding to messages

Simple replies

Methods on a behavior object can simply return their result, like any other method on any other object, and it will be relayed to the caller.

Replying with a promise

They may also choose to return a promise of an eventual answer. The caller will get their reply when the promise resolves or is rejected.

Suspending the caller

Finally, they may suspend the decision about how to reply to the caller.

Calling the method Actor class >> #caller retrieves and detaches the request object that the actor is working on right now. The actor can then store the request in a variable, making its reply using #resolveWith: or #rejectWith: later. Calling Actor class >> #caller a second time will return nil, since the request was detached on the first call.

The actor only automatically replies to a request if Actor class >> #caller has not been called. Making a call to Actor class >> #caller signals that the request will be taken care of manually.

The Barrier tutorial shows an example of this technique.

Class ActorBehavior

Any Smalltalk object can serve as a behavior object, but inheriting from ActorBehavior offers a number of convenient features:

  • ActorBehavior class >> #spawn is a convenient abbreviation for Actor class >> #bootProxy:. See ways of constructing an actor.

  • ActorBehavior >> #log: and #logAll: produce log message events using the tracing mechanism. This allows log messages to be recorded in the correct order with respect to surrounding events when tracing is active. (By default, these messages go to the Transcript; see the section on tracing for details.)

  • ActorBehavior >> #changed, #changed: and #changed:with: ensure that dependents of the behavior are updated in the UI process, rather than directly in the actor’s own process. This is important not only because Morphic dependents often rely on executing in the UI process, but also for robustness. If any of the update methods signals an exception, the offending dependent is simply removed, rather than killing the actor. An exception from one update method therefore will not prevent the other dependents of the changed object from running.

Error handling

During its execution, if an ActorProcess or any method of an Actor’s behavior object signals an uncaught exception, the actor is terminated permanently.

By default, the system debugger is not invoked when such a crash occurs. Instead, the stack trace of the exception is logged to the standard error and to the Transcript. All of this can be configured.

When an actor terminates, its links and monitors are triggered. This happens for both normal and abnormal termination.

Exit reason

Every ActorProcess has an exitReason instance variable (accessible via the #exitReason message) that is set when the actor is terminated. It is set when the actor terminates normally, as well as when it terminates because of an uncaught exception.

The exit reason can take on many different values:

  • nil indicates “normal” termination.
  • An Exception indicates an uncaught exception.
  • An instance of ActorTerminated indicates termination caused by a linked peer’s termination.
  • Any other value can be supplied when explicitly terminating an actor.

An actor can be terminated in three different ways:

  • Process >> #terminate sets exitReason to be nil.
  • ActorProcess >> #terminateWith: sets exitReason to the argument of the message.
  • ActorProcess >> #kill simulates an uncaught generic Error exception in the actor.

ActorTerminated

Instances of ActorTerminated represent a chain of actor terminations propagating through links. An actor that signals an uncaught exception will be terminated with the exception as its exit reason; actors linked to that will be terminated with an ActorTerminated as the exit reason, with its actor field the original signalling actor and its exitReason field the original signalled exception; and actors linked to those will be terminated with an exit reason that adds another ActorTerminated to the chain; and so on.

As exit reasons propagate across links, the use of ActorTerminated rather than just the exception value alone allows the program or programmer to identify the actor that originally crashed.

Links and Monitors

Erlang pioneered two important additions to the Actor model: links and monitors.

Links and monitors offer a mechanism for failure-signaling and error propagation that works well in a concurrent system, unlike the stack-based approach of normal exception-handling.

They allow an actor to keep track of the lifecycle of another actor. That is, an actor receives a message when a linked or monitored actor terminates.

Exit reasons propagate through links and monitors. Each message describing the termination of a linked or monitored actor includes the actor’s exit reason value.

More information on links and monitors in Erlang:

A link is a bidirectional, symmetric relationship between two actors. If one actor links itself to another, the other becomes linked to the one.

When an actor terminates, a link activation message is sent along each link connecting it to a peer. The message carries the identity of the terminated actor and its exit reason.

Links may be established at boot time, by using #spawnLink, #bootLinkProxy:, #boot:link:, or any of the other constructors mentioning linking, or at any time thereafter, by calling ActorProcess >> #link.

A link may be established to a terminated process: this will cause a link activation message to be enqueued immediately.

Links can be removed by calling ActorProcess >> #unlink. Be warned that a link activation message may be enqueued but not yet processed at the time unlink is called! Actors must be prepared to handle this situation.

Idempotency

Adding a link and removing a link are both idempotent actions.

A link activation report message will by default terminate the receiving actor. This gives an easy method for making an entire group of actors “live and die together”.1

The exception to the rule is for Actor instances that have behavior objects that respond to #linkedPeer:terminatedWith:. In this case, that method is called with the identity of the newly-terminated peer and its exit reason, and the actor is not automatically terminated. The actor is still free to call #terminate or #terminateWith: when appropriate.

Note that Erlang treats “normal” exits specially. If an Erlang process exits with normal exit reason, none of its links are activated, and it exits without terminating linked peers. My experience with Erlang programming has led me to believe that Erlang’s default is perhaps not quite right: when I link to something, I want there to be a consequence when it exits, no matter whether the exit is considered “normal” or not. Therefore, in this library, I have chosen not to special-case “normal” exits; they are treated like any other exit, and always cause link activation.

Monitors

A monitor is a unidirectional, asymmetric relationship from a monitored actor to a monitoring actor.

Each monitor includes a reference object that can be used by a monitoring actor to tell monitors apart. A single actor can monitor a particular peer any number of times, so long as each monitor is created with a different reference object.

When an actor terminates, a monitor activation message is sent for each monitor installed on the actor. The message carries the identity of the terminated actor and its exit reason.

Adding a monitor

Monitors are installed with

anActorProcess monitor: aReference.

The calling actor will be informed when the receiver, anActorProcess, terminates.

Actors must use unique reference objects. Monitors are stored internally in a Dictionary keyed by reference object. If a particular reference object is passed to monitor: by two different actors, the second call will overwrite the results of the first call.

A monitor may be installed on a terminated process: this will cause a monitor activation message to be enqueued immediately.

Removing a monitor

The ActorProcess >> #unmonitor: method removes a previously-added monitor, keyed by the reference object supplied as the argument. Be warned that a monitor activation message may be enqueued but not yet processed at the time unmonitor: is called! Actors must be prepared to handle this situation.

Idempotency

Adding a link and removing a link are both idempotent actions, modulo the caveat about unique reference objects above.

Handling monitor activation

A monitor activation message will by default invoke the #peer:monitor:terminatedWith: of the actor’s behavior object, if any, and if it responds to that selector, and will otherwise do nothing.

In particular, while a link will by default terminate the receiving actor, a monitor will never automatically terminate the receiving actor.

The arguments to #peer:monitor:terminatedWith: are the identity of the newly-terminated peer, the reference object associated with the activated monitor, and the terminated peer’s exit reason.

  1. This idea is also known as fate sharing. See David Clark’s paper,

    “The Design Philosophy of the DARPA Internet Protocols”, David D. Clark, ACM SIGCOMM Computer Communication Review, 1988. ACM metadata. PDF.

Morphic GUI

The library offers some support for using an actor as a model for Morphs in Squeak’s Morphic GUI systems.

A Morph can be configured to use an ActorProxyModel as its model, meaning that when information is needed from the model for UI display and interaction, requests are sent to an actor.

Morphic support is experimental.

I am a novice when it comes to Morphic programming. I am no doubt making incorrect assumptions about how it is supposed to work. I’d very much appreciate some help in improving the interaction between Morphic and actors.

Challenges in integrating Morphic with Actors

Morphic really isn’t set up for an actor-style approach to concurrency.

The main problem is that morphs and model objects must run in the UI process, because some of the interplay between morphs and their models is synchronous.

Dependents protocol

In particular, there’s the back-and-forth dance between changed: and update: that one must remain painfully aware of.

Normal Morphic models use changed to cause a synchronous notification to be delivered to dependents (i.e. morphs), in the form of calls to their update: methods. This causes problems for using an actor as a model in two ways:

  1. The call to update: has to happen on the UI process, while the call to changed must run on the actor’s own process.
  2. The calls to update: cannot be synchronous in an actor system, because the view may wish to call back into the actor to read its updated state, and if the actor is blocked waiting for a reply to the update: message, deadlock will occur.

Synchronous UI actions

It is quite easy to lock up the user interface while developing. The interrupt key (Alt-.) is helpful when this happens. However, don’t rely on it: from time to time you will simply have to kill and restart your VM. Save often.

Model callbacks invoked by morphs are synchronous, waiting for an answer from the model before the UI becomes responsive again. For example, clicking on a PluggableButtonMorph causes an “action” selector to be invoked on the morph’s model, and the UI pauses until that call returns.

A similar, simpler example is the kind of lockup that can occur if you run a blocking do-it in a Workspace. For example, the following creates a new actor, and immediately invokes its halt method in the actor’s process. The UI process then waits for the reply. Because the code uses blocking, the UI process will wait forever for the Promise to be resolved.

ActorBehavior spawn blocking halt.

However, the implementation of halt queues a request to the UI process to open a debugger—but the UI process is waiting for the actor to finish before it checks its work queue.

Pressing Alt-. interrupts the wait, bringing up a debugger on the do-it and allowing the actor’s halt debugger to open.

In addition, I have implemented heuristic support for preemptively interrupting the UI process when a halt happens in a situation where it is likely that the UI process is waiting for the thing that has halted.

Perhaps (post-hoc: following the logic that exceptions traverse links) a halt should halt all causally dependent processes as well? Ultimately, new tooling will be needed to properly show the branching contexts that arise once there are cross-process dependencies.

Example

Class DemoSocketTerminal implements a simple TCP/IP terminal program that can connect to an arbitrary host and port. Input from the server is displayed in the main panel, and input from the user is accepted in the lower input text field.

DemoSocketTerminal screenshot

The implementation of DemoSocketTerminal relies on a component called TerminalOutputMorphActor, which backs a PluggableTextField with an actor. That actor in turn accepts appendText: messages. A TerminalOutputMorphActor is the implementation of the main panel in each DemoSocketTerminal.

DemoSocketTerminal structure

In the diagram above, on the left we see ordinary Morphic structure, with a PluggableSystemWindow having five submorphs. Four of those submorphs, and the window itself, have an ActorProxyModel connected to the DemoSocketTerminal actor as their model. The remaining morph, the large output panel, has an ActorProxyModel connected to the TerminalOutputMorphActor as its model.

When connected, the DemoSocketTerminal has an associated SocketActor in its sock instance variable, which in turn has a pair of worker actors for performing blocking reading and writing actions.

The double orange lines in the diagram denote links between actors. When any actor dies, all the actors linked to it are terminated automatically.

Actors inheriting from MorphActor, as both TerminalOutputMorphActor and DemoSocketTerminal do, enjoy a postExitCleanup: method which abandons their associated morph. Hence, if the DemoSocketTerminal actor terminates, its window is automatically closed.

ActorProxyModel

ActorProxyModel is a subclass of Model, part of the Morphic user interface framework.

Its purpose is to act as a model for a morph, relaying Morphic callbacks to an actor, thus reconciling the tension between Morphic models needing to be run in the UI process and Actors running in separate processes.

Instances of ActorProxyModel are normally constructed by instances of MorphActor.

The approach of using an ActorProxy (or a BlockingTransientActorProxy) as the model for a morph fails for a few reasons:

  1. an ActorProxy returns Promises for callback invocations, which Morphic is not expecting;
  2. a terminated Actor invoked via its ActorProxy yields Promise rejections and signalled BrokenPromise exceptions, which Morphic doesn’t expect; and
  3. a BlockingTransientActorProxy waits forever for a reply, which can lead to UI deadlock.

These reasons motivate the existence of ActorProxyModel. Instances of ActorProxyModel directly handle reasons 1 and 3 by way of their proxySend: method, and handle reason 2 by way of their proxyStub: method and related functionality.

Relationship between ActorProxyModel and its Actor

Every ActorProxyModel holds an ActorProxy for its actor in its proxy instance variable.

Furthermore, every ActorProxyModel installs itself as a dependent of its actor, via proxy addDependent: self. When the actor calls changed on itself, this will trigger the ActorProxyModel’s update methods.

In turn, the update: and update:with: methods on ActorProxyModel are relayed to its own changed: and changed:with: methods, relaying the change notification to the dependents of the ActorProxyModel, such as its associated morphs.

Synchronous requests to the actor

When morphic calls methods on the actor, it blocks waiting for the reply. In order to avoid some cases of UI lockup, by default ActorProxyModel makes such calls with a five-second timeout.

Stub behavior after actor termination

In some cases, it is necessary for an ActorProxyModel to respond to requests after its associated actor has terminated. Actors may install an ad-hoc dictionary of behavior by calling ActorProxyModel >> #proxyStub:. Following a call to proxyStub:, the ActorProxyModel will no longer invoke its actor, instead preferring to call blocks in the ad-hoc dictionary to respond to morphic model methods.

An example of this can be seen in senders and implementors of MorphActor >> #buildProxyStub.

MorphActor

MorphActor is an Actor behavior responsible for acting as a model to a Morph, mediated by the ActorProxyModel held in the model instance variable.

A MorphActor’s associated Morph(s) have model as their model. The model then delegates their requests to the MorphActor, taking care of timeouts, promises, and so forth.

Subclass MorphActor to add application-specific functionality. For example, both DemoSocketTerminal and TerminalOutputMorphActor are subclasses of MorphActor.

Subclasses should implement:

  • buildSpecWith: (mandatory), to produce a build specification for constructing their main Morph. See MorphActor >> #open for the use of buildSpecWith:.
  • customizeMorph:builtWith: (optional), to invoke methods on newly-constructed morphs that were not catered for in the pluggable-spec API.
  • buildProxyStub (optional), to provide custom behavior in case the actor terminates but morphic still needs to access the model in the moments before the view is destroyed.

The postExitCleanup: method of MorphActor destroys its main Morph when the actor terminates.

Other actor systems

There do not seem to be very many other implementations of the actor model for Squeak.

Actalk

The Actalk system dates back to 1989, and was subsequently ported to Squeak. A version that can be loaded in to Squeak is still available on SqueakMap:

Installer squeakmap update; install: 'Actalk'

The system is described in a paper:

“Actalk: a Testbed for Classifying and Designing Actor Languages in the Smalltalk-80 Environment”, Jean-Pierre Briot, Proc. ECOOP, 1989. Full ECOOP’89 conference proceedings. PDF (scan of proceedings). PDF (alternate version).

While I only have a shallow understanding of Actalk, it seems that differences between Actalk and this system include:

  • our Actor class subclasses Process
  • we use Promises in a pervasive convention for RPC to Actors
  • we allow any object to be the behavior of an Actor
  • we offer links and monitors

Squeak-E and Raven

Research into object capability systems has led to a number of implementations of languages and libraries inspired by Mark Miller’s E language:

“Robust Composition: Towards a Unified Approach to Access Control and Concurrency Control”, Mark Samuel Miller, PhD. dissertation, Johns Hopkins University, 2006. Dissertation resource page. PDF.

In particular, Squeak-E is “a project putting E concepts into Squeak”.

The design and goals of E and Squeak-E go far beyond the ambition of this library. In particular, Squeak-E aims to support a concept called refraction which would not only allow secure actor-style interaction among parties that do not necessarily trust one another, but also secure reflection, management, monitoring, debugging and administration of such a system. Because our library subclasses Process but does not otherwise change the VM or any other aspect of the image, it cannot provide such features.

Another major difference is in the vat concept of E. A vat is analogous to an actor in our system, but is like a small segment of a running image, containing multiple distinct and individually addressable objects. Where in our system an ActorProxy denotes an actor, in Squeak-E (and E), a reference denotes an object within a vat, rather than denoting an entire vat. This allows for a more flexible, fine-grained style of programming with concurrent objects.

Finally, E has special syntactic and language support for working with promises of various kinds, while Squeak and our library implement Promises as an ordinary objects in the system.

The original Squeak-E implementation was not integrated with the core of Squeak, and does not seem to be available any more.

However, in recent months, Henry House has been working on reviving the Squeak-E ideas, based on some of the original code, in a project called Raven, available as part of the Cryptography project on SqueakSource.

Processes

ActorProcess hierarchy Each Actor is a Smalltalk Process. There are two important subclasses of Process: ActorProcess, which implements a subset of the Erlang process model; and Actor, which goes a step further, adding a convention for using Message objects for RPC and using ordinary objects as actor behaviors.

Most programs will use Actor rather than ActorProcess.

No process isolation is implemented. This is a big difference from languages like Erlang. All Smalltalk objects coexist in a single mutable shared heap. This means that it is very easy to accidentally pass mutable objects between actors. See below.

Actor: Concurrent Smalltalk objects

Every Actor’s behavior is specified by a distinct behavior object, usually (but not always!) a subclass of ActorBehavior. Because Actor is a subclass of both ActorProcess and Process, it inherits the public interfaces of both. See the section on ActorProcess below for details.

Creating Actors

ActorBehavior subclass: #SimpleTestActor
              instanceVariableNames: ''
              classVariableNames: ''
              poolDictionaries: ''
              category: 'Actors-Tests'.

a := SimpleTestActor spawn.
    "an ActorProxy for an Actor (92060) on a SimpleTestActor"

a := Actor bootProxy: [ SimpleTestActor new ].
    "Equivalent to the previous line"

Actors are created with Actor class >> #boot: and friends, or with the convenience method ActorBehavior class >> #spawn, if the desired behavior object is an ActorBehavior.

If boot: is used, the result is a (running) Actor instance.

If spawn (or Actor class >> #bootProxy:) is used, the result is an ActorProxy instance, which automatically performs many of the parts of the RPC protocol that Actor instances expect. See the section on proxies.

See below for a complete list of available constructors.

Sending requests

Given an ActorProxy for an Actor, requests can be sent with ordinary message syntax. They are handled by methods on the behavior object. In this example, a’s behavior object is a SimpleTestActor, which has the following method on it:

SimpleTestActor >> addOneTo: aNumber
    ^ aNumber + 1

This allows us to send requests like this:

a addOneTo: 1.        "a Promise"
(a addOneTo: 1) wait. "2"

By default, requests will be synchronous, yielding a promise for the eventual result.

The ActorProxy methods async, sync or blocking select alternative behaviors:

a async addOneTo: 1.    "nil"
a sync addOneTo: 1.     "a Promise" "(like the default)"
a blocking addOneTo: 1. "2"

See the section on interaction patterns for more information on why and when you might want to use each of these variations.

Under the covers, a proxy builds an ActorRequest instance and sends it as a message to the proxy’s actor. If programs directly use Actor >> #sendMessage:, they must do the same.

Of course, any uncaught exception from the behavior object causes immediate, permanent termination of the actor, and rejection of all outstanding and future requests.

Given the following method:

SimpleTestActor >> divideOneBy: aNumber
    ^ 1 / aNumber

The following request will cause a to crash:

a divideOneBy: 0. "a Promise"

The resulting promise will be rejected, with an ActorTerminated bearing the ZeroDivide exception as an error value. All subsequent requests to the now-dead actor will also be rejected with a similar ActorTerminated object. See the section on error handling for more details.

If the promise is waited for, a BrokenPromise exception will be signalled:

(a divideOneBy: 0) wait.   "Signals BrokenPromise"
a blocking divideOneBy: 0. "Signals BrokenPromise"

Implementing a behavior

Behaviors must take care to distinguish between three important objects:

  • self is the behavior object, whose methods are invoked by its corresponding Actor instance.

  • Actor current is the currently-executing Actor instance.

  • Actor me is an ActorProxy for the currently-executing Actor.

A behavior may freely invoke methods on self, but must take care when performing RPC using ActorProxy >> #blocking or Promise >> #wait, lest it deadlock: while an actor is blocked, waiting for a reply to an RPC request, it does not process incoming requests.

If a method on a behavior object returns self, the request that led to the method call is answered with Actor me in place of the behavior object. No other translation of request messages, reply messages, or exception values takes place as they travel back and forth between actors. See the section on weaknesses for more information.

Terminating an actor

An actor’s behavior object may perform a “normal” exit via

Actor current terminate.

This terminates the currently-executing actor with nil as an exit reason. Alternatively, any uncaught exception terminates the actor abnormally with the exception as its exit reason:

Actor current kill.   "Terminates with a generic exception."
self error: 'Oh no!'. "Any other exception will work."
1 / 0.                "Ordinary exceptions do the same kind of thing."

Whenever an actor terminates for any reason, normally or abnormally, all its outstanding requests are rejected, as are any requests that may be sent to it in future.

An actor may be terminated from the outside, as well: if an actor holds p, an ActorProxy for another actor, it can cause the other actor to terminate abnormally by executing

p actor kill.

Note that kill is a method on Actor, not a method on p’s behavior object. See the section on proxies for more information on the actor method of ActorProxy.

Cleaning up associated resources

If an actor’s behavior object responds to postExitCleanup:, that method is called after the actor has terminated. The argument passed to the method is the exitReason of the terminating actor.

The method runs in a fresh, temporary process, not in the actor’s own process. By the time of the call to postExitCleanup:, the actor’s own process is guaranteed to have terminated. See ActorProcess >> signalExit.

ActorProcess: Erlang-style processes

Instances of ActorProcess implement a “process style” actor, in the terminology of De Koster et al.. Specifically, they implement a subset of the Erlang approach to the actor model, providing

  • a main process routine;
  • a “receive” operation;
  • a form of selective receive;
  • distinct system-level and user-level messages; and
  • Erlang-style “links” and “monitors”.

They are more general than Actor instances, in that they do not enforce any particular convention for the user-level messages exchanged by the actor. However, they are awkward to use directly.

It is almost always better to use Actor with a custom ActorBehavior object instead of using ActorProcess directly.

The main process routine

A plain ActorProcess (as opposed to an Actor) is started with ActorProcess class >> #boot: and friends:

a := ActorProcess boot: [ "... code ..."
                          msg := ActorProcess receiveNext.
                          "... more code ..." ].
a sendMessage: 'Hello!'.

Like any other Process, an ActorProcess can be terminated. Any time an ActorProcess terminates, normally or abnormally, its links and monitors fire, letting interested peers know that it has died.

If an uncaught exception is signalled by the main process routine, the actor is terminated permanently. See the section on error handling for more details.

The currently-executing Actor or ActorProcess instance can be retrieved via Actor class >> #current, and an ActorProxy for the currently-executing actor can be retrieved via Actor class >> #me.

Actor current. "an Actor (79217) on a SimpleTestActor"
Actor me.      "an ActorProxy for an Actor (79217) on a SimpleTestActor"

System-level and user-level messages

Messages exchanged among ActorProcess instances come in two kinds: system- and user-level.

System-level messages are a private implementation detail. They manage things like links and monitors, and a specific type of system-level message carries user-level messages back and forth. See senders of performInternally: to discover the types and uses of system-level messages. System messages are comparable to (and inspired by) Erlang’s system messages.

User-level messages are the things sent by ActorProcess >> sendMessage: and received by ActorProcess class >> #receiveNext. They are a public aspect of working with Actors.

Programmers design their Actors in terms of the exchange of user-level messages, and never in terms of system-level messages.

While any object can be sent as a user-level message between Actors, the convention is that instances of ActorRequest are the only kind of user-level message exchanged.

“Internal” and “External” protocols

Unlike ordinary Smalltalk objects, which have public methods and private methods, ActorProcess instances have three kinds of method:

  • public, “external” methods, for use by other actors and processes;
  • public, “internal” methods, for use only by the actor itself;
  • and ordinary private methods, part of the actor system implementation.

External methods include sendMessage:, kill, terminate, isActor and so on. Internal methods include receiveNext, receiveNextOrNil:, and receiveNextTimeout:.

Actor and ActorProcess constructors

There are many different ways to start an actor.

Behavior objects

ActorBehavior spawn.
ActorBehavior spawnLink.
ActorBehavior spawnLinkName: aStringOrNil.
ActorBehavior spawnName: aStringOrNil.

These constructors first instantiate their receiver (a subclass of ActorBehavior), and then pass the result to one of the bootProxy: variations on class Actor.

Actors with a behavior object

Actor bootLinkProxy: aBlock.
Actor bootLinkProxy: aBlock name: aStringOrNil.
Actor bootProxy: aBlock.
Actor bootProxy: aBlock name: aStringOrNil.

These constructors produce Actors having the result of evaluating aBlock as their behavior object. The variations with Link in the name link the new actor to the calling process, and if a name is supplied, it is used when printing the actor and in the Squeak process browser.

Actor for: anObject.
Actor for: anObject link: aBoolean.

These constructors produce Actors with anObject as their behavior object.

Erlang-style processes

ActorProcess boot: aBlock.
ActorProcess boot: aBlock link: aBoolean.
ActorProcess boot: aBlock link: aBoolean name: aStringOrNil.
ActorProcess boot: aBlock priority: anInteger link: aBoolean name: aStringOrNil.

These constructors produce actors running aBlock as their main routine. Generally speaking, aBlock will use Actor receiveNext to explicitly receive and handle incoming user-level messages.

If aBoolean is absent or false, the new actor will not be linked by default to the calling process; if it is present and true, it will be linked to the calling process.

If a name is supplied, it is used as the Process name, which is displayed in the Squeak process browser and anywhere that the ActorProcess is printed.

Actor boot: aBlock.
Actor boot: aBlock link: aBoolean.
Actor boot: aBlock link: aBoolean name: aStringOrNil.
Actor boot: aBlock priority: anInteger link: aBoolean name: aStringOrNil.

Since Actor is a subclass of ActorProcess, it inherits these methods. However, it reinterprets the meaning of aBlock: instead of aBlock enacting the main routine of the new actor, it is expected to return a value that will be used as the behavior object of the new actor, and the standard Actor mainloop (Actor >> #dispatchLoop) will be used as the main routine.

Weaknesses of the design

Smalltalk poses a number of challenges to implementation of the actor model.

As has already been mentioned, chief among them is the total lack of process isolation. This means that, if a mutable object is accessible by two or more running actors, you still end up having to worry about concurrent access to data structures, even though the actor model is supposed to eliminate this as a programming concern.

An example of a distortion induced by this problem can be seen in the use of the copy method in ChatRoom >> #tcpServer:accepted:. When a new user connects to the chat room, they are sent a list of the already-connected users:

agent initialNames: present copy.

The chat room must take care to send a copy of present to the new ChatUserAgent, because of the asynchrony in the system: if it is not copied before being sent, it may change while in flight!

A second weakness is connected to the way a returned self is changed to Actor me. It adds a special case for convenience, but a general solution would ensure greater process isolation by copying (or otherwise specially treating) mutable values.

A third weakness is that block arguments to methods invoked on an actor’s behavior are not translated, which makes custom control flow awkward. For example, consider the following snippet:

b := Actor bootProxy: [ true ].
(b ifTrue: [ 1 ] ifFalse: [2]) wait

Here, b is an Actor with a Boolean as its behavior. Invoking b not works perfectly, but ifTrue:ifFalse: doesn’t work, signalling NonBooleanReceiver. This example demonstrates a leak of some of the optimization-enabling assumptions the VM makes.

A similar case occurs in the following example:

d := Actor bootProxy: [ Dictionary new ].
a async at: 1 put: 2.
a blocking removeKey: 1.  "This works fine..."
a blocking removeKey: 99. "... but this kills the whole actor."

If, instead, we use removeKey:ifAbsent:, we can avoid killing the whole actor, but at the cost of having the ifAbsent: block execute in the wrong context. That is, if it runs, it will run in a context where self is the Dictionary, and Actor current and Actor me denote the actor that the client knows as d. The tutorial on collections as behavior covers this topic in more detail.

Perhaps, in future, a special case for wrapping block arguments in an outer block that causes the block to execute in the correct Actor’s context could be added.

Promises

Squeak includes an implementation of promises, and this library uses them for RPC between actors.

Promises are values representing a “future value or error”. Each promise holds two collections of callbacks: one set to invoke if the promise “resolves” to a value; and another to invoke if the promise is “rejected” with an error.

They are useful for representing eventual completions of asynchronous tasks such as RPC.

Squeak’s promises follow the Promises/A+ specification as far as is possible within Squeak’s unique context.

More information on promises generally:

Promises from interactions with Actors

Every non-asynchronous ActorRequest includes a Promise object (specifically, an ActorPromise). The promise is used by the callee to reply to the caller with either a value or an error, and by the caller to wait for and retrieve the value or error.

If p is a Promise object, then

  • if p isResolved, then p value is its value; otherwise p value is nil.
  • if p isRejected, then p error is its error value; otherwise p error is nil.
  • if p is neither resolved nor rejected, then p is pending.

Waiting for a promise to be resolved or rejected

Use Promise >> #wait, Promise >> #waitTimeoutMSecs:, and ActorPromise >> #waitFor:ifTimedOut: to block until a promise is either resolved or rejected.

  • p wait returns p value as soon as p is resolved, or signals BrokenPromise if p is rejected.

  • p waitTimeoutMSecs: ms waits for at most ms milliseconds for p to become resolved or rejected. It returns true if p is resolved, and false if p is rejected or the timeout expires.

  • p waitFor: ms ifTimedOut: aBlock waits for at most ms milliseconds for p to become resolved or rejected. It returns p value if p is resolved, signals BrokenPromise if p is rejected, or returns aBlock value if the timeout expires.

Adding a continuation to a Promise

Use the operator >>= to attach a continuation to a Promise.

(some operationYieldingAPromise) >>= [:v | v + 1]

In this example, a continuation block [:v | v + 1] is attached to the promise returned by #operationYieldingAPromise—call it “promise A”—and a new promise, “promise B” is returned.

Make sure to use bindActor in conjunction with >>= if your continuation is supposed to run “inside” your actor. See below.

When promise A is resolved with a value, the continuation block is invoked, and its result is used to resolve promise B.

If promise A is rejected, promise B is also rejected (with the error value from promise A).

The >>= operator takes advantage of Smalltalk’s left-associativity of binary operators, allowing “stacking” of continuations:

(some operationYieldingAPromise)
  >>= [:v | v + 1]
  >>= [:v | v * 99]

The >>= operator is punningly, though inaccurately, named after the monadic bind operator seen in some functional programming languages. It’s not quite a bind operator for the same reason that then isn’t a bind operator in Promises/A+. It’s an odd, reflective hybrid of bind and fmap.

Where and when do handlers run?

By default, callbacks registered with a Promise run in the process that is resolving or rejecting the promise.

In an actor system, this is often not the right thing.

The problem

For example, consider the following method on a behavior object:

doSomethingWith: anActorProxy
    (anActorProxy compute)
      >>= [:result | self pvtHandleResult: result]

When the promise returned by compute resolves, the continuation block will execute.

However, the private method of the calling actor will run as part of the called actor’s process! If the calling actor—that is, the one implementing doSomethingWith:—happens to be running at the same time, we have a race condition.

The solution

Use BlockClosure >> #bindActor to transform an 0-ary or 1-ary block into an equivalent object that ensures that the block will run as part of the actor that called bindActor.

A correct version of the incorrect example above:

doSomethingWith: anActorProxy
    (anActorProxy doSomething)
      >>= [:result | self pvtHandleResult: result] bindActor

Could we design a general mechanism that avoids this kind of problem? What about storing an optional Process along with each resolver and rejecter in a Promise instance? If a Process slot is non-nil, we’d have some method like executePromiseCallback: aBlock with: aValue on various kinds of Process that automatically did the right thing. If a Process slot is nil, the callback would be executed on the process that happened to be running at the time.

ActorPromise

Every time a proxy kicks off a “synchronous” or “blocking” RPC request, it constructs a promise to receive the result of the RPC.

It specifically uses instances of ActorPromise, which inherits from the Promise class included with Squeak.

ActorPromise augments the default Promise waiting behavior with special treatment of ActorProcesses, which must continue to receive system-level messages while they wait.

It relies on the fact that just such a system-level message is the means by which each request is answered. This ensures that the waiting ActorProcess is woken up when the promise becomes fulfilled.

Proxies

Instances of ActorProxy override doesNotUnderstand:, perform: and perform:withArguments: in order to provide a convenient way of sending requests to Actors.

Methods that go through doesNotUnderstand: etc. return ActorPromise instances, which can be used to pick up replies.

Switching between an Actor and its ActorProxy

Every Actor has exactly one ActorProxy, and every ActorProxy is associated with exactly one Actor.

Strictly speaking, ActorProcesses also have proxies: see the comment on ActorProcess >> #proxy.

Both Actor and ActorProxy have methods actor and proxy. Calling actor always returns the Actor of a matched pair; calling proxy always returns the ActorProxy.

           anActor actor == anActor.
     anActor proxy actor == anActor.
      anActorProxy proxy == anActorProxy.
anActorProxy actor proxy == anActorProxy.

ActorProxy and dependents

Each proxy implements addDependent: and removeDependent: by asynchronously forwarding them to its backing actor. Similarly, each proxy forwards calls to update: and update:with: to its backing actor.

Transient proxies

ActorProxy instances are intended to be long lived—at least as long lived as their backing actor.

For this reason, ActorProxy inherits from Object, including all the behavior of Object. This causes quite a bit of clutter in the interface of ActorProxy!

For example, consider calling windowTitle on an ActorProxy.

myProxy windowTitle.

The intention here is for the request to be forwarded on to the backing actor’s behavior object. Unfortunately, Object implements windowTitle. The request will thus be answered locally at the proxy, without ever being sent on to its actor.

To avoid this problem, proxy objects allow creation of transient proxy objects which have a much smaller interface and can therefore easily forward more requests than an ordinary proxy can:

myProxy async windowTitle.
myProxy sync windowTitle.
myProxy blocking windowTitle.

The three variants, async, sync, and blocking are discussed below.

Think twice before storing a TransientActorProxy instance in a variable!

The class TransientActorProxy inherits from ProtoObject, not Object. While the interface of ProtoObject is small, it is so small that transient proxies do not play nicely with the inspector, the explorer and other tools in the development environment. This is why they should be considered “transient” or temporary.

Interaction patterns

TransientActorProxy hierarchy The three methods async, sync and blocking on an ActorProxy instance each return instances of a distinct subclass of TransientActorProxy.

Each allows a distinct interaction pattern:

  • AsyncTransientActorProxy offers asynchronous (one-way) messaging;
  • SyncTransientActorProxy offers request/reply/error messaging, using Promises to mediate between caller and callee; and
  • BlockingTransientActorProxy is like SyncTransientActorProxy, but hides the promises away, waiting for a reply at the time the call is made.

Asynchronous calls

someProxy async notifyOfSomeEvent: details

The result of ActorProxy >> #async is an AsyncTransientActorProxy. Messages sent this way do not allocate any promises, and always return nil immediately.

Synchronous RPC

(someProxy sync doSomethingWith: anArgument)
  >>= [:result | self handleResult: result] bindActor.
self whileWeWaitDoSomethingElse.

The result of ActorProxy >> #sync is a SyncTransientActorProxy. Like messages sent to a plain old ActorProxy, messages sent this way allocate and immediately return ActorPromise instances.

These promises are eventually resolved or rejected according to the code executing at the remote actor.

Use >>= and bindActor to attach a continuation to a returned promise. See here for details of >>=, and here for information on why bindActor is so important.

Blocking RPC

result := someProxy blocking doSomethingWith: anArgument.
self handleResult: result.
self nowDoSomethingElse.

The result of ActorProxy >> #blocking is a BlockingTransientActorProxy. Messages sent this way internally allocate an ActorPromise, but do not expose it: instead, they immediately wait for the promise to be resolved or rejected. These messages eventually either return a value or signal BrokenPromise.

There are circumstances where a blocking call will not return, such as when the callee “detaches”, but never replies to, the request it receives.

Requests

While any object can be sent as a user-level message between Actors, the convention is that instances of ActorRequest are the only kind of user-level message exchanged.

Class ActorRequest

An ActorRequest (a “request”) is a triple of a Message destined for a remote behavior (the “message”), a Process interested in the reply (the “sender”), and a Promise by which the reply is to be delivered to the Process (the “promise”).

The sender is not always an ActorProcess. An ordinary Process can send ActorRequests and can wait for eventual replies or exceptions.

A request is not intrinsically targeted at any actor in particular; it does not store any information about the identity of its target.

Asynchronous requests

If the promise is nil, the request is asynchronous and no-one cares about the reply to the eventual evaluation of the message. However, even in this case, the sender is almost always non-nil: an asynchronous request can still usefully have a notion of “sender” associated with it.

Sending requests

Requests can be sent to an actor with ActorRequest >> #sendTo: or the #redirectTo: family of methods. There are also convenience methods Actor class >> #sendRequest:to: and sendAsyncRequest:to:, and of course any instance of ActorProxy builds and sends ActorRequest instances.

Once a request has been sent, its reply may be retrieved via the Promise it holds.

Sending replies or notifying of failures

Ultimately, when a response is ready for a request, it is transmitted by invoking ActorRequest >> #resolveWith: or #rejectWith:, as appropriate. Compare these to similar and similarly-named methods on Promise.

In order to ensure that every request receives an answer, a request sometimes stores the identity of an Actor (in its worker instance variable) that has taken responsibility for the request, so that it can send a default answer if no-one else supplies anything first. The request object takes care to signal the worker whenever someone supplies a reply to it, so that the worker knows it no longer needs to bother with the request.

Sockets

Support for client (connecting) and server (listening) TCP/IP sockets is modeled on Erlang’s gen_tcp library.

Socket support is experimental.

Like Erlang, each connected socket is managed by a separate actor, and every SocketActor is linked to a controlling actor, which receives socket-related events.

Unlike Erlang, credit-based flow control1 is used to manage input from a connected socket, as well as to manage the rate at which new sockets are accepted from a server socket.

Reading from a socket is asynchronous and event-based. Writing to a socket produces a promise which is resolved when the write has been delivered to the socket.

Example

See the TCP/IP Echo Server tutorial for a full client-server example.

In addition, class DemoSocketTerminal demonstrates combining Socket actors with actor-based Morphic programming.

Here is a very simple example, a rough HTTP client that retrieves http://localhost/. After the headers have been read, the client switches from double-linefeed-separated work units to raw chunks of data. At the moment of the change, the read credit amount is zero, ensuring that there is no risk of accidentally reading the wrong data in the wrong mode.

ActorProcess boot: [ | s |
    s := SocketActor connectToHost: 'localhost' port: 80.
    "The new SocketActor takes the current actor as controlling actor."

    s lineTerminator: String crlf. "HTTP protocol requires this."
    s sendLine: 'GET / HTTP/1.0'.
    s sendLine: ''.

    s delimiterMode: String crlfcrlf.
    "This makes the first work item a string up to the header/body break."
    s issueCredit. "Issue one work-unit's credit: this reads the headers."

    s rawMode. "Switch to raw mode for the remainder of the connection."
    s infiniteCredit. "Retrieve it all."
    "NB be more careful about credit, in production use!"

    Actor receiveUntil: [:v |
        Transcript crlf; nextPutAll: (v printStringLimitedTo: 400); flush.
        v message selector = #tcpSocketClosed:reason: ]].

ServerSocketActor

Creating a server socket

ServerSocketActor listenOnHost: interfaceDnsNameString Port: portNumber.
ServerSocketActor listenOnPort: portNumber.

These two methods spawn a fresh ServerSocketActor that starts listening on the given port. If an interfaceDnsNameString is not given, '0.0.0.0' is used, accepting connections on any interface.

The new socket takes the calling actor as its controlling actor, and starts off with zero credit for accepting connections.

Readiness events

controllingActorProxy tcpServer: serverSocketActorProxy ready: aBoolean.

The server socket actor persists in the image until explicitly stopped. As circumstances change, such as the image being stopped and restarted, the underlying listening socket may come and go. It will not always be possible to listen on the configured interface and port number.

Therefore, the socket actor sends its controlling actor a readiness event each time the socket is able to start listening or is forced to stop listening. The aBoolean field in the event will be true when listening is in progress, and false when listening is on hold.

If listening could not be established, and credit for accepting new connections is non-zero, then the server socket actor will keep trying to re-establish its listening socket every few seconds.

Accepting connections

To start receiving connections, call ServerSocketActor >> #issueCredit each time you want to allow an additional connection to be accepted.

For example, you might wish to call issueCredit once at startup time, and then once at the top of the code handling each tcpServer:accepted: event.

Use #issueCredit: aNumber to increment the available credit by aNumber steps in one go.

When the server has credit and a connection arrives, the controlling actor will be sent a #tcpServer:accepted: request:

controllingActorProxy tcpServer: serverSocketActorProxy accepted: socketActorProxy.

The socketActorProxy refers to a SocketActor.

Reconfiguring the listener

To alter the interface, port number, or underlying kernel-level listen backlog size of an already-existing ServerSocketActor, use the following methods:

serverSocketActorProxy listenOnHost: aString port: aPortNumber backlogSize: aBacklog.
serverSocketActorProxy listenOnHost: aString port: aPortNumber.

If not supplied, aBacklog is set to 128.

Shutting down a server socket

serverSocketActorProxy stop.

The ServerSocketActor will terminate, activating its links as usual.

Remember that the actor links to its controlling actor! You will often want to unlink before calling stop:

serverSocketActorProxy actor unlink.
serverSocketActorProxy stop.

SocketActor

Creating a connected socket

There are two ways to create a connected socket: either wait for one to be accepted, or create a new outbound client socket:

SocketActor connectToHost: hostname port: portNumber.

The new socket takes the calling actor as its controlling actor, and starts off with zero credit for receiving data.

Connection events

controllingActorProxy tcpSocketConnected: socketActorProxy.

Issued when a client socket has connected.

Only issued for new, outbound client connections: when a connection is accepted from a server socket, the tcpServer:accepted: event takes the place of this event.

controllingActorProxy tcpSocketClosed: socketActorProxy reason: anExceptionOrNil.

Issued when the socket closes.

Sending data

socketActorProxy send: anObject.

If anObject is a ByteArray, sends the raw bytes; otherwise, sends anObject asString, encoded into bytes as UTF-8.

socketActorProxy sendAll: aCollection.

Just like aCollection do: [:x | socketActorProxy send: x], but more efficient.

socketActorProxy sendLine: anObject.

Sends anObject followed by the current line terminator, which defaults to

String lf

The line terminator can be retrieved and set via

socketActorProxy lineTerminator.
socketActorProxy lineTerminator: aString.

Receiving data

Data is read from the socket in work units, which can be either

  • delimiter-separated spans of bytes or characters, or
  • arbitrarily-sized chunks of data read in a single call to the underlying socket.

Credit for reading from the socket is issued in terms of these work units.

For example, if the work unit selected is spans separated by String crlf, then the credit value in the actor will be interpreted as the number of CRLF-terminated lines of input to read from the socket and send to the controlling actor. If the work unit separated is raw binary data, the credit value will just denote the number of chunks to be sent.

The default work unit type is rawMode.

Delimiter-separated work units
socketActorProxy delimiterMode: aString.
socketActorProxy delimiterMode: aStringOrByteArray binary: aBoolean.

Selects delimiter-separated work units.

If aBoolean is not supplied or is false, then UTF-8 decoding will be automatically performed on the input. In this case, aString or aStringOrByteArray must be a String, the delimiter to watch for. Delivered work unit items will be Strings.

If aBoolean is supplied and is true, then aStringOrByteArray must be a ByteArray. No decoding of input will be performed. Delivered work unit items will be ByteArrays.

Arbitrary chunk work units
socketActorProxy rawMode. "Default at actor startup time."
socketActorProxy rawModeAscii.

These select either raw binary mode or raw ASCII mode, respectively. Each time any data is available at all, the entirety of the available data will be delivered as a single work unit to the controlling actor.

For rawMode, delivered work unit items will be ByteArrays; for rawModeAscii, they will be Strings.

Using ASCII mode is almost always wrong.
 
Strongly prefer to either use a delimiter-separated mode, which does UTF-8 conversion for you, or rawMode, with UTF-8 decoding performed in the controlling actor.

Issuing credit
socketActorProxy issueCredit.

Allows the receiver to relay one more work unit from the underlying socket.

socketActorProxy issueCredit: amount.

Allows the receiver to accept amount more units of input from the underlying socket. The amount may be negative, in which case the final credit is clamped to be non-negative.

socketActorProxy infiniteCredit.

Sets credit to infinity, allowing data to be read and delivered as fast as it arrives. This is usually not a good idea. Useful for testing, and in tightly controlled situations, but in the wild this can overwhelm your image with input.

socketActorProxy zeroCredit.

Sets credit to zero, preventing future read work (other than anything that has already completed or is currently running, but hasn’t been fully delivered to the controlling actor yet) until credit is subsequently increased.

Data delivery events

controllingActorProxy tcpSocket: socketActorProxy data: workUnit

This event is delivered to the controlling actor whenever a unit of credit has been used up in receiving a work unit from the socket.

Other events

controllingActorProxy tcpSocketTimeout: socketActorProxy.

Issued when a connection attempt, an attempt to send data, or an attempt to read data times out.

There is currently no way to set a read timeout implemented. Also, Squeak’s send timeout appears to be hard-coded.

Shutting down a connected socket

socketActorProxy stop.

The SocketActor will disconnect (if it was connected) and close and then terminate, activating its links as usual.

Remember that the actor links to its controlling actor! You will often want to unlink before calling stop:

socketActorProxy actor unlink.
socketActorProxy stop.
  1. Surprisingly, “credit-based flow control” doesn’t seem to be a term in common usage outside very narrow circles such as ATM-based networking. However, it is widely known in distributed systems folklore. One example of its use is in the RabbitMQ server internals for managing flows of messages. A more in-depth look at the topic in the setting of ATM networking can be found in chapter 4 of

    “Traffic Management for High-Speed Networks: Fourth Lecture International Science Lecture Series”, ed. H. T. Kung. The National Academies Press, Washington, DC. 1997. DOI.

Supervision

In addition to links and monitors, Erlang pioneered an application of links called “supervisors”.

Support for supervision is experimental.

A supervisor is an actor which holds specifications describing the construction and maintenance of other, supervised (“child”), actors.

When a supervised actor terminates, the supervisor restarts it, following the specification associated with it when it was added to the supervisor.

Erlang has a rich suite of supervisor behaviors; so far, this library includes only a very simple Supervisor class that offers the ability to restart child actors after they terminate, so long as either they terminate normally (with nil exit reason) or they do not terminate abnormally more frequently than a configured limit.

Example

We will create a Supervisor that supervises a DemoSocketTerminal.

s := Supervisor spawn.
s instantiateSpec: [ | a |
	a := DemoSocketTerminal spawn.
	a async open.
	a ].

Executing these commands will lead to a DemoSocketTerminal opening on the screen.

Closing the window leads to a replacement being created after the window has closed. The supervisor is re-evaluating the “specification” block supplied to it each time the supervisee terminates.

s intensity: 2 period: 5 seconds.

This configures the supervisor to terminate itself if its supervisees exceed a restart rate of two restarts within a five-second window.

Closing the DemoSocketTerminal counts as a normal termination; we will have to get creative to simulate an abnormal termination.

Executing the following a few times in rapid succession will do the trick:

s blocking children do: [:a | a kill ].

The result is that, after a few restarts of the “failed” actor, the supervisor itself terminates with a MaxRestartIntensityExceeded exception.

Time and Timers

Sending requests after a delay

If an actor wishes to send itself a message immediately, it can use Actor me.

Actor me log: 'Here''s a message'.

The result is a Promise of an eventual reply.

To send a message at some point in the future, use Squeak’s built-in future mechanism. For example, this will cause the current actor’s behavior to be sent the log: 'Tick!' message in one second (1,000 milliseconds):

(Actor me future: 1000) log: 'Tick!'.

Again, a Promise of an eventual reply is the result.

This causes the message to be enqueued after 1,000 ms have elapsed - it may not be received and processed until later.

The same technique works for sending delayed messages to other actors, too. Here, we send doSomething via the ActorProxy p after a second has gone by:

(p future: 1000) doSomething

The same technique works for asynchronous, synchronous or blocking transient proxies for an actor:

(p async future: 1000) doSomething "Later sends an asynchronous request to p"
(p sync future: 1000) doSomething "Later sends a synchronous request to p"
"NB. the `sync` option is just like '(p future: 1000) doSomething'"
(p blocking future: 1000) doSomething "Later sends a blocking request to p"

Confusingly, in all three cases, a Promise is returned. The promise is resolved in different ways depending on whether async, sync (the default), or blocking is used:

  • For async, the promise is resolved with nil as soon as the asynchronous request is sent on to p’s actor.

  • For sync, the promise (call it “promise A”) is linked to the promise resulting from the synchronous call (“promise B”). Once promise B, in turn, settles, promise A will take on its state, either resolved or rejected.

  • For blocking, the UI process will block until p’s actor replies. Once this happens, the promise will be resolved with the reply value.

The reason the UI process is involved is that Squeak’s delayed-execution mechanism itself always works via the UI process.

Because the delayed message send happens on the UI process, and promise resolution also happens on the UI process, execution of resolution/rejection handlers also happens on the UI process. Make sure to use #bindActor if a resolution handler needs to execute in a particular actor’s context. See here for more information.

Waiting for a reply with a timeout

Promises resulting from invoking a request object are in fact instances of ActorPromise, which augments the built-in Squeak Promise with a new method, ActorPromise >> #waitFor:ifTimedOut:.

anActorProxy someSlowRequest waitFor: 1000 ifTimedOut: [ "..." ]

Sending someSlowRequest to an ActorProxy starts the request process as usual, and the #someSlowRequest method starts running in the other actor. Meanwhile, the calling actor receives an ActorPromise, which is immediately sent #waitFor:ifTimedOut: with a timeout of 1,000ms and a timeout handler block.

The call to ActorPromise >> #waitFor:ifTimedOut: behaves differently depending on what happens. Its result will be

  • the result of someSlowRequest, if that method returns a value before the timeout fires;

  • a BrokenPromise, if the called actor terminates abnormally before the timeout fires; or

  • the result of the timeout handler block, if the timeout fires before any of the other two possibilities come to pass.

The timeout handler block can take any action, including signalling an error, returning nil, or returning any other useful value.

The special case of a timeout handler block returning nil can be written []:

anActorProxy someSlowRequest waitFor: 1000 ifTimedOut: []

Tracing and debugging

Every ActorProcess may have a tracer object associated with it.

Tracers capture interesting events corresponding to important parts of the Actor-style programming model.

Any time an actor is started, it inherits its tracer from its parent. Actors without an associated tracer object of their own use the systemwide tracer object, ActorProcess defaultTracer.

Tracer objects

Tracer objects are called as part of exception handling; message sending, delivery, and interpretation; RPC requesting, replying, and failure; link and monitor registration and activation; and at major points in an actor’s lifecycle. They are also called as part of the logging facility available in ActorBehavior instances.

ActorEventTracer: the default tracer

The default tracer object is an instance of ActorEventTracer, which does nothing with any of the trace events it is sent except for:

  • logging requests, which are sent to the Transcript, and
  • exceptions, which are printed to the Transcript and the standard error stream, and which may optionally trigger the opening of a debugger.

Enabling the system debugger

The default for ActorEventTracer instances is not to debug exceptions; they are only logged. Use ActorEventTracer >> #debugExceptions: to enable debugging:

ActorProcess defaultTracer debugExceptions: true.

ActorEventStreamTracer

Instances of ActorEventStreamTracer inherit from ActorEventTracer.

They respond to trace events by logging them to a configurable collection of streams, often including the Transcript or a standard-error output stream.

  • ActorEventStreamTracer forStderr constructs an instance sending output to the standard error (only).
  • ActorEventStreamTracer forTranscript constructs an instance sending output to both the standard error and the Transcript.

For example, trace events can be logged to the standard error stream, system wide, using

ActorEventStreamTracer forStderr beDefault.

An example of the output of ActorEventStreamTracer can be seen in the “counter” tutorial.

Trace events

Tracer objects are called with the following messages. Implicit in each is the identity of the currently-active actor.

In the following descriptions,

  • aReason is usually an Exception, an ActorTerminated, or nil;
  • aUserMessage is usually an ActorRequest; and
  • anActorish is either an ActorProcess or an ActorProxy.

Logging

traceLogAll: anOrderedCollection.

Triggered in order to print each item in anOrderedCollection to the log output.

Actor lifecycle

traceActorCreated: anActorProcess.
traceActorStarted.
traceNewBehavior: anObject.
traceException: anException.
traceActorStopped: aReason.

The first event, traceActorCreated:, describes the creation of an actor from the perspective of the spawning actor, and describes the newly-spawned actor.

The remainder describe events in the lifecycle of an actor from that actor’s own perspective.

traceLinkAddedTo: anActorProcess.
traceLinkRemovedTo: anActorProcess.
traceLinkedPeer: anActorProcess terminatedWith: aReason.
traceMonitorAddedTo: anActorProcess reference: anObject.
traceMonitorRemovedTo: anActorProcess reference: anObject.
tracePeer: anActorProcess monitor: anObject terminatedWith: aReason.

These two groups of events capture the addition, removal, and activation of links and monitors, respectively.

User-level messages

traceEnqueuedMessage: aUserMessage.
traceDeliveredMessage: aUserMessage.
traceReceiveNextTimeout.
traceRejectedMessage: aUserMessage.

The first event describes the moment when aUserMessage is enqueued for later consumption by the current actor. The second describes the moment it is dequeued, just prior to interpretation.

The third event, traceReceiveNextTimeout, is emitted when a call to ActorProcess >> #receiveNextTimeout: times out.

The fourth event, traceRejectedMessage:, is emitted when an actor dies with pending, unhandled user messages in its queue.

Request handling

traceHandleRequest: anActorRequest.
traceRequest: anActorRequest redirectedTo: anActorish message: aMessage.
traceRequest: anActorRequest rejectedWith: anObject.
traceRequest: anActorRequest resolvedWith: anObject.
traceRequest: anActorRequest sentTo: anActorProcess.

These events relate to ActorRequest instances.

The first is emitted at the moment an actor begins to interpret a received request.

The second is emitted if a request is redirected elsewhere as the result of a use of #redirectTo:.

The third and fourth capture rejection (error) and resolution (reply) of a request, respectively; and the fifth is emitted just before a new request is enqueued for a recipient.

Interactive debugging utilities

The utility ActorProcess class >> #loggingActorNamed: spawns and returns an actor (proxy) which simply logs every user-level message it receives to its tracer. Such an actor can be useful during interactive debugging and exploration of a system.

Tutorials

The easiest way to learn the library is to try it out. There’s no substitute for experimentation in a live image!

The code snippets in these pages cannot be as interactive as the real thing. It can be difficult to get a feel for the system without hands-on experience. Install the system to try it out.

If you prefer to load the completed tutorial classes, rather than building them yourself while following along with each tutorial, you can load the ActorExamples package from SqueakSource:
(Installer squeaksource project: 'Actors') install: 'ActorExamples'

Examples and demos

Besides the tutorial examples below, the library includes a handful of demos and larger examples in the Actors-Demos package.

Tutorials

Basics

The Counter tutorial goes over the basics, with a simple stateful actor.

The Ping-pong tutorial introduces more complex multi-party interaction.

Scheduling and inter-actor continuations

The Barrier tutorial introduces techniques for managing an Actor’s incoming requests. These techniques allow suspension of active requests and replying to requests in a different order than they were received in.

Plain Old Smalltalk Objects

The Collections-as-behavior tutorial covers use of existing Smalltalk objects as Actor behaviors, discussing the benefits, drawbacks and pitfalls of the idea.

Client and Server TCP/IP Sockets

The TCP/IP Echo Server tutorial covers programming with Actors representing TCP/IP connection sockets and listening TCP/IP server sockets.

Writing a “Counter” actor

Our first example is a simple counter.

While developing, it is useful to have open a Process Browser, so that you can see which actor processes are running, and manually terminate them if necessary.
Process Browser  
Open a process browser by choosing “open…”, then “process browser” from the World menu. Don’t forget to enable auto-updating by right-clicking in the Process Browser’s left-hand pane and selecting “turn on auto-update”.

Defining our behavior class

ActorBehavior subclass: #CounterActor
    instanceVariableNames: 'count'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'ActorExamples'

We define an initialize method…

initialize
    super initialize.
    count := 0.

… and count accessor, just as if this behavior object were a normal Smalltalk object.

count
    ^ count.

Constructing an instance

In a workspace, evaluate

a := CounterActor spawn. "an ActorProxy for an Actor (10138) on nil"
a count. "a Promise"

The result of the second expression is a promise of an eventual answer.

We can wait for the promise to resolve with wait.

a count wait. "0"

Alternatively, we can use the blocking convenience method to arrange for wait to be called for us:

a blocking count. "0"

Counting

Add the following two methods to CounterActor.

add: aNumber
    count := count + aNumber.
    ^ count
increment
    ^ self add: 1

Now, in the workspace,

a blocking add: 123. "123"
a blocking add: 123. "246"
a blocking add: 123. "369"

We can also send a one-way, fire-and-forget asynchronous request to the actor:

a async add: 123. "nil"

And we can increment its count by one:

a increment wait. "493"

Notice that increment is implemented in terms of self add:. In a behavior object, self refers to the object itself, not the current actor. Use Actor me to refer to the current actor.

Tracing activity

Evaluate

a actor tracer: ActorEventStreamTracer forTranscript.

Open a Transcript window.

Now, when we interact with the actor, we will see information about which messages are sent where in both the Transcript and on Squeak’s standard error stream.

For example, evaluating

a async increment.
(a add: 123) wait.
a blocking count.

might yield the following trace in the Transcript:

2018-02-17 19:54:17 (82053) traceEnqueuedMessage: an asynchronous ActorRequest from 54210 (increment)
2018-02-17 19:54:17 (82053) traceEnqueuedMessage: a synchronous ActorRequest from 54210 (add: 123)
2018-02-17 19:54:17 (82053) traceDeliveredMessage: an asynchronous ActorRequest from 54210 (increment)
2018-02-17 19:54:17 (82053) traceHandleRequest: an asynchronous ActorRequest from 54210 (increment)
2018-02-17 19:54:17 (82053) traceDeliveredMessage: a synchronous ActorRequest from 54210 (add: 123)
2018-02-17 19:54:17 (82053) traceHandleRequest: a synchronous ActorRequest from 54210 (add: 123)
2018-02-17 19:54:17 (82053) traceEnqueuedMessage: a synchronous ActorRequest from 54210 (count)
2018-02-17 19:54:17 (82053) traceDeliveredMessage: a synchronous ActorRequest from 54210 (count)
2018-02-17 19:54:17 (82053) traceHandleRequest: a synchronous ActorRequest from 54210 (count)

Each actor can have its own ActorEventTracer, and if it spawns other actors, they inherit its tracer. If no special tracer is assigned to an actor, it uses the system-wide default.

Terminating an actor

Our variable a holds an instance of ActorProxy referring to an underlying Actor, which in turn holds a reference to an instance of CounterActor.

If we want to terminate our actor, we must call terminate or kill on the Actor.

a actor kill. "an Actor (82053) on a CounterActor (terminated)"

Since we earlier configured a tracer, the event is logged to the Transcript:

2018-02-17 19:57:44 (82053) traceActorStopped: Error: Killed

And now our actor has become inert. Making requests of it will yield BrokenPromise exceptions:

p := a count.
p isKindOf: Promise. "true"
p isResolved. "false"
p isRejected. "true"
p error. "an ActorTerminated for an Actor (82053) on a CounterActor (terminated) with Error: Killed"

Even the rejected call to count generates a Transcript message:

2018-02-17 20:00:32 (54210) traceRejectedMessage: a synchronous ActorRequest from 54210 (count)

If we synchronously wait for the result of a Promise resulting from a call to a terminated actor, a BrokenPromise exception will be signaled.

a count wait.     "signals BrokenPromise"
a blocking count. "signals BrokenPromise"

BrokenPromise: Promise was rejected

Ping-pong

This tutorial introduces different interaction patterns by way of two peers exchanging messages in a back-and-forth, “ping-pong” pattern.

It also touches on linking actors together.

Synchronous version

Our first example will “ping-pong” back and forth between two actors. The “ping” step will be a request from a PingPong1 to a CounterActor (from the previous tutorial), and the eventual reply from the CounterActor to the PingPong1 will be the “pong” step.

Defining our behavior class

We do not need any instance variables for our PingPong1 actor. It is stateless.

ActorBehavior subclass: #PingPong1
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'ActorExamples'

Defining our main method

Invoking increment: aCounterActorProxy times: anInteger causes anInteger RPC calls to be issued to aCounterActorProxy.

increment: aCounter times: count
    count = 0 ifTrue: [^self].
    self logAll: 'Synchronously pinging ', aCounter printString.
    aCounter increment >>= [ :incrementedValue |
        self increment: aCounter times: count - 1 ] bindActor.

Notice use of the >>= operator. Because aCounter is a proxy, invoking methods on it results in a promise. The >>= operator on a promise adds a continuation to it: when the promise is eventually resolved, the continuation is invoked with the resolved value of the promise. The call to bindActor on the continuation block ensures that the code inside it will run in the context of the PingPong1 actor.

It looks like the code is making a recursive call to itself, but this is not what is happening, and you will not see more than a single increment:times: context on the PingPong1 actor’s stack at once.

After >>= finishes attaching the continuation block to the promise returned by increment, the method simply continues executing. Since it is at the end, it returns self, as usual for Smalltalk.

This means that the original call to increment:times: completes well before the resulting chain of ping-pong RPC calls has finished. By attaching a continuation to a promise and allowing the main thread of execution to continue, the actor returns to responding to new requests, even while a “background” task is continuing in some other actor.

Running the example

First, create the two actors that will interact with each other:

actor1 := PingPong1 spawn.
actor2 := CounterActor spawn.

Now, open a Transcript window. Then, start the interaction:

actor1 increment: actor2 times: 5.

You should see output like the following in your Transcript.

Transcript PingPong1

Finally, terminate the two actors:

actor1 actor terminate.
actor2 actor terminate.

Asynchronous version

Our second example will be similar to the first, but will use asynchronous, one-way messages instead of synchronous RPC. The “ping” and “pong” steps will be symmetrical. Each will be a one-way request from one PingPong2 actor to another.

Defining our behavior class

Again, our actor is stateless, and needs no instance variables.

ActorBehavior subclass: #PingPong2
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'ActorExamples'

Defining our main method

The chief difference between ping:times: here and increment:times: above is the use of async. A one-way request doesn’t return a promise, since no reply (or exception) will be forthcoming. Also, a one-way request is asynchronous, so execution continues on after sending the next message to peer, and the method completes normally, freeing the actor up to handle the next incoming request.

ping: peer times: count
    count = 0 ifTrue: [^self].
    self logAll: 'Asynchronously pinging ', peer printString.
    peer async ping: Actor me times: count - 1.

Running the example

First, create the two actors that will interact with each other:

actor1 := PingPong2 spawn.
actor2 := PingPong2 spawn.

Now, open a Transcript window. Then, start the interaction:

actor1 ping: actor2 times: 10.

You should see output like the following in your Transcript.

Transcript PingPong2

Finally, terminate the two actors:

actor1 actor kill.
actor2 actor kill.

Linking

We can arrange for our two interacting actors to terminate together—fate sharing—by using the Actor >> #link method.

Add the following method to PingPong2:

linkTo: anActorProxy
    anActorProxy actor link.

Usually, linking is a private implementation decision of a behavior object. It is unusual to see outsiders supply actors to link to. The linkTo: method would be unlikely to exist in a real program.

As explained on the page about proxies, every actor has a “proxy” that takes care of the details of marshalling requests to be sent to it, and every proxy has an associated actor. The main difference between them is that messages sent to the proxy are converted into requests and sent as inter-actor messages to the actor, which will then invoke them on the actor’s behavior object; whereas messages sent to the actor, the instance of Actor, will directly trigger behavior on the Actor object itself.

If you like, the proxy is the “user-level” object that is for everyday use interacting with an actor’s behavior, and its actor is for meta-level or reflective operations on actors, interacting with an actor’s process.

Here, we want to link two processes together, so that when one terminates, the other does as well, and so we invoke the link method on an Actor. If we had sent link to the proxy, it would have looked for a method called link on anActorProxy’s behavior object, which is unlikely to exist.

After creating the actors,

actor1 := PingPong2 spawn.
actor2 := PingPong2 spawn.

we link them to each other:

actor1 linkTo: actor2.

Now, if one dies, so does the other.

Open a process browser, and check to see that both actors are there. Now, kill actor1.

actor1 actor terminate.

Check on the process browser (you may need to refresh it, or turn on auto-updating), and see that neither of the actors remain.

In your workspace, invoking “print it” on actor1 and actor2 will similarly show that they have both terminated:

actor1. "an ActorProxy for an Actor (24140) on a PingPong2 (terminated)"
actor2. "an ActorProxy for an Actor (10267) on a PingPong2 (terminated)"

Barrier

This tutorial introduces some more sophisticated control flow, achieved through postponing transmission of replies to requests.

BarrierActor

ActorBehavior subclass: #BarrierActor
    instanceVariableNames: 'waiters'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Actors-Demos'

Class BarrierActor is a simple ActorBehavior that implements a barrier.

Clients may call #barrierWait, in which case they will not receive a reply until some other client calls #releaseWaiters or #releaseWaiters:.

Any value supplied to #releaseWaiters: is used as the resolved value of waiting clients’ promises; #releaseWaiters supplies nil as this value.

The instance variable waiters holds an IdentitySet of ActorRequests, the waiting continuations.

Initialization

initialize
    super initialize.
    waiters := IdentitySet new.

Waiting for a value

barrierWait
    waiters add: Actor caller.

Retrieving the current request by invoking Actor class >> #caller suspends the remote caller of the actor until either resolveWith: or rejectWith: is called on the resulting ActorRequest object.

Releasing the current set of waiters

releaseWaiters: anObject
    waiters do: [ :c | c resolveWith: anObject ].
    waiters := IdentitySet new.

Each of the waiting requests is released, with anObject as the reply value given to the suspended caller via the promise associated with the request.

releaseWaiters
    self releaseWaiters: nil

Running an example

Exploring the behavior of BarrierActors in a workspace must be done with some care because of the way calls to barrierWait block until a releaseWaiters: arrives from another actor.

Instead of using blocking RPC, we will use Promises and Squeak’s explorer.

Creating a BarrierActor and some waiting promises

First, in a workspace, spawn a BarrierActor.

b := BarrierActor spawn.

Then, type in

b barrierWait

but instead of choosing “do it” or “print it”, right click on that line and choose “explore it” (or press Shift-Alt-I).

Select the workspace window again, and choose “explore it” on the b barrierWait line a second time.

Your screen should now look something like this:

Barrier World 1

Exploring the state of the BarrierActor

At this moment, the two Promises waiting for barrierWait to return are associated with two instances of ActorRequest held in the waiters instance variable of the BarrierActor.

If we “explore it” on b in our workspace, and drill down to see the behavior object, we can see the two requests sitting where they should:

BarrierActor Explorer

Releasing the waiters

Finally, “do it” with the following expression in the workspace:

b releaseWaiters: 1234.

Both the promises have now changed—though the explorers do not automatically update!

To update the view, click on the triangle next to “root” in each explorer, to collapse the view of the promise, and then click a second time, to reopen it.

After this, both explorer views on the promises should look like this:

Fulfilled barrierWait promise

Cleaning up

Finally, we can terminate our BarrierActor.

b actor terminate.

Collections as behavior

This tutorial covers use of ordinary Smalltalk objects not inheriting from ActorBehavior as behavior objects for Actor process instances. It focuses primarily on the problems that might arise in such situations.

An OrderedCollection actor

Let’s experiment with using an OrderedCollection as behavior for an Actor.

a := Actor bootProxy: [ OrderedCollection new ].

We are now able to send messages to our actor, as usual:

a add: 3.
a add: 4.

If we explore our actor using “explore it” on a in our workspace, we see the state of the actor is as follows:

OrderedCollection actor 1

As expected, we see an OrderedCollection(3 4) as the behavior object.

Interactions with actors default to asynchronous:

a sum. "a Promise"

But we can wait for them to complete as usual if we like:

a sum wait. "7"

However, a surprise is in store for us if we try to ask for the size of the OrderedCollection.

a size.

Instances of ActorProxy are not indexable

What is happening here is that ActorProxy inherits from Object, which implements size. Instead of forwarding our request to the Actor (and hence to its behavior), the ActorProxy tried to answer size itself.

This is one of the reasons for the different interaction patterns that ActorProxy objects offer. Use of sync here allows us to ensure the request is forwarded to the Actor correctly.

a sync size wait. "2"

Other methods that trigger this class of difficulty include at: and at:put:, along with all the other methods defined on class Object.

Another surprise lies in wait when it comes to exceptions signaled by an object as part of its normal interface.

Running removeFirst twice works just fine. The third request will signal an empty collection error:

a removeFirst wait. "3"
a removeFirst wait. "4"
a removeFirst wait. "(results in the following window)"

BrokenPromise: Promise was rejected

Clicking on “Debug” allows us to find out what went wrong. In the lower-left panel on the debugger, when the topmost (ActorPromise(Promise)>>wait) context is selected, is an error variable. Selecting that variable shows the underlying error in the adjacent panel:

BrokenPromise: Promise was rejected (detail, removeFirst)

The main takeaway from this is that, since the behaviour object of the actor signaled an uncaught exception, the entire actor has been terminated and is no longer able to reply to requests.

If we ask for its sum again, we’re notified that the actor has terminated, with the “collection is empty” exception that caused it to stop.

At this point, there is no recovery; termination of an actor is final.

A Dictionary actor

Similarly, we can try using a Dictionary as an actor’s behavior object.

d := Actor bootProxy: [ Dictionary new ].
d async at: 1 put: 2. "nil"
d async at: 3 put: 4. "nil"

As before, ordinary methods work just fine.

To avoid the problems with exceptions as part of an object’s normal interface, we can avoid those parts of the interface, and instead use alternatives that allow us to execute specific pieces of code when otherwise an exception would be signalled:

(d at: 1 ifAbsent: [ Actor callerError: 'No such key' ]) wait. "2"
(d at: 11 ifAbsent: [ Actor callerError: 'No such key' ]) wait. "an error"
(d removeKey: 1 ifAbsent: [ Actor callerError: 'No such key' ]) wait. "works once..."
(d removeKey: 1 ifAbsent: [ Actor callerError: 'No such key' ]) wait. "...but not twice."

By using Actor class >> #callerError:, we avoid having an uncaught exception be thrown. Instead, only the promise associated with the request is rejected, causing a BrokenPromise exception in the caller, but leaving the Dictionary actor itself unharmed. Sending future requests to the dictionary actor will continue to work.

Our strategy of using methods like at:ifAbsent: instead of at:, and removeKey:ifAbsent: instead of removeKey:, has allowed us to avoid killing the whole actor, but at the cost of having the ifAbsent: block execute in the wrong context. That is, if it runs, it will run in a context where self is the Dictionary, and Actor current and Actor me denote the actor that the client knows as d.

TCP/IP Echo Server

This tutorial implements a ServerSocketActor-based TCP/IP “echo” service, and a matching client. Documentation on socket support is available.. A larger TCP/IP chat server example can be found in the Actors-Demos package in class ChatRoom.

Socket support is experimental.

The server

We will need two classes for the server: one for the connection-accepting part, and one for each connected session.

EchoServiceActor

The EchoServiceActor has a single instance variable, sock, which holds an ActorProxy for a ServerSocketActor.

ActorBehavior subclass: #EchoServiceActor
    instanceVariableNames: 'sock'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'ActorExamples'

Initialization

Initialization of the behavior object involves two steps. First, we create the listening socket actor. Second, we issue it some credit, allowing it to begin sending us new connections. Each connection we accept depletes its credit by one unit; we will need to replenish it.

initialize
    super initialize.
    sock := ServerSocketActor listenOnPort: 4096.
    sock issueCredit.

Readiness notification

The first event we are sent is an indication that the server socket is listening for new connections.

tcpServer: aServer ready: aBoolean
    self logAll: {
        'Echo server '.
        aBoolean ifTrue: ['is'] ifFalse: ['is not'].
        ' accepting connections'.
        String cr
    }

Accepting a connection

Second, whenever a new connection arrives, we are notified with a tcpServer:accepted: event.

tcpServer: aServer accepted: aConnection
    | session |
    self logAll: {'Connection accepted: '. aConnection}.
    session := EchoSessionActor spawn.
    session actor monitor: self.
    session connection: aConnection.
    aConnection controllingActor: session.
    sock issueCredit.

Let’s take this a step at a time.

First, we log a message to Transcript about the new connection:

self logAll: {'Connection accepted: '. aConnection}.

Then, we spawn the actor that will be responsible for this connection:

session := EchoSessionActor spawn.

We monitor the new actor, so that we are notified when it terminates:

session actor monitor: self.

We tell it about the socket it is to be using:

session connection: aConnection.

We tell the socket that the new EchoSessionActor is to be its controlling actor:

aConnection controllingActor: session.

Finally, we tell the server socket that we are ready to accept another connection when one arrives:

sock issueCredit.

Handling session termination

Finally, because we called monitor: on the EchoSessionActor, we will be sent an event when it terminates:

peer: aSession monitor: aReference terminatedWith: aReason
    self logAll: {'Session ended: '. aSession}.

EchoSessionActor

The EchoSessionActor has a single instance variable, conn, which holds an ActorProxy for a SocketActor.

ActorBehavior subclass: #EchoSessionActor
    instanceVariableNames: 'conn'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'ActorExamples'

Initialization

The actor’s connection is passed to it by its EchoServiceActor with a call to connection:.

connection: aConnection
    conn := aConnection.
    conn delimiterMode: String lf.
    conn issueCredit.

The first thing it does with its new connection is configures it to use linefeed-terminated UTF-8 work units:

conn delimiterMode: String lf.

Then, having declared what kind of work unit it wishes to receive, it sends some receive credit to the connection:

conn issueCredit.

The next line of input to arrive is sent to the actor, consuming one credit unit in the process. Once credit reaches zero, the EchoSessionActor has to issue more credit to allow more data to flow in from the remote peer.

Handling incoming data

Each time a work unit arrives, it is delivered using a tcpSocket:data event.

tcpSocket: aSocket data: line
    self logAll: {'Received line: '. line}.
    conn sendLine: 'You said: ', line.
    conn issueCredit.

All the EchoSessionActor does is repeat the line back to the remote peer, and issue credit to its connection allowing the next line of input to arrive.

Handling disconnection

When the remote peer disconnects, a tcpSocketClosed:reason: event is delivered. Here, we simply terminate the EchoSessionActor.

tcpSocketClosed: aSocket reason: aReason
    self logAll: {'Disconnected: '. aReason}.
    Actor terminateNormally.

Running the service

s := EchoServiceActor spawn.

Now connect to the service on port 4096, perhaps using nc on the command line, or telnet, or the EchoClientActor developed below. You could also use the DemoSocketTerminal from the Actors-Demos package.

When you’ve finished creating and destroying connections, terminate the listening actor:

s actor kill.

This stops the EchoServiceActor, thereby closing the listening socket, but leaves the EchoSessionActors running, allowing them to continue conversing with their remote peers until they disconnect.

The client

For the client, we only need one class, representing a connected session.

As well as using this client, you can also interact with the server using DemoSocketTerminal in the Actors-Demos package.

EchoClientActor

The EchoClientActor has two instance variables: conn, which holds an ActorProxy for a SocketActor; and readers, which holds an OrderedCollection of ActorRequests.

When a line is sent to the server with sendLine:, the call to sendLine: doesn’t return until the reply is received from the server. The request object representing the call is placed in the readers queue. Lines of input are sent to the elements of the queue in order.

ActorBehavior subclass: #EchoClientActor
    instanceVariableNames: 'conn readers'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'ActorExamples'

Initialization

We create the new actor, which will have the current actor as its controlling actor, and will automatically be linked to us.

initialize
    super initialize.
    conn := SocketActor connectToHost: 'localhost' port: 4096.
    readers := OrderedCollection new.

Connection establishment

The first event we receive is notification of successful connection.

tcpSocketConnected: aSocket
    conn delimiterMode: String lf.

We configure the new connection by setting it to linefeed-terminated UTF-8 work units, just like we did in EchoSessionActor.

We don’t issue any credit at this point, however: this is done in sendLine:.

Transmitting data

The sendLine: method does three things. It first suspends and stores its caller in the readers queue. Then, sends a line of text to the server. Finally, it issues a unit of credit, allowing the socket to send us the next line of input from the server—the reply.

sendLine: aString
    readers add: Actor caller.
    conn sendLine: aString.
    conn issueCredit.

Receiving data

When a line arrives from the server, we get a tcpSocket:data: event. We remove the first waiting reader (we know there will be one), and finally reply to it with the received line.

tcpSocket: aSocket data: line
    readers removeFirst resolveWith: line.

Closing the connection

The stop method terminates the actor. Because the EchoClientActor is linked to the SocketActor, this automatically causes the latter to disconnect and terminate.

stop
    Actor terminateNormally.

Example session

Start an EchoServiceActor, as described above.

Then, create a new client:

c := EchoClientActor spawn.

Sending a line results, ultimately, in a reply:

(c sendLine: 'hello') wait. "'You said: hello'"

Finally, stopping the EchoClientActor disconnects the connection:

c stop.

About

Author

Gravatar for Tony Garnock-Jones Actors for Squeak was written by Tony Garnock-Jones, tonyg@leastfixedpoint.com.

License

Actors for Squeak is licensed under the MIT license:

Copyright © 2017–2018 Tony Garnock-Jones.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.