Actors in computer science literature respond to messages, store state internally to change future responses, and have the ability to launch other actors. We support these, as well as a publish-subscribe mechanism where an actor can publish changes to any & all subscribed clients. This push-based model allows for actors to notify clients in a natural way for their UI stack. For example, if an actor representing a data structure is changed, the updates to that data structure can be shared with all interested clients easily. For instance, we’ve built an ObservableCloudList<T> type that works with data binding in .NET GUI applications.

Our intention is that actors run in an Actor Runtime, a cluster of machines that host actors on multiple replicas. An actor’s state will be duplicated on a number of replicas. There is a primary replica that services all incoming requests and handles replicating any state changes to secondary replicas. If a primary replica crashes, a secondary is promoted and takes over. Clients reconnect to the new replica and continue executing. Here’s a high level usage of actors, focusing on usage from client apps.[1]

clip_image004

An additional property of actors is that they only process one message/request at a time, so that the logic for each message/request has exclusive access to the actor’s state. An additional property of our actors is that, for any one client request, either all state changes (i.e., writes) associated with that request happen or none of them happen (in which case we report an error to the client). This relieves some of the burden around consistency from an actor author.

To address scalability concerns, the Actor Runtime supports co-locating multiple partitions of an actor’s state into one multi-tenant actor.  A section below describes this in detail, with pictures of how concepts map to processes.

1. ActorFx Basics

The essence of the ActorFx model is captured in two interfaces: IActor and IActorState. IActor is the interface for an actor service, and IActorState is the interface through which actor logic accesses actor state.

1.1. The IActor interface

An ActorFx actor can be thought of as a highly available service, and IActor serves as the interface for that service. The IActor interface (and accompanying IPublicationCallbackContract interface) look like this:

    public interface IActor
    {
        // Returns an ActorServiceResult encoded as a JSON string, ASR.Result is object
        string CallMethod(string clientId, int clientSequenceNumber, string partitionName, string methodName, string[] parameters);

        // Returns an ActorServiceResult encoded as a JSON string, ASR.Result is bool
        string AddAssembly(int clientSequenceNumber, string partitionName, string assemblyName, byte[] assemblyBytes);

        // Returns an ActorServiceResult encoded as a JSON string, ASR.Result is null
        string Subscribe(int clientSequenceNumber, string partitionName, string eventType, IPublicationCallbackContract context);

        // Returns an ActorServiceResult encoded as a JSON string, ASR.Result is null
        string Unsubscribe(int clientSequenceNumber, string partitionName, string eventType, IPublicationCallbackContract context);

        // Returns an ActorServiceResult encoded as a JSON string, ASR.Result is null
        string UnsubscribeAll(int clientSequenceNumber, string partitionName, IPublicationCallbackContract context);

        // Returns an ActorServiceResult encoded as a JSON string, ASR.Result is the
        // service's address (e.g., "fabric:/actor/list/mylist") in string form.
        string Ping(int clientSequenceNumber, string partitionName);

        // Returns an ActorServiceResult encoded as a JSON string, ASR.Result is null
        // Tweaks parameters for running the actor, such as ETW event verbosity.
        // Configuration string format: Foo=x;Bar=y
        string Configure(int clientSequenceNumber, string partitionName, string configurationString);

        string AddMethod(int clientSequenceNumber, string partitionName, int languageIndex, string methodName, string methodImpl);
    }

    public interface IPublicationCallbackContract
    {
        void OnNewEvent(ActorEvent actorEvent);
        void OnCompleting();
    }

 

The CallMethod method allows you to call one of the actor’s methods. The AddAssembly and AddMethod methods allow you to ship actor methods to the actor in the form of .NET assemblies or methods in raw string form from other platforms. Actor methods are the “lingua franca” of actor logic, and will be discussed further in the next section.

