Freigeben über


Lowering the barrier of entry to the cloud: announcing the first release of Actor Framework from MS Open Tech (Act I)

From:

Erik Meijer, Partner Architect, Microsoft Corp.

Claudio Caldato, Principal Program Manager Lead, Microsoft Open Technologies, Inc.

 

There is much more to cloud computing than running isolated virtual machines, yet writing distributed systems is still too hard. Today we are making progress towards easier cloud computing as ActorFX joins the Microsoft Open Technologies Hub and announces its first, open source release. The goal for ActorFx is to provide a non-prescriptive, language-independent model of dynamic distributed objects, delivering a framework and infrastructure atop which highly available data structures and other logical entities can be implemented.

ActorFx is based on the idea of the Actor Model developed by Carl Hewitt, and further contextualized to managing data in the cloud by Erik Meijer in his paper that is the basis for the ActorFx project − you can also watch Erik and Carl discussing the Actor model in this Channel9 video.

What follows is a quick high-level overview of some of the basic ideas behind ActorFx. Follow our project on CodePlex to learn where we are heading and how it will help when writing the new generation of cloud applications.

ActorFx high-level Architecture

At a high level, an actor is simply a stateful service implemented via the IActor interface. That service maintains some durable state, and that state is accessible to actor logic via an IActorState interface, which is essentially a key-value store.

image

 

There are a couple of unique advantages to this simple design:

  • Anything can be stored as a value, including delegates. This allows us to blur the distinction between state and behavior – behavior is just state. That means that actor behavior can be easily tweaked “on-the-fly” without recycling the service representing the actor, similar to dynamic languages such as JavaScript, Ruby, and Python.
  • By abstracting the IActorState interface to the durable store, ActorFx makes it possible to “mix and match” back ends while keeping the actor logic the same. (We will show some actor logic examples later in this document.)

ActorFx Basics

The essence of the ActorFx model is captured in two interfaces: IActor and IActorState.

IActorState is the interface through which actor logic accesses the persistent data associated with an actor, it is the interface implemented by the “this” pointer.

 public interface IActorState
    {
        void Set(string key, object value);
        object Get(string key);
        bool TryGet(string key, out object value);
        void Remove(string key);
        Task Flush(); // "Commit"
    }

By design, the interface is an abstract key-value store. The Set, Get, TryGet and Remove methods are all similar to what you might find in any Dictionary-type class, or a JavaScript object. The Flush() method allows for transaction-like semantics in the actor logic; by convention, all side-effecting IActorState operations (i.e., Set and Remove) are stored in a local side-effect buffer until Flush() is called, at which time they are committed to the durable store (if the implementation of IActorState implements that).

The IActor interface

An ActorFx actor can be thought of as a highly available service, and IActor serves as the computational interface for that service. In its purest form, IActor would have a single “eval” method:

 public interface IActor
    {
        object Eval(Func<IActorState, object[], 
                    object> function, object[] parameters);
    }

That is, the caller requests that the actor evaluate a delegate, accompanied by caller-specified parameters represented as .NET objects, against an IActorState object representing a persistent data store. The Eval call eventually returns an object representing the result of the evaluation.

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 function passed to the IActor.Eval method takes an IActorState argument that can conceptually be thought of as the “this” pointer for the actor. So actor methods (described below) can be thought of as instance methods for the actor.

Actor Methods

In practice, passing delegates to actors can be tedious and error-prone. Therefore, the IActor interface calls methods using reflection, and allows for transmitting assemblies to the actor:

 public interface IActor
    {
        string CallMethod(string methodName, string[] parameters);
        bool AddAssembly(string assemblyName, byte[] assemblyBytes);
    }

Though the Eval method is still an integral part of the actor implementation, it is no longer part of the actor interface (at least for our initial release). Instead, it has been replaced in the interface by two methods:

  • The CallMethod method allows the user to call an actor method; it is translated internally to an Eval() call that looks up the method in the actor’s state, calls it with the given parameters, and then returns the result.
  • The AddAssembly method allows the user to transport an assembly containing actor methods to the actor.

There are two ways to define actor methods:

(1) Define the methods directly in the actor service, “baking them in” to the service.

myStateProvider.Set(

"SayHello",

(Func<IActorState, object[], object>)

delegate(IActorState astate, object[] parameters)

{

return "Hello!";

});

(2) Define the methods on the client side.

         [ActorMethod]
        public static object SayHello(IActorState state, object[] parameters)
        {
            return "Hello!";
        }

       

You would then transport them to the actor “on-the-fly” via the actor’s AddAssembly call.

All actor methods must have identical signatures (except for the method name):

  • They must return an object.
  • They must take two parameters:
    • An IActorState object to represent the “this” pointer for the actor, and
    • An object[] array representing the parameters passed into the method.

Additionally, actor methods defined on the client side and transported to the actor via AddAssembly must be decorated with the “ActorMethod” attribute, and must be declared as public and static.

Publication/Subscription Support

We wanted to be able to provide subscription and publication support for actors, so we added these methods to the IActor interface:

 public interface IActor
    {
        string CallMethod(string clientId, int clientSequenceNumber,
                          string methodName, string[] parameters);
        bool AddAssembly(string assemblyName, byte[] assemblyBytes);
        void Subscribe(string eventType);
        void Unsubscribe(string eventType);
        void UnsubscribeAll();
    }

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, event names and event payload formats. And the pub/sub feature is opt-in; it is perfectly fine for an actor to not support any events.

A simple example: 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 and return the new value
            state.Set("_count", count);
            return count;
        }

Initially, the state for the actor would be empty.

After an IncrementCounter call with parameters[0] set to 5, the actor’s state would look like this:

Key

Value

“_count”

5

 

 

 

After another IncrementCounter call with parameters[0] set to -2, the actor’s state would look like this:

Key

Value

“_count”

3

 

 

 

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

Example: 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;
        }

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”

 

 

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 FabricActorClient. This is the FabricActorClient’s interface:

 public class FabricActorClient
    {
        public FabricActorClient(Uri fabricUri, Uri actorUri, bool useGateway);
        public bool AddAssembly(string assemblyName, byte[] assemblyBytes, 
                                 bool replaceAllVersions = true);
        public Object CallMethod(string methodName, object[] parameters);
        public IDisposable Subscribe(string eventType, 
                                     IObserver<string> eventObserver);
    }

When constructing a FabricActorClient, you need to provide three 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://<yourDeployment>.cloudapp.net:9000”.
  • actorUri: This is the URI, within the ActorRuntime, 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.

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. Take note that the “replaceAllVersions” parameter is ignored.

What’s next?

This is only the beginning of a journey. The code we are releasing today is an initial basic framework that can be used to build a richer set of functionalities that will make ActorFx a valuable solution for storing and processing data on the cloud. For now, we are starting with a playground for developers who want to explore how this new approach to data storage and management on the cloud can become a new way to see old problems. We will keep you posted on this blog and you are of course more than welcome to follow our Open Source projects on our MSOpenTech CodePlex page. See you there!