Actors for Squeak Smalltalk
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
- Download a recent Squeak image and VM.
- Update to at least trunk update number 17768.
(Installer squeaksource project: 'Actors') install: 'ConfigurationOfActors'
- (optional) Run the tests in package
Actors-Tests
.
Features
- Erlang-inspired model, including
- Smalltalk-inspired extensions and conveniences
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
- Download a recent Squeak image and VM.
- Update to at least trunk update number 17768.
(Installer squeaksource project: 'Actors') install: 'ConfigurationOfActors'
- (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”:
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
in a Workspace. Select the Actors-Tests
package from the
left-hand-side list, and click “Run Selected”:
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 languages.
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.
Links and Supervision
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.
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 update
d 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:
Links
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.
Adding a link
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.
Removing a link
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.
Handling link activation
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”.
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.
Morphic GUI
The library offers some support for using an actor as a model for
Morph
s 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:
- The call to
update:
has to happen on the UI process, while the
call to changed
must run on the actor’s own process.
- 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
halt
ed.
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.
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
.
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 abandon
s 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:
- an
ActorProxy
returns Promise
s for callback invocations, which
Morphic is not expecting;
- a terminated
Actor
invoked via its ActorProxy
yields Promise
rejections and signalled BrokenPromise
exceptions, which Morphic
doesn’t expect; and
- 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
Promise
s in a pervasive convention for RPC to Actor
s
- 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
Promise
s 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
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"
Actor
s 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
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
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 terminate
d. 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 Actor
s 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 Actor
s 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 print
ed.
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 ActorProcess
es, 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 Actor
s.
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, ActorProcess
es 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
.
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
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 control
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
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 String
s.
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 ByteArray
s.
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 ByteArray
s; for
rawModeAscii
, they will be String
s.
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
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.
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.
Links and monitors
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.
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.
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
.
Alternatively, we can use the blocking
convenience method to arrange for wait
to be called for us:
Counting
Add the following two methods to CounterActor
.
add: aNumber
count := count + aNumber.
^ count
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:
And we can increment its count by one:
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"
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.
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.
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, link
ing 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.
Reprise, with links this time
After creating the actors,
actor1 := PingPong2 spawn.
actor2 := PingPong2 spawn.
we link them to each other:
Now, if one dies, so does the other.
Open a process browser, and check to see that both actors are there.
Now, kill actor1
.
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 BarrierActor
s 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
.
Then, type in
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:
Exploring the state of the BarrierActor
At this moment, the two Promise
s 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:
Releasing the waiters
Finally, “do it” with the following expression in the workspace:
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:
Cleaning up
Finally, we can terminate our BarrierActor
.
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:
If we explore our actor using “explore it” on a
in our workspace, we
see the state of the actor is as follows:
As expected, we see an OrderedCollection(3 4)
as the behavior
object.
Interactions with actors default to asynchronous:
But we can wait for them to complete as usual if we like:
However, a surprise is in store for us if we try to ask for the size
of the OrderedCollection
.
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.
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)"
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:
.png)
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:
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:
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:
This stops the EchoServiceActor
, thereby closing the listening
socket, but leaves the EchoSessionActor
s 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 ActorRequest
s.
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:
Feedback and Contributing
Email me at
tonyg@leastfixedpoint.com for bug reports or feature suggestions.
Alternatively, start a discussion on
the squeak-dev mailing list.
The source code is managed under the
Actors project at
SqueakSource.
The documentation is managed as a
Jekyll-based website at
this Github project.
Actors for Squeak is licensed under the
MIT license; see the
about page for details.
About
Author
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.