Azure Service Fabric introduction - Part 4: Actors

Azure Service Fabric Introduction Part 4

In the previous chapter of this introduction we wrote a stateful service to keep track of our herd of sheep. Whenever a sheep reports its whereabouts the service will update a timestamp for the sheep, keeping track of when it was last seen.

But rather than storing the latest location of the sheep in the stateful service, it makes more sense to introduce a sheep "actor".

Actors are a good approach when you are dealing with large numbers of small individual objects each needing their own state. In our case we want to track each sheep individually, each with its own location history that can be written to or read from independently from all the other sheep in the herd.

Adding a stateful actor service

Let's get cracking, adding a new actor service for our project, we can call it Sheepishly.Sheep since what we are trying to model here are individual sheep.

Create Azure Service Fabric Actor service

When we create our Actor service we get two projects added to our solution.

  • Sheepishly.Sheep
  • Sheepishly.Sheep.Interfaces

The reason for this is the same as with our stateful service. The services in our Service Fabric application don't reference each other directly.

While our stateful service will be interacting with our Sheep actors, we never reference the Sheep-project, and we never just create a new Sheep object from within our stateful service.

Instead we define an interface which we can reference and let Service Fabric handle the actual actor objects for us.

The interface project

Before digging into the actual implementation of the Sheep actor we will define the ISheep interface, and create a small factory we can use to get references to the individual Sheep actors.

ISheep.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.ServiceFabric.Actors;

namespace Sheepishly.Sheep.Interfaces
{
    public interface ISheep : IActor
    {
        Task<KeyValuePair<float, float>> GetLatestLocation();
        Task SetLocation(DateTime timestamp, float latitude, float longitude);
    }
}

Here we have defined a small interface for our actor. It derives from IActor and has two methods.

  • SetLocation
  • GetLatestLocation

To keep things simple our GetLatestLocation will just return a KeyValuePair containing latitude and longitude as floats. To step this up a bit we could of course have created a small Location-model containing the values.

SheepConnectionFactory.cs
using System;
using Microsoft.ServiceFabric.Actors;
using Microsoft.ServiceFabric.Actors.Client;

namespace Sheepishly.Sheep.Interfaces
{
    public static class SheepConnectionFactory
    {
        private static readonly Uri SheepServiceUrl = new Uri("fabric:/Sheepishly/SheepActorService");

        public static ISheep GetSheep(ActorId actorId)
        {
            return ActorProxy.Create<ISheep>(actorId, SheepServiceUrl);
        }
    }
}

Our factory simply wraps the Service Fabric ActorProxy, which we can use to get a reference to one of our sheep based on an ActorId. Notice the Uri which follows the convention for addressing services in Service Fabric.

ActorId and the actor life cycle

When we want to get a reference to a Sheep actor we use the ActorProxy.Create method and let Service Fabric handle it for us.

The first time we request an actor that doesn't already exist Service Fabric will simply go ahead and create that actor for us. We don't have to deal with making sure a specific actor exists, this is handled for us by Service Fabric.

The ISheep implementation

Now that we have an interface, we can implement it in our Sheepishly.Sheep project.

Sheep.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using Microsoft.ServiceFabric.Actors.Runtime;
using Sheepishly.Sheep.Interfaces;

namespace Sheepishly.Sheep
{
    internal class Sheep : Actor, ISheep
    {
        [DataContract]
        internal sealed class LocationAtTime
        {
            public DateTime Timestamp { get; set; }
            public float Latitude { get; set; }
            public float Longitude { get; set; }
        }

        [DataContract]
        internal sealed class SheepState
        {
            [DataMember]
            public List<LocationAtTime> LocationHistory { get; set; }
        }

        protected override async Task OnActivateAsync()
        {
            var state = await StateManager.TryGetStateAsync<SheepState>("State");
            if (!state.HasValue)
                await StateManager.AddStateAsync("State", new SheepState { LocationHistory = new List<LocationAtTime>() });
        }

        public async Task SetLocation(DateTime timestamp, float latitude, float longitude)
        {
            var state = await StateManager.GetStateAsync<SheepState>("State");
            state.LocationHistory.Add(new LocationAtTime() { Timestamp = timestamp, Latitude = latitude, Longitude = longitude });

            await StateManager.AddOrUpdateStateAsync("State", state, (s, actorState) => state);
        }

        public async Task<KeyValuePair<float, float>> GetLatestLocation()
        {
            var state = await StateManager.GetStateAsync<SheepState>("State");
            var location = state.LocationHistory.OrderByDescending(x => x.Timestamp).Select(x => 
                new KeyValuePair<float, float>(x.Latitude, x.Longitude)
            ).FirstOrDefault();

            return location;
        }
    }
}

Let's discuss the individual parts of the implementation. First, notice that the Sheep-class inherits from Actor and implements our ISheep-interface.

The state

At the very top of our implementation we have defined a couple of classes which will hold the state of our Sheep.

[DataContract]
internal sealed class LocationAtTime
{
    public DateTime Timestamp { get; set; }
    public float Latitude { get; set; }
    public float Longitude { get; set; }
}

[DataContract]
internal sealed class SheepState
{
    [DataMember]
    public List<LocationAtTime> LocationHistory { get; set; }
}

The state has to be serializable. For our Sheep, the state consists of a list of reported locations, each with a timestamp of when the location was reported.

