Hello world example with actors
HelloWorldActors is a simple program to get you started using the Coyote actors programming model.
What you will need
To run the Hello World Actors example, you will need to:
- Install Visual Studio 2022.
- Install the .NET 8.0 version of the coyote tool.
- Be familiar with the
coyote
tool. See using Coyote. - Clone the Coyote git repo.
Build the sample
You can build the sample by following the instructions here.
Run the HelloWorldActors application
Now you can run the HelloWorldActors application:
"./Samples/bin/net8.0/HelloWorldActors.exe"
Press the ENTER key to terminate the program when it is done. Note that a bug has been inserted into
the code intentionally and will be caught by a Coyote Assert
. The program should display 1 to 5
greetings like Hello World!
or Good Morning
. The intentional bug is hard to reproduce when you
run the program manually.
The typical “normal” run will look like this:
press ENTER to terminate...
Requesting 5 greetings
Received greeting: Good Morning
Received greeting: Hello World!
Received greeting: Hello World!
Received greeting: Hello World!
Received greeting: Good Morning
When the error is caught, an extra line of output is written saying:
Exception 'Microsoft.Coyote.Runtime.AssertionFailureException' was thrown in 0 (action '1'):
Too many greetings returned!
Finding the bug using Coyote test tool
See Using Coyote for information on how to
find the coyote
test tool and setup your environment to use it.
Enter the following from the command line:
coyote test ./Samples/bin/net8.0/HelloWorldActors.dll --iterations 30
The result is:
. Testing .\Samples\bin\net8.0\HelloWorldActors.dll
Starting TestingProcessScheduler in process 16432
... Created '1' testing task.
... Task 0 is using 'random' strategy (seed:308255541).
..... Iteration #1
..... Iteration #2
... Task 0 found a bug.
... Emitting task 0 traces:
..... Writing .\Samples\bin\net8.0\Output\HelloWorldActors.exe\CoyoteOutput\HelloWorldActors_0_2.txt
..... Writing .\Samples\bin\net8.0\Output\HelloWorldActors.exe\CoyoteOutput\HelloWorldActors_0_2.trace
... Elapsed 0.0906639 sec.
... Testing statistics:
..... Found 1 bug.
... Exploration statistics:
..... Explored 2 schedules: 2 fair and 0 unfair.
..... Found 50.00% buggy schedules.
..... Number of scheduling decisions in fair terminating schedules: 23 (min), 30 (avg), 37 (max).
... Elapsed 0.1819877 sec.
. Done
The Coyote tester has found the bug very quickly—something that takes much longer if testing manually. When using Coyote for testing more complex, real-world systems the time savings can be huge! This will give you the ability to find and fix even the most difficult to reproduce concurrency bugs before pushing to production.
To learn more about testing with Coyote read here.
The Greeter
Please read Programming model: asynchronous actors to gain a basic understanding of the Actors programming model.
The core of an Actor
based Coyote program, is, well, an Actor
class! The following Actor
named
Greeter
is able to receive a RequestGreetingEvent
to which it responds with a GreetingEvent
:
[OnEventDoAction(typeof(RequestGreetingEvent), nameof(HandleGreeting))]
public class Greeter : Actor
{
/// This method is called when this actor receives a RequestGreetingEvent.
private void HandleGreeting(Event e)
{
if (e is RequestGreetingEvent ge)
{
string greeting = this.RandomBoolean() ? "Hello World!" : "Good Morning";
this.SendEvent(ge.Caller, new GreetingEvent(greeting));
if (this.RandomBoolean(10))
{
// bug: a 1 in 10 chance of sending too many greetings.
this.SendEvent(ge.Caller, new GreetingEvent(greeting));
}
}
}
}
Notice the OnEventDoAction
custom attribute decorating the class. This tells the Coyote runtime
that this class expects to receive RequestGreetingEvents
. These events will be queued in the
inbox for this Actor
until the right time to dequeue them and process them. The custom attribute
tells the Coyote
runtime to call the HandleGreeting
method. The Coyote runtime will also report
an error if a different type of event is sent to the Greeter
.
The HandleGreeting
method is called with an Event
parameter, and you know in this case the
event will be of type RequestGreetingEvent
. This event is defined in Events.cs
like this:
internal class RequestGreetingEvent : Event
{
public readonly ActorId Caller;
public RequestGreetingEvent(ActorId caller)
{
this.Caller = caller;
}
}
The inbox is crucial to the Actor model
You can think of an Event
declaration as a type of interface definition for Actors
. Events
really define the interface of an Actor
because the caller will never get to see the Greeter
class, so the caller can’t just call HandleGreeting
directly. The caller only gets to see an
ActorId
, which is like a Coyote runtime handle to the actor.
There is a really important reason for this.
Events are queued in an inbox managed by the Actor
base class. This serializes the incoming
events so that at any given time, only one event is being handled in the Greeter
. This greatly
simplifies concurrent programming. For example, there is no need to use lock
to protect against
data race conditions, and therefore no need to worry about deadlocks. Each Actor
instance can run
in a separate thread, so all actors in the system can be hugely parallel, but at the same time the
processing inside an Actor
is incredibly simple. So you get the best of both worlds, code that
scales but is also easy to write. An Actor
receives messages, and can create other Actor
objects, and can send events.
Notice the Greeter
gets an ActorId
from the RequestGreetingEvent
and uses that to send a
greeting back to the caller using SendEvent
:
this.SendEvent(ge.Caller, new GreetingEvent(greeting));
You can also see the bug which has been injected deliberately, namely, it randomly returns more than
one GreetingEvent
.
A TestActor
To test this Greeter
you will need to setup a TestActor
which is done in Program.cs
. The
TestActor
creates the Greeter
and sends between 1 and 5 RequestGreetingEvents
. The TestActor
is declared in a way that tells the Coyote
runtime it is expecting to receive GreetingEvents
as
follows:
[OnEventDoAction(typeof(GreetingEvent), nameof(HandleGreeting))]
To make this a good test, the TestActor
keeps track of how many greetings were sent and how many
were received, and it writes an Assert
to ensure it doesn’t receive too many as follows:
private void HandleGreeting(Event e)
{
// this is perfectly thread safe, because all message handling in actors is
// serialized within the Actor class.
this.Count--;
string greeting = ((GreetingEvent)e).Greeting;
Console.WriteLine("Received greeting: {0}", greeting);
this.Assert(this.Count >= 0, "Too many greetings returned!");
}
Lastly, to test the TestActor
you need to fire up the Coyote runtime which is done in the HostProgram
Main
entry point as follows:
public static void Main()
{
var config = Configuration.Create();
IActorRuntime runtime = RuntimeFactory.Create(config);
Execute(runtime);
runtime.OnFailure += OnRuntimeFailure;
Console.WriteLine("press ENTER to terminate...");
Console.ReadLine();
}
Notice that Coyote actors run in parallel, so you have to stop the program from terminating
prematurely, which can be done with a Console.ReadLine
call. In order for this program to also be
testable using the coyote test
tool, you need to declare a test method as follows:
[Microsoft.Coyote.SystematicTesting.Test]
public static void Execute(IActorRuntime runtime)
{
runtime.CreateActor(typeof(TestActor));
}
The coyote test
tool will call this method. But it will not call your Main
entry point. So
this Execute
method needs to be standalone. It cannot depend on anything except statically
initialized variables. There is no way to get command line arguments from coyote
test through to
this method.
This Execute
method is very simple, it just creates the TestActor
. In Coyote once an actor is
created it lives forever until it is halted. Note that Coyote programs can run forever, the coyote test
tool has ways of interrupting and restarting this Execute
method based on --iterations
and
--max-steps
arguments provided to the test tool.
So now you know what happened when you ran the following command line:
coyote test ./Samples/bin/net8.0/HelloWorldActors.exe --iterations 30
A special coyote TestingEngine
was created, it invoked the Execute
method 30 times, and during
those executions the test engine took over all concurrent activity and the non-determinism (like
RandomInteger
) to ensure the program covered lots of async non-deterministic choices, and recorded
all this in a way that when a bug was found, it is able to reproduce that bug. See also Using
Coyote for information on how to replay a test that found a bug
using coyote replay
.
Summary
In this tutorial you learned:
- How to create an
Actor
and useSendEvent
to send messages to it. - How an
Actor
can handle events usingOnEventDoAction
. - The importance of the inbox to simplify parallel programming.
- How to run the HelloWorldActors sample from the command-line.
- How to use
Assert
in Coyote code. - How to write a
[Test]
method for use incoyote test
runs. - How to run that test using the
coyote test
tool.