How to Build Headless Akka.NET Services with IHostedService

Creating Stand-alone Akka.NET Services with Microsoft.Extensions.Hosting and IHostedService

At Akka.NET’s inception, most of the server-side code samples we produced all demonstrated how to build so-called “headless” Akka.NET services as outright Windows Services primarily using libraries like Topshelf.

.NET has changed tremendously since then and become a truly cross-platform runtime that is capable of working with and within all of the amazing innovations that have sprung from the greater global open source community, such as Kubernetes and Docker. Thus, it’s time we modernize our guidance for how to build headless services in the age of .NET Core / .NET 5 and beyond.

We’ve produced a YouTube video demonstrating how to Host Akka.NET using IHostedService which demonstrates the modern approach to configuring and hosting stand-alone Akka.NET services:

This blog post compliments the YouTube video!

“Headless” Service?

“Headless service” is a concept derived from “headless” computers - computer systems without a monitor, mouse, and keyboard. Headless computers could only be accessed over the network.

A “headless” service is one that doesn’t expose a public-facing API or UI of any kind - no HTTP APIs, no gRPC, etc. Headless services can still communicate with other services via private messaging systems such as Akka.Remote, Kafka, RabbitMQ, and so forth - but they run in the background of most networked applications.

Hosting Akka.NET with IHostedService

The IHostedService interface was added as part of the .NET Core 2.0 update via the Microsoft.Extensions.Hosting NuGet package, but that package targets .NET Standard 2.0 and thus is available to .NET Framework 4.6.1 and upward.

The goals of IHostedService are threefold:

  1. To make it easy to run background services inside ASP.NET Core applications, which is another method by which Akka.NET is commonly deployed;
  2. To make it possible to compose many different services together into the same host process; and
  3. To provide a standardized model for integrating configuration, logging, and dependency injection into .NET services.

We can take advantage of those in Akka.NET - especially dependency injection support.

AkkaService

So using the Cluster.WebCrawler code sample, let’s take a look at simple but complete and functional IHostedService implementation for one of the four services that runs inside WebCrawler:

See the source.

public sealed class AkkaService : IHostedService
{
    private IActorRef _apiMaster;
    private ActorSystem ClusterSystem;
    private IActorRef _downloadMaster;

    private readonly IServiceProvider _serviceProvider;

    public TrackerService(IServiceProvider sp){
        _serviceProvider = sp;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
         var config = HoconLoader.ParseConfig("tracker.hocon");
         var bootstrap = BootstrapSetup.Create()
            .WithConfig(config.ApplyOpsConfig()) // injects environment variables into HOCON
            .WithActorRefProvider(ProviderSelection.Cluster.Instance); // launch Akka.Cluster

        // N.B. `WithActorRefProvider` isn't actually needed here 
        // the HOCON file already specifies Akka.Cluster

        // enable DI support inside this ActorSystem, if needed
        var diSetup = ServiceProviderSetup.Create(_serviceProvider);

        // merge this setup (and any others) together into ActorSystemSetup
        var actorSystemSetup = bootstrap.And(diSetup);

        // start ActorSystem
        ClusterSystem = ActorSystem.Create("webcrawler", actorSystemSetup);

        ClusterSystem.StartPbm(); // start Petabridge.Cmd (https://cmd.petabridge.com/)

        // instantiate actors
        _apiMaster = ClusterSystem.ActorOf(Props.Create(() => new ApiMaster()), "api");
        _downloadMaster = ClusterSystem.ActorOf(Props.Create(() => new DownloadsMaster()), "downloads");
        
        return Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        // strictly speaking this may not be necessary - terminating the ActorSystem would also work
        // but this call guarantees that the shutdown of the cluster is graceful regardless
         await CoordinatedShutdown.Get(ClusterSystem).Run(CoordinatedShutdown.ClrExitReason.Instance);
    }
}

The IHostedService interface includes two simple methods:

  • Task StartAsync(CancellationToken cancellationToken) - we start our ActorSystem and any of our top-level actors here and
  • Task StopAsync(CancellationToken cancellationToken) - we terminate the ActorSystem here, in this instance using CoordinatedShutdown and return the Task from that.

Notice that in our implementation we also have a constructor that takes a dependency on an IServiceProvider - that interface is our DI container and we can hang onto to a copy of it for use with Akka.DependencyInjection later.

HostBuilder

After we’ve created our Akka.NET IHostedService we need to actually run it inside Program.cs, and this is accomplished via the .NET Generic Host, which we can instantiate via the HostBuilder class:

See the source.

internal class Program
{
    private static async Task Main(string[] args)
    {
        var host = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddLogging();

                // register our host service
                services.AddHostedService<AkkaService>();

            })
            .ConfigureLogging((hostContext, configLogging) =>
            {
                configLogging.AddConsole();

            })
            .UseConsoleLifetime()
            .Build();

        await host.RunAsync();
    }
}

The IHostedService contract guarantees that when your generic host starts the IHostedService.StartAsync method will be called.

When the process attempts to exit the .NET Generic Host guarantees that all of your IHostedService instances will have their StopAsync methods called and usually those will be allowed to run to completion. This helps us guarantee that our Akka.NET nodes will gracefully leave the cluster, for instance.

Akka.NET Project Templates

So all of this is pretty straightforward and gives us a repeatable way of hosting, configuring, and safely disposing our Akka.NET services. If you want to use this type of functionality inside your own applications we’ve made a set of dotnet new template for this exact purpose.

Install

To install Petabridge’s Akka.NET templates, execute the following command using the .NET CLI:

PS> dotnet new -i "Petabridge.Templates::*"

Use

To create a brand new headless Akka.NET application that uses this IHostedService setup, just run the following dotnet new command:

PS> dotnet new pb-akka-cluster -n [app name]

This will give you a brand new project that is ready to be Dockerized and deployed as a .NET 5 headless service.

We also include a second template that will do the same, but for running Akka.NET as a background service inside an ASP.NET application:

PS> dotnet new pb-akka-web -n [app name]

Happy coding!

If you liked this post, you can share it with your followers or follow us on Twitter!
Written by Aaron Stannard on April 15, 2021

 

 

Observe and Monitor Your Akka.NET Applications with Phobos

Did you know that Phobos can automatically instrument your Akka.NET applications with OpenTelemetry?

Click here to learn more.