OnActivateAsync
protected override async Task OnActivateAsync()
{
    var state = await StateManager.TryGetStateAsync<SheepState>("State");
    if (!state.HasValue)
        await StateManager.AddStateAsync("State", new SheepState { LocationHistory = new List<LocationAtTime>() });
}

Next we override the OnActivateAsync-method from Actor. This is run the first time the actor is introduced and can be used to create the initial state for the actor.

Actors have a StateManager which stores the state of the Actor. We can store any number of state objects using the StateManager, in our example we will just be dealing with one instance of "SheepState", which we will store in the StateManager named "State".

SetLocation()
public async Task SetLocation(DateTime timestamp, float latitude, float longitude)
{
    var state = await StateManager.GetStateAsync<SheepState>("State");
    state.LocationHistory.Add(new LocationAtTime() { Timestamp = timestamp, Latitude = latitude, Longitude = longitude });

    await StateManager.AddOrUpdateStateAsync("State", state, (s, actorState) => state);
}

This is the first of the two methods we defined in our interface. It simply fetches our state using the StateManager and adds a new "LocationAtTime"-entry to it before committing it.

Access to the state is handled by Service Fabric and any requests to our Sheep actor is handled in turn.

GetLatestLocation()
public async Task<KeyValuePair<float, float>> GetLatestLocation()
{
    var state = await StateManager.GetStateAsync<SheepState>("State");
    var location = state.LocationHistory.OrderByDescending(x => x.Timestamp).Select(x => 
        new KeyValuePair<float, float>(x.Latitude, x.Longitude)
    ).FirstOrDefault();

    return location;
}

Same as with adding entries, here we get the state using the StateManager and return the last location that was reported.

Registering the actor

Now we have a complete implementation of a Sheep actor and just need to register it with Service Fabric in order to start using it from our other services. Our Sheepishly.Sheep project is no more than an ordinary console application, and we simply register the actor type in our main method.

Program.cs
using System;
using System.Threading;
using Microsoft.ServiceFabric.Actors.Runtime;

namespace Sheepishly.Sheep
{
    internal static class Program
    {
        private static void Main()
        {
            try
            {
                ActorRuntime.RegisterActorAsync<Sheep>((context, information) => new ActorService(context, information, () => new Sheep()));
                Thread.Sleep(Timeout.Infinite);
            }
            catch (Exception e)
            {
                ActorEventSource.Current.ActorHostInitializationFailed(e.ToString());
                throw;
            }
        }
    }
}

Using the Sheep actor from our stateless service

We can access our Sheep actors from any Service Fabric service in our application.

The way we have structured the example application, all queries to individual sheep goes through our tracker service, and we can now go ahead and implement the missing parts of the tracker service.

Reporting locations
public async Task ReportLocation(Location location)
{
    using (var tx = StateManager.CreateTransaction())
    {
        var timestamps = await StateManager.GetOrAddAsync<IReliableDictionary<Guid, DateTime>>("timestamps");
        var sheepIds = await StateManager.GetOrAddAsync<IReliableDictionary<Guid, ActorId>>("sheepIds");

        var timestamp = DateTime.UtcNow;

        // Update sheep
        var sheepActorId = await sheepIds.GetOrAddAsync(tx, location.SheepId, ActorId.CreateRandom());
        await SheepConnectionFactory.GetSheep(sheepActorId).SetLocation(timestamp, location.Latitude, location.Longitude);

        // Update service with new timestamp
        await timestamps.AddOrUpdateAsync(tx, location.SheepId, DateTime.UtcNow, (guid, time) => timestamp);
        await tx.CommitAsync();
    }
}

Here we have added a couple of lines under the "Update sheep"-comment. First we create a new ActorId and store it in the Tracker-service, connecting it to the Id of the sheep.

Next we use our SheepConnectionFactory to get a reference to that sheep using the actorId and report the new location.

Finding last sheep location
public async Task<KeyValuePair<float, float>?> GetLastSheepLocation(Guid sheepId)
{
    using (var tx = StateManager.CreateTransaction())
    {
        var sheepIds = await StateManager.GetOrAddAsync<IReliableDictionary<Guid, ActorId>>("sheepIds");

        var sheepActorId = await sheepIds.TryGetValueAsync(tx, sheepId);
        if (!sheepActorId.HasValue)
            return null;

        var sheep = SheepConnectionFactory.GetSheep(sheepActorId.Value);
        return await sheep.GetLatestLocation();
    }
}

Finally we implement the Tracker method to query for last reported sheep location. It works the same way. First we find the actorId for a sheep, we then use the factory to get a reference to the Sheep actor and return the latest location.

Running it all together

All communication with our application happens through the API we set up earlier. When we created our tracker service in the previous chapter we hooked up all the API methods needed to both report and query sheep whereabouts.

With our Sheep actor in place and our updated tracker service we are now ready to deploy the Service Fabric application to our local cluster and take it for a spin.

Let's run it from Visual Studio, which will create a local cluster and install our application. It will take a few seconds, and then we are ready to try it out using Postman.

POSTing locations

POSTing location to Azure Service Fabric application

GETting latest location

GETting latest location from Azure Service Fabric application

There we go, a working Azure Service Fabric application using a stateless API endpoint, a stateful tracker service and actors to track individual sheep.

Next chapter

In the next chapter of this introduction we will be looking at how we can move the application out of our local machine and into the cloud.

View Comments