The Subscribe, Unsubscribe and UnsubscribeAll methods facilitate subscribing and unsubscribing to events being produced by an actor. As can be seen, event types are coded as strings. An event type might be something like “Collection.ElementAdded” or “Service.Shutdown”. Event notifications are received through the FabricActorClient. Each actor can define its own events and event names. And note that the pub/sub feature is opt-in; it is perfectly fine for an actor to not support any events, or for clients to choose to not listen to an actor’s events.

The Ping method can be used to ascertain that an actor is up and ready to receive requests.  The Configure method allows for tweaks to aspects of the actor, currently allowing users to dynamically adjust how verbose our logging is for an actor.

A few more things to note about the methods on IActor:

  • All return an ActorServiceResult object encoded as a JSON string. This encoding allows actor method results to be consumed by platforms other than .NET.
  • All accept an integer clientSequenceNumber as one of their parameters. This client sequence number is used to properly route responses on the client side, and also to effect retries of failed requests.
  • All accept a partition name to support multi-tenant actors.  This allows one actor to store multiple independent pieces of state, and lets us increase scalability significantly.  The partition name can be null or “” for non-partitioned actors. 
  • The parameters argument to CallMethod is encoded as an array of JSON strings. (This encoding allows non-.NET platforms to pass arguments to actor methods.) The runtime then converts those strings into objects to pass to the specified actor method[2].

For .NET client applications that want to send objects over the wire, note that those types must be serializable using Json.NET.  Json.NET supports most .NET forms of serialization attributes.  While testing with the BinaryFormatter isn’t required, it is a good way to think about serialization requirements for object graphs.

1.2. Actor Methods

Actor methods are the mechanism through which actor logic is defined, encapsulated and invoked. Actor methods can be conveyed to an actor in one of three ways:

  1. They can be defined in .NET languages like C# and sent via the IActor.AddAssembly method.
  2. They can be defined in other languages (e.g., JavaScript or Python) and sent via the IActor.AddMethod method.
  3. They can be hard-coded into the state during actor initialization. Our ListActor and DictionaryActor do this, so that these actors come up pre-loaded with useful actor methods.

Methods (1) and (2) will be discussed further below.

1.2.1. C# Actor Methods

You can define methods in C# within an assembly, and then ship that assembly to an actor via the IActor.AddMethod method. The following is an example of such a method:

        [ActorMethod]
        public static object MethodA(IActorState state, object[] args)
        {
            var arg0 = (int)args[0];
            state.Set("_someKey", arg0);
            state.CallMethod("someMethod", new object[] { 42 });
            return state.Get("_someOtherKey");
        }

Actor methods defined in such a fashion must follow these constraints:

  1. The method is decorated with an [ActorMethod] declaration. This will allow the actor logic to identify those methods that are intended to be used as actor methods.
  2. The method is public and static.
  3. The method accepts two arguments:
    • An IActorState argument. This object/argument is provided by the runtime and allows the logic within the actor method to interact with the actor’s state.
    • An object[] argument. This argument contains the arguments to the actor method, conveyed as an array objects.
  4. The method returns an object of type object.

Those familiar with object-oriented programming should be able to see a parallel here. In OOP, an instance method call is equivalent to a static method call into which you pass the “this” pointer. In the C# sample below, for example, Method1 and Method2 are equivalent in terms of functionality:

    class SomeClass
    {
        int _someMemberField;

        public void Method1(int num)
        {
            _someMemberField += num;
        }

        public static void Method2(SomeClass thisPtr, int num)
        {
            thisPtr._someMemberField += num;
        }
    }

Similarly, the IActorState argument provided by the runtime can conceptually be thought of as the “this” pointer for the actor. So actor methods can be thought of as instance methods for the actor. This transformation from instance fields to storing state via an interface abstracts away the storage mechanism for an object’s state, allowing implementations of that interface to replicate state across multiple machines. High availability is achieved through redundant replicas of actors, each with its own copy of the IActorState. Currently we are providing an IActorState implementation that stores state in memory replicated across multiple machines. We intend to provide an IActorState implementation with persistence support in a future release.

1.2.1.1. Dynamic Actor Methods

