Advanced Developer Conference 2019

This year at traditional German ADC, I will talk about how to design and operate modern backends based on Actor Programming Model. This is session about reasoning about computation, states and partitioning. All this I will present based on Akka.NET open source framework. I will show how to design actors and how to deploy them to Docker container and then operate in Azure with help of Azure Container Registry and Azure Container Instances.

231225_adc

Imagine you have a nit of code, which does some computation.

   public class MyComputeActor : UntypedActor
   {
       protected override void OnReceive(object message)
       {
           // This is a CPU intensive operation.
           Console.WriteLine($"MyCompute : {{message} - Sender : {Sender}");
       }
    }

Now, we want to run this code on one physical node. This can be achived with following statement:


var remoteAddress1 = Address.Parse($"akka.tcp://DeployTarget@localhost:8090");

var remoteActor1 =
                system.ActorOf(
                    Props.Create(() => new MyComputeActor())
                    .WithDeploy(Deploy.None.WithScope(new mRemoteScope(remoteAddress1))), "customer1");

var remoteActor2 = system.ActorOf(Props.Create(() => new MyComputeActor())
                       .WithDeploy(Deploy.None.WithScope(new RemoteScope(remoteAddress2))),
                       "customer2");

To make this happen, we need to create a host for MyComputeActor. Following is the full implementation of host, which I have used in this example:

using Akka.Actor;
using Akka.Configuration;
using Microsoft.Extensions.Configuration;
using System;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;


namespace AkkaCluster
{
    class Program
    {
       /// <summary>
       /// --AKKAPORT 8089 --AKKAPUBLICHOST localhost --AKKASEEDHOST localhost:8089
       /// </summary>
       /// <param name="args"></param>
        static void Main(string[] args)
        {
            var builder = new ConfigurationBuilder();
            builder.AddEnvironmentVariables();
            builder.AddCommandLine(args);

            IConfigurationRoot netConfig = builder.Build();

            var assembly = Assembly.Load(new AssemblyName("AkkaShared, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"));

            Console.ForegroundColor = ConsoleColor.Cyan;

            Console.WriteLine("Cluster running...");

            int port = 8090;
            string publicHostname = "localhost";
            string seedhostsStr = String.Empty;

            if (netConfig["AKKAPORT"] != null)
                int.TryParse(netConfig["AKKAPORT"], out port);

            if (netConfig["AKKAPUBLICHOST"] != null)
                publicHostname = netConfig["AKKAPUBLICHOST"];

            if (netConfig["AKKASEEDHOSTS"] != null)
            {
                seedhostsStr = netConfig["AKKASEEDHOSTS"];
            }

            string config = @"
                akka {
                    actor.provider = ""Akka.Remote.RemoteActorRefProvider, Akka.Remote""
                    remote {
                        helios.tcp {
                            port = @PORT
                                public-hostname = @PUBLICHOSTNAME
                                hostname = 0.0.0.0
                            }
                    }
                    cluster {
                        seed-nodes = [@SEEDHOST]
                    }
            }";
         
            config = config.Replace("@PORT", port.ToString());
            config = config.Replace("@PUBLICHOSTNAME", publicHostname);
            
            if (seedhostsStr.Length > 0)
            {
                var seedHosts = seedhostsStr.Split(',');
                seedHosts = seedHosts.Select(h => h.TrimStart(' ').TrimEnd(' ')).ToArray();
                StringBuilder sb = new StringBuilder();
                bool isFirst = true;

                foreach (var item in seedHosts)
                {
                    if (isFirst == false)
                        sb.Append(", ");

                    sb.Append($"\"akka.tcp://DeployTarget@{item}\"");
                    //example: seed - nodes = ["akka.tcp://ClusterSystem@localhost:8081"]

                    isFirst = false;
                }

                config = config.Replace("@SEEDHOST", sb.ToString());
            }
            else
                config = config.Replace("@SEEDHOST", String.Empty);

            Console.WriteLine(config);

            using (var system = ActorSystem.Create("DeployTarget", ConfigurationFactory.ParseString(config)))
            {
                var cts = new CancellationTokenSource();
                AssemblyLoadContext.Default.Unloading += (ctx) => cts.Cancel();
                Console.CancelKeyPress += (sender, cpe) =>
                {
                    CoordinatedShutdown.Get(system).Run(reason: CoordinatedShutdown.ClrExitReason.Instance).Wait();
                    cts.Cancel();
                };

                system.WhenTerminated.Wait();

                return;
            }
        }

