Azure Service Fabric introduction - Part 3: Stateful services

Azure Service Fabric Introduction Part 3

In part 2 we managed to get an API up and running allowing external clients to connect and communicate with the Sheepishly platform. Now it's time to implement those API methods and make our application do something.

public class TrackerController : ApiController
{
    [HttpGet]
    [Route("")]
    public string Index()
    {
        return "Welcome to Sheepishly 1.0.0 - The Combleat Sheep Tracking Suite";
    }

    [HttpPost]
    [Route("locations")]
    public async Task<bool> Log(Location location)
    {
        var reporter = TrackerConnectionFactory.CreateLocationReporter();
        await reporter.ReportLocation(location);
        return true;
    }

    [HttpGet]
    [Route("sheep/{sheepId}/lastseen")]
    public async Task<DateTime?> LastSeen(Guid sheepId)
    {
        var viewer = TrackerConnectionFactory.CreateLocationViewer();
        return await viewer.GetLastReportTime(sheepId);
    }

    [HttpGet]
    [Route("sheep/{sheepId}/lastlocation")]
    public async Task<object> LastLocation(Guid sheepId)
    {
        var viewer = TrackerConnectionFactory.CreateLocationViewer();
        var location = await viewer.GetLastSheepLocation(sheepId);
        if (location == null)
            return null;

        return new { Latitude = location.Value.Key, Longitude = location.Value.Value };
    }
}

What we have here is an implementation of our three API methods. All of them follow the same pattern.

  1. Use the TrackerConnectionFactory to create a stateful service
  2. Perform some operation on the stateful service
  3. Return a result

In the remainder of this post we will create the stateful service and the factory used to instantiate it.

Creating the Stateful Service projects

First we add a new Stateful Service project by right-clicking on the Service Fabric project and choosing "Add -> New Fabric Service".

Creating a new Azure Service Fabric Stateful Service

I've chosen the name "Sheepishly.Tracker" for the project name. This will create a new Service Fabric stateful service project for us.

Our API will not reference the newly created service project directly. Instead we will create an additional project "Sheepishly.Tracker.Interfaces" that will contain the following.

  • Interfaces for the stateful services
  • Models used for communication with the service
  • TrackerConnectionFactory used for accessing the stateful service.

We will add a couple of files to our interfaces project, resulting in the following project structure.

Azure Service Fabric Stateful service project structure

The interface project

Before implementing the actual tracker, let's go ahead and get everything up and running in our interface project.

ILocationReporter

The ILocationReporter specifies functionality for reporting sheep locations to our service.

using System.Threading.Tasks;
using Microsoft.ServiceFabric.Services.Remoting;

namespace Sheepishly.Tracker.Interfaces
{
    public interface ILocationReporter : IService
    {
        Task ReportLocation(Location location);
    }
}
ILocationViewer

The ILocationViewer specifies functionality for querying the service about sheep locations.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.ServiceFabric.Services.Remoting;

namespace Sheepishly.Tracker.Interfaces
{
    public interface ILocationViewer : IService
    {
        Task<KeyValuePair<float, float>?> GetLastSheepLocation(Guid sheepId);
        Task<DateTime?> GetLastReportTime(Guid sheepId);
    }
}
Location

Location contains a model we can use when reporting new sheep locations to the service.

using System;

namespace Sheepishly.Tracker.Interfaces
{
    public class Location
    {
        public Guid SheepId { get; set; }
        public float Latitude { get; set; }
        public float Longitude { get; set; }
    }
}
TrackerConnectionFactory

Finally our factory provides a way for API to request an instance of our service.

using System;
using Microsoft.ServiceFabric.Services.Remoting.Client;

namespace Sheepishly.Tracker.Interfaces
{
    public static class TrackerConnectionFactory
    {
        private static readonly Uri LocationReporterServiceUrl = new Uri("fabric:/Sheepishly/Tracker");