The Actor Framework supports actor methods with a dynamic actor state parameter and dynamic method arguments. This allows for more natural notations for property-set, property-get and method calls inside your actor method. For example, this actor method with a “traditional” signature:

        [ActorMethod]
        public static object MethodA(IActorState state, object[] args)
        {
            var arg0 = (int)args[0];
            state.Set("_someKey", arg0);
            state.CallMethod("someMethod", new object[] { 42 });
            return state.Get("_someOtherKey");
        }

could be re-written as this actor method with a dynamic signature:

        [ActorMethod]
        public static object MethodB(dynamic state, dynamic[] args)
        {
            int arg0 = args[0];
            state._someKey = arg0;
            state.someMethod(42);
            return state._someOtherKey;
        }

It may be desirable to access computed keys, rather than constant keys, so our support for dynamic actor methods also includes support for index-style accessing of state values:

        [ActorMethod]
        public static object MethodC(dynamic state, dynamic[] args)
        {
            int index = args[0];
            var desiredKey = "keyCollection_" + index;
            // "state.desiredKey = 42;" would assign the value 42 to a key named 
            // "desiredKey", which is not what you want.
            state[desiredKey] = 42; // assigns value 42 to computed key value
            return null;
        }

The use of dynamic actor state essentially does away with the need to explicitly call Set(), Get() or CallMethod() on the actor state. However, the other standard IActorState methods are still supported:

  • void Remove(string key): Removes the key-value pair associated with the specified key from the actor state.
  • bool TryGet(string key, out object value): This is not explicitly supported when using dynamic actor state. However, it is replaced by these method:
    • bool Contains(string key): returns true if the key is present in the actor state, false otherwise.
    • object ValueOrDefault(string key, object defaultValue): if the key is present in the actor state, returns the associated value; otherwise, returns the provided defaultValue.
  • void Publish(string eventType, object eventPayload): publishes the specified payload to the specified event stream.
  • IActorProxy GetActorProxy(string externalActorName, string partitionName): returns an IActorProxy for the specified actor.

Any method call on a dynamic actor state that is not one of the above-named methods is assumed to be a call to a user-defined actor method.

And just to reiterate, a C# actor method with dynamic actor state must conform to the same signature restraints as “traditional” C# actor methods, with the following exceptions:

  • The actor state parameter is typed as “dynamic” instead of “IActorState”
  • The arguments are typed as an array of dynamics instead of an array of objects.

1.2.2. Actor Methods in Other Languages

The ActorFx framework has a goal of not only allowing actor methods to be consumed by non-.NET platforms, but also allowing actor methods to be written in non-.NET languages. Our initial foray into this space has been to allow actor methods to be written in JavaScript.

The mechanism by which to convey such actor method logic to an actor is the IActor.AddMethod method:

    string AddMethod(int clientSequenceNumber, int language, string methodname, string methodImpl); 

The IActor.AddMethod method allows client to theoretically provide actor methods in any language; at least, any language where the method’s implementation in string form can be easily transformed into an actual method. The “language” parameter specifies the language in which the method implementation is written, the “methodName” parameter specifies the name of the method, and the “methodImpl” parameter is a string containing the implementation of the method. Note that, by convention, the value of the “methodName” parameter must be the same as the name of the method being defined in the “methodImpl” parameter.

Currently, the Actor Framework supports two non-.NET languages for actor methods:  JavaScript (1) and Python (2).  See the SupportedLanguages enum to see the current state of the product.  We plan to add more choices in the future.

With this support, you can now do the something like this from a C# client:

            var bumpCountImpl = @"
                function bumpCount(state, value)
                {
                    var currcount = state.get(‘_count’);
                    var amountToIncrement = value;
                    state.set(‘_count’, currCount + value);
                    return currCount + value;
                }";

            client.AddMethod(SupportedLanguages.Javascript, "bumpCount", bumpCountImpl);

The JavaScript method “bumpCount” would now be registered as an actor method on the actor to which the client was connected. Note that the basic method signature constraints for C# methods also hold true for JavaScript:

  1. The function takes an IActorState parameter as its initial parameter, and method arguments in its subsequent parameters.
  2. The function returns an object.