        public static Task WhenCancelled(CancellationToken cancellationToken)
        {
            var tcs = new TaskCompletionSource<bool>();
            cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).SetResult(true), tcs);
            return tcs.Task;
        }
    }
}

As next, I will create a docker container, which contains a host code. To do this I used following dockerfile:

FROM microsoft/dotnet:2.2-runtime AS base
WORKDIR /app

FROM microsoft/dotnet:2.2-sdk AS build
WORKDIR /src
COPY AkkaCluster/AkkaCluster.csproj AkkaCluster/
COPY AkkaShared/AkkaShared.csproj AkkaShared/
RUN dotnet restore AkkaCluster/AkkaCluster.csproj
COPY . .
WORKDIR /src/AkkaCluster
RUN dotnet build AkkaCluster.csproj -c Release -o /app

FROM build AS publish
RUN dotnet publish AkkaCluster.csproj -c Release -o /app

FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "AkkaCluster.dll"]

Then I created container with following command:

docker build --rm -f "AkkaCluster\Dockerfile" -t akkacluster:v1 .

As next, I will push the container to Azure Container Registry:

docker tag akkacluster:v1 myreg.azurecr.io/akka-sum-cluster:v1
docker push myreg.azurecr.io/akka-sum-cluster:v1

Then I create two nodes:

az container create -g  RG-AKKA-SUMCLUSTER --name akka-sum-host1 --image damir.azurecr.io/akka-sum-cluster:v1 --ports 8089 --ip-address Public --cpu 2 --memory 1 --dns-name-label akka-sum-host1 --environment-variables AKKAPORT=8089 AKKASEEDHOSTS="akka-sum-host1.westeurope.azurecontainer.io:8089,akka-sum-host2.westeurope.azurecontainer.io:8089" AKKAPUBLICHOST=akka-sum-host1.westeurope.azurecontainer.io --registry-username myreg --registry-password ***

az container create -g  RG-AKKA-SUMCLUSTER --name akka-sum-host2 --image damir.azurecr.io/akka-sum-cluster:v1 --ports 8089 --ip-address Public --cpu 2 --memory 1 --dns-name-label akka-sum-host2 --environment-variables AKKAPORT=8089 AKKASEEDHOSTS="akka-sum-host1.westeurope.azurecontainer.io:8089,akka-sum-host2.westeurope.azurecontainer.io:8089" AKKAPUBLICHOST=akka-sum-host2.westeurope.azurecontainer.io --registry-username myreg --registry-password ***

Last two statements will provision a container instance with hosting code and run it in ACI.

231339_adc2

Finally I need to change the URL of actors before activating:

  
 var remoteAddress1 = Address.Parse($"akka.tcp://DeployTarget@akka-sum-host1.westeurope.azurecontainer.io:8089");
  
 var remoteActor1 =
                  system.ActorOf(
                      Props.Create(() => new MyComputeActor())
                      .WithDeploy(Deploy.None.WithScope(new mRemoteScope(remoteAddress1))), "customer1");

var remoteActor2 = system.ActorOf(Props.Create(() => new MyComputeActor())
                         .WithDeploy(Deploy.None.WithScope(new RemoteScope(remoteAddress2))),
                         "customer2");

This demo at ADC19 session, which I also shown at WinDays2019 demonstrates, how to run compute logic at different nodes as Azure Container Instance.
To recap, this demo shows how to implement a distributed system as Actor Programming Model and run it as serverless service.


comments powered by Disqus