Programming model: asynchronous actors

The asynchronous actors programming model of Coyote is an actor-based programming model that encourages a message passing (or event based) programming model where all asynchronous actions happen by sending asynchronous events from one actor to another. This model is similar to how real people interact asynchronously via email.

Each Coyote actor has an inbox for events, event handlers, as well as normal class fields and methods. Actors run concurrently with respect to each other, but individually they handle their input queue in a sequential way. When an event arrives, the actor dequeues that event from the input queue and handles it by executing a sequence of operations. Each operation might update a field, create a new actor, or send an event to another actor. In Coyote, creating actors and sending events are both non-blocking operations. In the case of a send operation the message (or event) is simply enqueued into the input queue of the target actor and, most importantly, it does not wait for the message to be processed at the target before returning control to the sender.

The actor model also provides a specialized type of actor called a StateMachine. State machines are actors that have explicitly declared states and state transitions. Every object oriented class that has member variables is really also just a state machine where that state is updated as methods are called, but sometimes this gets really complicated and hard to test. Formal state machines help you model your states more explicitly and coyote tester can help you find bugs by exploring different state transitions using information you provide declaring how various types of events causes those state transitions.

See also: how are Coyote Actors different from existing Microsoft Actor frameworks?.

Declaring and creating actors

An actor based program in Coyote is a normal .NET program that also uses the Actor, StateMachine and Event base classes from the Microsoft.Coyote.Actors namespace, which is available in the Microsoft.Coyote.Actors NuGet package. Actors can be declared in the following way:

using Microsoft.Coyote.Actors;

class Client : Actor { ... }

class Server : Actor { ... }

The above code declares two actors named Client and Server. Being a C# class you can also declare fields, properties and methods.

An actor can create an instance of another actor and send Events using the following Actor methods:

  ActorId clientId = this.CreateActor(typeof(Client));
  this.SendEvent(this.ClientId, new PingEvent());

When an event is being sent, it is enqueued in the event queue of the target actor. The coyote runtime will at some point dequeue the received event, and allow the target actor to handle it asynchronously.

All actors in Coyote have an associated unique ActorId which identifies a specific instance of that type. Note that Coyote never gives you the actual object reference for an actor instance. This ensures your code does not get too tightly coupled.

By limiting yourself to the Coyote API’s for interacting with an actor, you also get all the benefits of coyote test in terms of understanding more deeply how to test all asynchronous interactions and ensure your specifications are maintained correctly. There is a lot of literature on actor models that explain in more depth the importance of this message passing programming model which is especially popular in the world of distributed systems. Event based programming is also popular in User Interface development and even shows up in low level embedded systems. It is a powerful tool for solving the tangled web of complexity that happens with less disciplined architectures.

Starting a Coyote actor-based program

To create the first instance of an Actor you need to initialize the Coyote actor runtime inside your C# process (typically in the Main method). An example of this is the following:

using System;
using Microsoft.Coyote;
using Microsoft.Coyote.Actors;
using Microsoft.Coyote.SystematicTesting;

class Program
{
    static void Main(string[] args)
    {
        IActorRuntime runtime = RuntimeFactory.Create();
        Execute(runtime);
        Console.ReadLine();
    }

    [Test]
    public static void Execute(IActorRuntime runtime)
    {
        ActorId serverId = runtime.CreateActor(typeof(Server));
    }
}

You must first import the Coyote actor runtime library (Microsoft.Coyote.Actors.dll), which you can get from its NuGet package, then create a runtime instance (of type IActorRuntime) which you pass to a [Test] method.

The test method named Execute will be the entry point that is used during testing of your Coyote program. In this case it simply invokes the CreateActor method of the runtime to instantiate the first Coyote actor (of type Server in the above example).

The CreateActor method accepts as a parameter the type of the actor to be instantiated, and returns the ActorId representing that actor instance and this bootstraps a series of asynchronous events that handle initialization of that actor and any operations it performs during initialization.

Because CreateActor is an asynchronous method, we call the Console.ReadLine method, which pauses the main thread until some console input has been given, so that the host C# program does not exit prematurely.

The IActorRuntime interface also provides the SendEvent method for sending events to a Coyote actor. This method accepts as parameters an object of type ActorId and an event object. It also has a couple more advanced parameters which you don’t need to worry about right now.

An event can be created by sub-classing from Microsoft.Coyote.Actors.Event:

class PingEvent : Event
{
    public readonly ActorId Caller;

    public PingEvent(ActorId caller)
    {
        this.Caller = caller;
    }
}

class PongEvent : Event { }

An event can contain members of any type (including scalar values or references to object) and when an event is sent to a target actor there is no deep-copying of those members, for performance reasons. The target actor will be able to see the Event object and cast it to a specific type to extract the information it needs.