        public static ILocationReporter CreateLocationReporter()
        {
            return ServiceProxy.Create<ILocationReporter>(0, LocationReporterServiceUrl);
        }
        public static ILocationViewer CreateLocationViewer()
        {
            return ServiceProxy.Create<ILocationViewer>(0, LocationReporterServiceUrl);
        }
    }
}

Note that we provide two static methods, one to obtain an instance of the service for reporting new sheep locations, and one for querying about sheep locations.

The factory methods use the Service Fabric "ServiceProxy" for requesting a representation of our service. The service is identified by a URI consisting of the name of our Service Fabric application, and the name we've chosen for the service.

When our stateless WebAPI service references the Sheepishly.Tracker.Interfaces project it has everyone required to start using our service. Except of course an actual implementation of the service. So let's go ahead and create that.

The stateful service project

There are two things we need to pay attention to when implementing our stateful service project.

  1. Wire up the service to let Service Fabric know that it exists, we do this in the Program.cs file that was generated for us

  2. Implement the actual service functionality in the Tracker.cs file

Wiring up Program.cs
using System;
using System.Diagnostics;
using System.Fabric;
using System.Threading;

namespace Sheepishly.Tracker
{
    internal static class Program
    {
        private static void Main()
        {
            try
            {
                using (FabricRuntime fabricRuntime = FabricRuntime.Create())
                {
                    fabricRuntime.RegisterServiceType("TrackerType", typeof(Tracker));
                    ServiceEventSource.Current.ServiceTypeRegistered(Process.GetCurrentProcess().Id, typeof(Tracker).Name);
                    Thread.Sleep(Timeout.Infinite);
                }
            }
            catch (Exception e)
            {
                ServiceEventSource.Current.ServiceHostInitializationFailed(e.ToString());
                throw;
            }
        }
    }
}

This is boilerplate to register our Tracker-service with Service Fabric and ensuring that it keeps running.

Implementing the Tracker service
using Microsoft.ServiceFabric.Data.Collections;
using Microsoft.ServiceFabric.Services.Communication.Runtime;
using Microsoft.ServiceFabric.Services.Runtime;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.ServiceFabric.Actors;
using Microsoft.ServiceFabric.Services.Remoting.Runtime;
using Sheepishly.Sheep.Interfaces;
using Sheepishly.Tracker.Interfaces;

namespace Sheepishly.Tracker
{
    internal sealed class Tracker : StatefulService, ILocationReporter, ILocationViewer
    {
        protected override IEnumerable<ServiceReplicaListener> CreateServiceReplicaListeners()
        {
            return new[] {
                new ServiceReplicaListener(
                    initParams => new ServiceRemotingListener<Tracker>(initParams, this)
                )
            };
        }

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

                var timestamp = DateTime.UtcNow;

                // TODO: Update individual sheep actor

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

        public async Task<DateTime?> GetLastReportTime(Guid sheepId)
        {
            using (var tx = StateManager.CreateTransaction())
            {
                var timestamps = await StateManager.GetOrAddAsync<IReliableDictionary<Guid, DateTime>>("timestamps");

                var timestamp = await timestamps.TryGetValueAsync(tx, sheepId);
                await tx.CommitAsync();

                return timestamp.HasValue ? (DateTime?)timestamp.Value : null;
            }
        }

        public async Task<KeyValuePair<float, float>?> GetLastSheepLocation(Guid sheepId)
        {
            throw new NotImplementedException();
        }
    }
}

In the next part of the series we will see how we can create actors to keep track of individual sheep data. For now though, our Tracker service will keep track of when we last recorded a location for a given sheep.

When a new location is recorded we use the StateManager to retrieve a reliable dictionary to hold our timestamps. The dictionary holds a timestamp for each unique sheep Id. Updates to the dictionary are transactional and replicated across instances of our service (if we have multiple instances).

With the Tracker service implemented we can now start using our API.

  • POST /api/locations will update the timestamp for a sheep in our service
  • GET /api/sheep/{sheepId}/lastseen will return the latest timestamp for a given sheep

What's next

In the next part of the series we will start tracking state for individual sheep using actors, read "Part 4 - Actors" now.

View Comments