1.3. The IActorState Interface

So what can actor method logic do via the IActorState object provided by the runtime? Here is the IActorState interface definition:

    public interface IActorState : IDisposable
    {
        // Actor state interaction
        void Set(string key, object value);
        object Get(string key);
        bool TryGet(string key, out object value);
        void Remove(string key);
        object CallMethod(string methodName, object[] args);
        Task Flush(); // "Commit", called by runtime

        // Event publication support
        void Publish(string eventType, object eventPayload);
        void Publish(string eventType, object eventPayload, params object[] eventPayloadQualifiers);

        // Allows for communications with other actors
        IActorProxy GetActorProxy(string externalActorName);

        // Allow an actor method to determine its own identity
        string GetId();
        string PartitionName { get; }

        // Non-.NET Language support
        void AddLanguagePack(SupportedLanguages language, ILanguagePack pack);
        void AddMethod(SupportedLanguages language, string methodName, string methodImpl);

        // Allows for creation and deletion of other actors
        bool CreateActor(string actorName, string creationString, int numPartitions = 1);
        void DeleteActor(string actorName);
        void Configure(String configurationString);
    }

The ensuing sections will discuss various functionality offered by IActorState.

1.3.1. Accessing the Actor’s State

Of course the most basic use of IActorState is to allow actor method logic to access and/or change the actor’s state. This can be accomplished with the following IActorState methods:

        // Actor state interaction
        void Set(string key, object value);
        object Get(string key);
        bool TryGet(string key, out object value);
        void Remove(string key);
        object CallMethod(string methodName, object[] args);

The Set, Get, TryGet and Remove methods are all similar to what you might see on any key/value store:

  • Set() will assign the specified value to the specified key, regardless of whether or not any value previously existed for that key.
  • Get() will return the value corresponding to the specified key. If the specified key does not exist, an exception is thrown.
  • TryGet() will return true if it is able to find the specified key (and in the process populate the specified value), false otherwise.
  • Remove() will remove the specified key.

The CallMethod() method is a convenience method that allows actor logic to call other actor methods.

1.3.2. Publish/Subscribe Support

An actor publishes notifications to its subscribers by two IActorState methods:

        // Pub/sub support
        void Publish(string eventType, object eventPayload);
        void Publish(string eventType, object eventPayload, params object[] eventPayloadQualifiers);

Event types are coded as strings. An event type might be something like “Collection.ElementAdded” or “Service.Shutdown”. Event notifications are subscribed to via the ActorClient class, or within an actor method by getting an IActorStateObservable from an IActorProxy. Each actor can define its own events and event names.

Event publications arrive to subscribers in the form of an ActorEvent:

    // The ActorEvent class provides a standardized wrapper for Actor event data.
    [Serializable]
    public class ActorEvent
    {
        public string EventSource { get; set; } // name of originating actor
        public string EventSourcePartitionName { get; set; } // Partition name of originating actor, if it was partitioned.
        public string EventType { get; private set; }
        public string EventPayload { get; private set; }
        public string[] EventPayloadQualifiers { get; private set; }

        public ActorEvent(string eventType);
        public ActorEvent(string eventType, object eventPayload);
        public ActorEvent(string eventType, object eventPayload, params object[] eventPayloadQualifiers);

        public T GetTypedPayload<T>() { ... }
        public T GetTypedPayloadQualifier<T>(int index) {...}
    }

Note that the EventPayload and EventPayloadQualifiers fields are stored as strings; these contain JSON representations of the event payload and any qualifiers for that payload. The GetTypedPayload and GetTypedPayloadQualifier methods can be used to convert the JSON to the expected data type.

1.3.3. Actor-to-Actor Communication

Actor-to-actor communication is facilitated through this IActorState method:

        // Allows for communications with other actors
        IActorProxy GetActorProxy(string externalActorName);