Now you can write a complete actor, declaring what type of events it can handle:

[OnEventDoAction(typeof(PingEvent), nameof(HandlePing))]
class Server : Actor
{
    public void HandlePing(Event e)
    {
        PingEvent ping = (PingEvent)e;
        Console.WriteLine("Server handling ping");
        Console.WriteLine("Server sending pong back to caller");
        this.SendEvent(ping.Caller, new PongEvent());
    }
}

This Server is an Actor that can receive PingEvent. The PingEvent contains the ActorId of the caller and the Server uses that to send back a PongEvent in response.

An event handler controls how a machine reacts to a received event. It is clearly just a method so you can do anything there, including creating one or more actor instances, sending one or more events, updating some private state or invoking some 3rd party library.

To complete this Coyote program, you can provide the following implementation of the Client actor:

using System.Threading.Tasks;

class SetupEvent : Event
{
    public readonly ActorId ServerId;

    public SetupEvent(ActorId server)
    {
        this.ServerId = server;
    }
}

[OnEventDoAction(typeof(PongEvent), nameof(HandlePong))]
class Client : Actor
{
    public ActorId ServerId;

    protected override Task OnInitializeAsync(Event initialEvent)
    {
        Console.WriteLine("{0} initializing", this.Id);
        this.ServerId = ((SetupEvent)initialEvent).ServerId;
        Console.WriteLine("{0} sending ping event to server", this.Id);
        this.SendEvent(this.ServerId, new PingEvent(this.Id));
        return base.OnInitializeAsync(initialEvent);
    }

    void HandlePong()
    {
        Console.WriteLine("{0} received pong event", this.Id);
    }
}

This Client is an Actor that sends PingEvents to a server. This means the Client needs to know the ActorId of the Server. This can be done using an initialEvent passed to OnInitializeAsync. The Client then uses this ActorId to send a PingEvent to the Server.

When the Server responds with a PongEvent the HandlePong method is called because of the OnEventDoAction declaration on the class. Notice in this case the HandlePong event handler takes no Event argument. The Event argument is optional on Coyote event handlers.

Note that HandlePong could also be defined as an async Task method. Async handlers are allowed so that you can call external async systems in your production code, but this has some restrictions. You are not allowed to directly create parallel tasks inside an actor (e.g. by using Task.Run) as that can introduce race conditions (if you need to parallelize a workload, you can simply create more actors). Also, during testing, you should not use Task.Delay or Task.Yield in your event handlers. It is ok to have truly async behavior in production, but at test time the coyote test tool wants to know about, so that it can control, all async behavior of your actor. If it detects some uncontrolled async behavior an error will be reported.

One last remaining bit of code is needed in your Program to complete this example, namely, you need to create the Client actor in the Execute method, in fact, you can create as many Client actors as you want to make this an interesting test:

public static void Execute(IActorRuntime runtime)
{
    ActorId serverId = runtime.CreateActor(typeof(Server));
    runtime.CreateActor(typeof(Client), new SetupEvent(serverId));
    runtime.CreateActor(typeof(Client), new SetupEvent(serverId));
    runtime.CreateActor(typeof(Client), new SetupEvent(serverId));
}

The output of the program will be something like this:

Client(3) initializing
Client(3) sending ping event to server
Client(1) initializing
Client(2) initializing
Client(2) sending ping event to server
Client(1) sending ping event to server
Server handling ping from Client(1)
Server sending pong back to caller
Client(1) received pong event
Server handling ping from Client(3)
Server sending pong back to caller
Server handling ping from Client(2)
Server sending pong back to caller
Client(2) received pong event
Client(3) received pong event

The CreateActor and SendEvent methods are non-blocking so you can see those operations are interleaved in the log output. The Coyote runtime will take care of all the underlying concurrency using the Task Parallel Library, which means that you do not need to explicitly create and manage tasks. However, you must be careful not to share data between actors because accessing that shared data from multiple actors at once could lead to race conditions.

You can reduce race conditions in your code if you use events to transfer data from one actor to another. But since it is a reference model without deep copy semantics, you can actually share data between actors if you really need to. See sharing objects for more information in this advanced topic.

Assertions

Coyote also supports specifying invariants through assertions. You can do this by using the Assert method, which accepts as input a predicate that must always hold in that specific program point, e.g. this.Assert(k == 0), which holds if the integer k equals to 0. These Assert statement are useful for local invariants, i.e., they are about the state of a single actor. For global invariants it is recommended that you use Monitors.

Samples

To see a full working example of an Actor based program see the Hello World Actors tutorial.

Seel also precise definition of actor semantics.