-
Notifications
You must be signed in to change notification settings - Fork 1
SelectActor
Text compliant with Leucine version 0.5.x
With AcceptActor
we are able to receive messages from any other
actor. Most of the times, this is to broad a allowance. It would
be better if we are able to select where the letters are coming
from on the receiving side. SelectActor
is exactly made for that
purpose. It allows you to limit the types of the letters, as
well as their origin. Let's see how we can define that with an
example.
In this example we build a service that sends the time to all clients any time a new client makes a connection. The clients will be created from the command line, and typing 'exit' will terminate the demo. Let us build this application top down.
The example will consist of one fixed actor, the console, and actors that are created on request.
First of all we need a loop that reads the command line and
sends the connection requests to some service that will
handle them further. This Server
must at least be able to
handle to types of messages:
- A termination message, for example
Terminate
, which stops the service when we type 'exit' - A connection message, for example
Connect(name)
which opens a connection with a certain name.
With this, we can define the main loop as:
object Main :
/* All messages send from here are from an Anonymous source. */
given Actor.Anonymous = Actor.Anonymous
/* Define the Server */
val server = new Server
/* Print a welcome message. */
println("Demo started, type 'exit' to terminate.")
/* Start the execution loop: */
def main(args: Array[String]): Unit = while {
/* Read a new command/name from the command line */
val command = StdIn.readLine
/* See if this command is exit. */
val exit = command == "exit"
/* Send Terminate to the server if we need to exit, or open a new
* connection with the given name. */
server ! (if exit then Server.Terminate else Server.Connect(command))
/* If we do not want to exit, continue this loop. */
!exit } do {}
Observe that we now explicitly define a given anonymous actor. This is
needed since the SelectActor
we will be using as base actor
does track the sender so we must also state what this sender is.
When we are not inside an actor (where the sender is implicit) it is
needed to define this default.
Subsequently the server is created, and main
contains a small
loop that only terminates when the command equals exit
. Any
other word will create a new named connection.
Now, we need a server that responds to messages Connect
and
Terminate
. These messages originate from an Anonymous
source,
and there are no other sources that are allowed to send messages
to the server. So we only Accept
from Anonymous
.
Therefore we define the companion object to be:
object Server extends SelectDefine, Stateless :
/* Only accept messages from the outside world */
type Accept = Anonymous
/* Base type of all Server Letters, sealed enables the compiler to see if we handled them all. */
sealed trait Letter extends Actor.Letter[Accept]
/* Letter that requires a connection */
case class Connect(name: String) extends Letter
/* Letter that indicates the connection is over. */
case object Terminate extends Letter
That's it. The type Accept = ...
should contain all the (types of) actors Server
is able to receive letters from as a union. And Letter
should be derived from
Actor.Letter[Accept]
. This is what the extension on SelectDefine
requires.
To keep it simple here we store the state in a var
, therefore we also
mix in Stateless
.
The definition of the class itself is not much longer. There will
be a var providers
which holds an provider actor for every new
connection and two method's. One is of course the obligatory
receive
method.
And we overload the stopped
method, so we can report that
the server is terminated.
class Server extends SelectActor(Server,"server") :
/* Keep all providers here */
var providers : List[Provider] = Nil
/* Send to the client that we have started. */
println(s"Server Constructed")
/* Handle all incoming letters. */
protected def receive(letter: Letter, sender: Sender): Unit = letter match
/* The new connection will come in as a letter with a connection name. */
case Server.Connect(name) =>
println(s"Accepted a connection with name: $name.")
/* We see the providers as workers and generate automatic names for them. */
providers = new Provider(name) :: providers
/* Trigger each provider to action. */
providers.foreach(_ ! Provider.Send)
/* The request has come to close stop this server. */
case Server.Terminate =>
println("Server Termination Request")
/* Stop all providers directly. */
providers.foreach(_.stop(Actor.Stop.Direct))
/* Stop the server actor directly. */
stop(Actor.Stop.Direct)
override def stopped(cause: Actor.Stop, complete: Boolean) =
/* Decently say goodbye */
println("Server stopped.")
When a new connection request comes in, the work is passed to a provider actor which is created on the fly and stored in the list of providers. On every connection request we also tickle all providers for some action.
When a termination request comes in, we stop all providers by calling a direct stop on them and subsequently stop ourselves.
That's about it. Of course, we could make this more fancy, with timers etc, but that's for an other chapter.
The Provider
only needs to accept messages from the Server
, so
we set the type Accept
to Server
. Besides that, we only need
to define one letter, and that is Send
, which makes the request
to send some time data over the connection.
Therefore the companion object looks like:
object Provider extends SelectDefine, Stateless:
/* Only accept messages from the Server */
type Accept = Server
/* Base type of all Provider Letters, sealed enables the compiler to see if we handled them all. */
sealed trait Letter extends Actor.Letter[Accept]
/* Letter that tells we must send a new time message. */
case object Send extends Letter
As said, for each connection the user wants to open, a provider actor is created. The name for this actor is the connection name (actually, this might be tricky in a real world application for it allows for names to appear twice. In the current application that does not hurt however).
The letter that comes in simple prints the time with the connection name.
And, when the actor stops, we report it did so. So the class body
for Provider
looks like:
class Provider(name: String) extends SelectActor(Provider,name) :
/* Send to the client that we are connected. */
println(s"Provider constructed for connection $name")
/* Handle the messages, which is only the posted letter in this case. */
def receive(letter: Letter, sender: Sender): Unit = letter match
case Provider.Send =>
/* Report the new message */
val datetime = new Date().toString
println(s"Provider $name says: $datetime")
/* If this actor is stopped, we must close the connection. */
override def stopped(cause: Actor.Stop, complete: Boolean) =
println(s"Provider stopped, goodbye $name.")
This completes the code for this example, lets put it to the test.
You can copy/paste the code in your editor, or
see this run on Scastie
(with slight modification of the main loop, for is StdIn.readLine
not supported on that platform). Do not forget to add the following
preamble in your own code to get this to work:
import java.util.Date
import scala.concurrent.duration.FiniteDuration
import scala.concurrent.duration.DurationInt
import scala.io.StdIn
import s2a.leucine.actors.*
given ActorContext = ActorContext.system
The result should be something like this:
sbt:leucine > run
Server Constructed
Demo started, type 'exit' to terminate.
> piet
Accepted a connection with name: piet.
Provider constructed for connection piet
Provider piet says: Fri May 19 17:00:27 CEST 2023
> klaas
Accepted a connection with name: klaas.
Provider constructed for connection klaas
Provider klaas says: Fri May 19 17:00:30 CEST 2023
Provider piet says: Fri May 19 17:00:30 CEST 2023
> jan
Accepted a connection with name: jan.
Provider constructed for connection jan
Provider jan says: Fri May 19 17:00:32 CEST 2023
Provider klaas says: Fri May 19 17:00:32 CEST 2023
Provider piet says: Fri May 19 17:00:32 CEST 2023
> kees
Accepted a connection with name: kees.
Provider constructed for connection kees
Provider kees says: Fri May 19 17:00:35 CEST 2023
Provider jan says: Fri May 19 17:00:35 CEST 2023
Provider klaas says: Fri May 19 17:00:35 CEST 2023
Provider piet says: Fri May 19 17:00:35 CEST 2023
> exit
Server Termination Request
Provider stopped, goodbye kees.
Provider stopped, goodbye piet.
Server stopped.
Provider stopped, goodbye jan.
Provider stopped, goodbye klaas.
Further down this manual we will extend the example with timers and family actor aids to automate the service even more.
In the demo's you will find an extended example that provides the service on a raw TCP socket.
The fact that we selected only some source to act as sender for actors protects the them from unsolicited messages. No spam possible, so to say. You can try this by changing the receive method of the provider to return a message:
def receive(letter: Letter, sender: Sender): Unit = (letter,sender) match
case (Provider.Send,server:Server) =>
/* Report the new message */
val datetime = new Date().toString
println(s"Provider $name says: $datetime")
/* Invalid message */
server.send(Server.Terminate,this)
Found: (Provider.this : Provider)
Required: server.Sender
if you would replace this
with Actor.Anonymous
it
will be excepted as valid (but stop the application
prematurely).