An IActorProxy is defined as follows:

    // An abstraction of an actor proxy, returned by IActorState.GetActorProxy()
    public interface IActorProxy
    {
        // Call a method on the actor
        void Command(string methodName, object[] parameters); // fire-and-forget

        // allow for return value handling
        IActorPromise Request(string methodName, object[] parameters); 

        // Obtain an observable to which you can subscribe for events
        IActorStateObservable this[string eventType] {get;}
    }

And IActorPromise and IActorStateObservable are defined as follows:

    public interface IActorPromise
    {
        string Result { get; } // Blocks waiting for result
        T GetTypedResult<T>(); // Allows conversion from JSON string to T

        // Allows for continuation-based handling of the result.                                              
        // This promise will be passed as the only argument to 
        // the continuation handler
        void ContinueWith(string methodName); 

        // Properties associated with exception handling
        bool IsFaulted { get; } // Check whether or not the operation faulted
        string ExceptionType { get; }
        string ExceptionMessage { get; }
        string ExceptionStackTrace { get; }
    }

    // Since we needed our Subscribe method to accept a (string) eventHandlerName, rather
    // than an IObserver, we had to make our own slightly tweaked observable interface.
    public interface IActorStateObservable
    {
        IDisposable Subscribe(string eventHandlerName);
    }

The IActorState.GetActorProxy method yields an IActorProxy for the specified actor. The IActorProxy allows you to call methods on the specified actor and subscribe to events that the specified actor publishes.

You can call methods on other actors in one of three ways:

  1. “Fire-and-forget”: The caller is not concerned with the result of the operation, or whether it faulted, or whether it completed at all. You use IActorProxy.Command to initiate a method call in such a manner.
  2. Synchronous completion: Synchronously wait for the method to complete. You use IActorProxy.Request to issue such a method call.
  3. Continuation-based result processing: Schedule a continuation, in the form of an actor method, to handle the completion of the actor method being called. This actor method will look like any other actor method, but will expect an IActorPromise as its first and only argument. You use IActorProxy.Request to initiate such a method call.

This (somewhat contrived) actor method demonstrates the use of all of these concepts:

        [ActorMethod]
        public static object SomeActorMethod(IActorState state, object[] args)
        {
            // Grab a proxy for actor named "otherActor"
            var proxy = state.GetActorProxy("fabric:/actor/adhoc/otherActor");

            // Call "MethodA" method on "otherActor", in fire-and-forget fashion
            proxy.Command("MethodA", new object[] { "foo", 42 });

            // Call "MethodB" method on "otherActor", and synchronously wait 
            // for the result.  This will throw if the method faults.
            var jsonResult = proxy.Request("MethodB", new object[] { 13 }).Result;

            // Call "MethodC" method on "otherActor", and schedule a continuation 
            // to process the result.
            var promise = proxy.Request("MethodC", new object[] { "hello" });
            promise.ContinueWith("CompletionHandler");

            // Grab an observable for event type "EventA" on "otherActor"
            var observable = proxy["EventA"];

            // Subscribe to "EventA" on "otherActor", handling events 
            // with method "EventAHandler".
            // Returning a disposable from Subscribe allows us to 
            // follow the Rx pattern of unsubscription via Dispose.
            var disposable = observable.Subscribe("EventAHandler");

            // It is safe to store the subscription disposal in 
            // our actor state for later use (i.e., unsubscription 
            // via Dispose).
            state.Set("MyDisposable", disposable);

            // You could subsequently unsubscribe from this event
            // at any time like this:
            //      var d = state.Get("MyDisposable") as IDisposable;
            //      d.Dispose();

            return true; // could be anything
        }

Note that it is NOT safe to store an IActorProxy (“proxy” in the example above) or an IActorStateObservable (“observable” in the example above) in the IActorState; attempting to do so will probably result in cryptic failure modes.

The “ContinuationHandler” method expects an IActorPromise as its first and only parameter, and could look something like this:

        [ActorMethod]
        public static object ContinuationHandler(IActorState state, object[] args)
        {
            var promise = args[0] as IActorPromise;
            if(promise.IsFaulted) { ... handle exception ... }
            else { ... handle completion, accessing promise.Result ...  }
            
            return null; // Nothing to return
        }

Similarly, an event handler method looks just like any other actor method, but it expects that the first (and only) parameter passed to it will be an ActorEvent. From the above example, “EventAHandler” might look something like this:

        [ActorMethod]
        public static object EventAHandler(IActorState state, object[] args)
        {
            var ae = args[0] as ActorEvent;
            // ... process ae ...

            return null;
        }

One final note on this subject: Beware of deadlocks. If you synchronously wait for method calls to other actors to complete, you introduce the potential for deadlock. Asynchronous completion processing via continuations or “fire-and-forget” method calls are almost always the preferred modes of calling out to other actors’ methods.

1.3.4. Support for Other Languages

For those thinking of adding support for additional non-.NET languages, be aware of a couple of methods on IActorState:

        // Language support
        void AddLanguagePack(SupportedLanguages language, ILanguagePack pack);
        void AddMethod(SupportedLanguages language, string methodName, string methodImpl);

The actor service, when it spins up, can add “language packs” via AddLanguagePack() to the underlying IActorState to support actor methods written in the desired non-.NET languages. The actor then simply delegates any IActor.AddMethod() calls to IActorState.AddMethod().

This is the interface to which a “language pack” must conform:

    // Common interface to which all language support packages must conform.
    public interface ILanguagePackage
    {
        // Query the language package as to whether or not it owns the specified method.
        // This will typically be ascertained by checking whether or not 
        // (somePrefix+methodName) exists as a key.
        bool OwnsMethod(string methodName);

        // Query the language package as to whether or not it owns the given methodKey,
        // which includes a prefix.
        bool OwnsKey(string methodKeyWithPrefix);

        // Add a method implementation pertinent to the language package.
        // No methodName is provided because one already exists.  (E.g.,
        // we're adding a method with a known key on a replica.)
        void AddMethod(string methodImpl);

        // Add a method implementation with the specified method name.
        void AddMethod(string methodName, string methodImpl);

        // Call a method.
        object CallMethod(string methodName, params object[] methodArgs); 
    }

The inaugural language pack is the JavascriptLanguagePackage, located in System.Threading.Actors.Languages.JavascriptLanguagePackage. If you would like to support additional languages, create another ILanguagePackage-implementing class in the manner of JavascriptLanguagePackage, and add your new language into the System.Threading.Actors.Languages.SupportedLanguages enum.

1.3.5. Creating and Deleting Other Actors

The following IActorState methods allow for the creation and deletion of other actors from an actor method:

        bool CreateActor(string actorName, string creationString, int numPartitions = 1);
        void DeleteActor(string actorName);

CreateActor() will attempt to create an actor with the specified actorName, using the specified implementation-specific creationString & the specified number of partitions. The creationString is a series of key-value pairs, where the keys and values are separated by ‘=’, and the key-value pairs are separated by ‘;’. Here is an example of a creationString:

"applicationType=fabric:/actor/adhoc;serviceType=EmptyActorReplica;numReplicas=3"

CreateActor() will return true if the specified actor already exists, false otherwise.

DeleteActor() will attempt to delete the actor with the specified name.

2. Actor Method Examples

In this section, we give some actor coding examples.

2.1. Counter

If you wanted your actor to support counter semantics, you could implement an actor method as follows:

        [ActorMethod]
        public static object IncrementCounter(IActorState state, object[] parameters)
        {
            // Grab the parameter
            var amountToIncrement = (int)parameters[0];

            // Grab the current counter value
            int count = 0; // default on first call
            object temp;
            if (state.TryGet("_count", out temp)) count = (int)temp;

            // Increment the counter
            count += amountToIncrement;

            // Store the new value
            state.Set("_count", count);

            // Publish new value to all subscribers
            state.Publish("Counter.Value", count);
            return count;
        }

Initially, the state for the actor would be empty. After an IncrementCounter call with a parameter of 5, the actor’s state would look like this:

Key

Value

“_count”

5

After another IncrementCounter call with a parameter of -2, the actor’s state would look like this:

Key

Value

“_count”

3

Note that the result of incrementing the counter is also published to all clients that subscribed to an event named “Counter.Value” on this instance of the actor. Any applications that subscribed to this event will be pushed a notification that the counter value has changed, and can update themselves appropriately.

Pretty simple, right? Let’s try something a little more complicated.

2.2. Stack

For a slightly more complicated example, let’s consider how we would implement a stack in terms of actor methods. The code would be as follows:

        [ActorMethod]
        public static object Push(IActorState state, object[] parameters)
        {
            // Grab the object to push
            var pushObj = parameters[0];
 
            // Grab the current size of the stack
            int stackSize = 0; // default on first call
            object temp;
            if (state.TryGet("_stackSize", out temp)) stackSize = (int)temp;

            // Store the newly pushed value
            var newKeyName = "_item" + stackSize;
            var newStackSize = stackSize + 1;
            state.Set(newKeyName, pushObj);
            state.Set("_stackSize", newStackSize );

            // Return the new stack size
            return newStackSize;
        }

        [ActorMethod]
        public static object Pop(IActorState state, object[] parameters)
        {
            // No parameters to grab

            // Grab the current size of the stack
            int stackSize = 0; // default on first call
            object temp;
            if (state.TryGet("_stackSize", out temp)) stackSize = (int)temp;

            // Throw on attempt to pop from empty stack
            if (stackSize == 0) throw new InvalidOperationException("Attempted to pop from an empty stack");

            // Remove the popped value, update the stack size
            int newStackSize = stackSize - 1;
            var targetKeyName = "_item" + newStackSize;
            var retrievedObject = state.Get(targetKeyName);
            state.Remove(targetKeyName);
            state.Set("_stackSize", newStackSize);

            // Return the popped object
            return retrievedObject;
        }

        [ActorMethod]
        public static object Size(IActorState state, object[] parameters)
        {
            // Grab the current size of the stack, return it
            int stackSize = 0; // default on first call
            object temp;
            if (state.TryGet("_stackSize", out temp)) stackSize = (int)temp;

            return stackSize;
        }

[Remember when examining these actor methods that each actor message is processed sequentially, so all actor methods are effectively called under lock. Thus there is no need to worry about thread-safety in the method logic.]

To summarize, the actor would contain the following items in its state:

  • The key “_stackSize” whose value is the current size of the stack.
  • One key “_itemXXX” corresponding to each value pushed onto the stack.

After the items “foo”, “bar” and “spam” had been pushed onto the stack, in that order, the actor’s state would look like this:

Key

Value

“_stackSize”

3

“_item0”

“foo”

“_item1”

“bar”

“_item2”

“spam”

A pop operation would yield the string “spam”, and leave the actor’s state looking like this:

Key

Value

“_stackSize”

2

“_item0”

“foo”

“_item1”

“bar”

3. The Actor Runtime Client

Once you have actors up and running in the Actor Runtime, you can connect to those actors and manipulate them via use of the ActorClient abstract base class.  Apps will indirectly use one of ActorClient’s concrete implementations, FabricActorClient or GatewayActorClient.  This is the FabricActorClient’s interface[3]:

    public class FabricActorClient : ActorClient
    {
        public FabricActorClient(Uri fabricUri, Uri actorUri, bool useGateway, String partitionName = null);
        public bool AddAssembly(string assemblyName, byte[] assemblyBytes);
        public Object CallMethod(string methodName, object[] parameters);
        public T CallMethod<T>(string methodName, object[] parameters);
        public IDisposable Subscribe(string eventType, IObserver<ActorEvent> eventObserver);
        public bool AddMethod(SupportedLanguages language, string methodName, string methodImpl);
    }

When constructing a FabricActorClient, you need to provide four parameters:

  • fabricUri: This is the URI associated with the Actor Runtime cluster on which your actor is running. When in a local development environment, this is typically “net.tcp://127.0.0.1:9000”. When in an Azure environment, this would be something like “net.tcp://<yourRuntimeDeployment>.cloudapp.net:9000”.
  • actorUri: This is the URI, within the Actor Runtime, that is associated with your actor. This would be something like “fabric:/actor/list/list1” or “fabric:/actor/adhoc/myFirstActor”.
  • useGateway: Set this to false when connecting to an actor in a local development environment, true when connecting to an Azure-hosted actor.
  • partitionName: If you are using multi-tenant actors, which specific piece of state to talk to.

The AddAssembly method allows you to transport an assembly to the actor. Typically that assembly would contain actor methods, effectively add behavior to or changing the existing behavior of the actor.

The CallMethod method allows you to call the method of that name on the actor. It returns the result returned by the actor.  The generic CallMethod<T> method will attempt to convert the result returned by the method into a “T”, and will throw an exception if such a conversion is not possible.

The Subscribe method allows you to subscribe to actor events. They will be published to the provided IObserver<ActorEvent> object.

The AddMethod argument allows you to add a method to an actor implemented in a language other than a common .NET language like C#. Currently, the only valid choices for “language” are JavaScript or Python.

4. Partitioning

One of the scalability challenges is getting the most use out of each machine. There are a finite number of sockets on each machine. Similarly, new actors require allocating state and entries in a naming table that can slow things down. The creation of service instances on the Actor Runtime is unfortunately slow and can gate our performance. Mapping one actor to one process is a horrible idea, and mapping one actor to shared state within a process can often be insufficient. To solve this, we looked into partitioned service instances for hosting multi-tenant actors.

Think of partitioning as creating buckets for actors on various machines. Those buckets can be empty, or you can fill them with multiple actors. By doing this, the cost of creating the buckets is amortized over the number of partitions. Each individual actor is created within its appropriate bucket, and receives its own isolated version of actor state.

clip_image002

Partitioned List Actors

This allows for actor creation times to be vastly faster, by about 3 orders of magnitude.

Now let’s draw a more complete picture. The Actor Runtime will use processes on various machines for each service type (like a list service, a dictionary service, our empty actor service, etc). Each process hosts zero or more service instances (both primaries and secondaries, though let’s ignore secondaries for now). Processes take a while to spin up and service instances are expensive to create. In a naïve hosting scenario without partitioning, consider a list service and a dictionary service with many instances, mapping to one collection each. Here is what will be running on a cluster. Service instances are in blue below.

clip_image004

Non-Partitioned Hosting

In the picture above, creating new individual collections requires creating new service instances, which is an expensive operation. With partitioning in the picture, we create fewer service instances.

clip_image006

Partitioned Hosting

Replication is done in the Actor Framework at the service instance level. So in this picture, “List A” and “List B” both share the same IActorState in terms of replication. However, this could lead to conflicts if both lists used a field of the same name, allowing one list to scribble over values from a separate list. The Actor Framework provides a further level of state isolation (an IsolatedActorState) that is a sub-space within an IActorState. So partitioned actors share the same replication mechanism & characteristics, but get their own isolated view of their state.

The mapping from name of an individual list is affected by partitioning. Without partitioning, the Actor Runtime will load balance at the service instance level and allocates them to machines in a reasonable way. However when using partitioning, the mapping from name of a list to a list partition is done by hashing the name then mapping it onto a range. For example, a name like “List A” may hash to a value between 1 and 100, and all hash values in the range 1-25 are mapped to list partition 1, 26-50 to list partition 2, etc.


[1] While our goal is to target the cloud, our initial Actor Runtime release only runs on development machines in an Azure development cluster.

[2] We accomplish this conversion by “cheating”; we encode .NET type information into the JSON strings passed as method arguments. This is clearly not portable to non-.NET platforms. At some point in the future, we will remove this .NET type information from the passed-in arguments, and pass arguments to actor methods as JSON strings instead of objects.  

[3] There’s actually more to it, but these are the important parts that one needs to know in order to begin experimenting with actors.

Last edited Aug 28, 2013 at 1:19 AM by briangru, version 33

Comments

No comments yet.