actor or standard class? - how to explore the differences
Many times I was talking with developers about using an actor system as a way to gain productivity and scalability. What I found that most peoples had no experience and no time to try it out, moreover existing projects and teams are on the hesitating side as new stuff will require a learning time (and who want to learn when all is gooooing goooood and sloooow).
Let me share some concepts that were able to help me to understand and use actor system in C#, so we are talking about akka.net.
First listing contains a class and an actor. Please read it carefully.
listing 1:
namespace FirstStepsInAkka.net | |
{ | |
public class FirstLook | |
{ | |
public void AddBookToLibrary(Parameters parameters) { | |
// do something here | |
} | |
} | |
public class FirstLookActor : ReceiveActor { | |
public FirstLookActor() | |
{ | |
Receive<Parameters>(HandleThat); | |
} | |
private void HandleThat(Parameters obj) | |
{ | |
//do something here | |
} | |
} | |
public class Parameters | |
{ | |
public int BookId { get; set; } | |
public string Title { get; set; } = string.Empty; | |
public string AuthorName { get; set; }= string.Empty; | |
} | |
} |
What we can see at the first glance that actor class is inheriting after ReceiveActor, has a strange Receive<T>(). In standard approach when we create an instance of class c, then we are calling their methods and passing parameters to do the job. In actor system, we are not calling methods inside an actor - we are passing messages - so we need to receive them and then handle.
This makes it a bit harder to debug in a standard way, as we were used to trace all the stack and calls from end to end, and here we have a invisible message bus, that disconnects sender and receiver.
So at this moment we can say that the actor need more operations to conduct an action. That's true for the first action.
Let's consider a scenario, when our actor receives a calls from an api-endpoint to create a book entry and same in standard approach.
To demonstrate that we will use a minimal api project, and let's try to go to the details as they will be revelated later.
As a task we need to save incoming book details into a text file, just for simplicity we will skip database interactions.
Below listing is using minimal apis with a .Net project template named pb-akka-web (here details)
listing 2 all in one...
using System.Diagnostics; | |
using Akka.Actor; | |
using Akka.Cluster.Hosting; | |
using Akka.Event; | |
using Akka.Hosting; | |
using Akka.Remote.Hosting; | |
using ASimpleApiwithActor; | |
using Petabridge.Cmd.Cluster; | |
using Petabridge.Cmd.Cluster.Sharding; | |
using Petabridge.Cmd.Host; | |
using Petabridge.Cmd.Remote; | |
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development"; | |
var builder = WebApplication.CreateBuilder(args); | |
builder.Configuration | |
.AddJsonFile("appsettings.json") | |
.AddJsonFile($"appsettings.{environment}.json", optional: true) | |
.AddEnvironmentVariables(); | |
builder.Logging.ClearProviders().AddConsole(); | |
builder.Services.AddControllers(); | |
var app = builder.Build(); | |
app.UseRouting(); | |
var system = ActorSystem.Create("example"); | |
var bookActor = system.ActorOf(Props.Create(() => new BookActor()), "book"); | |
var bookWriterForClass = new BookWriter($"trash\\fromClass_{DateTime.UtcNow.Date.ToFileTimeUtc()}.txt"); | |
app.UseEndpoints(endpoints => | |
{ | |
endpoints.MapControllers(); | |
endpoints.MapPost("/actor", async (HttpContext context, BookData bd) => | |
{ | |
bd.BookId = context.TraceIdentifier; | |
var resp = await bookActor.Ask<string>(bd); | |
await context.Response.WriteAsync(resp); | |
}); | |
endpoints.MapPost("/class", async (HttpContext context, BookData bd) => | |
{ | |
bd.BookId = context.TraceIdentifier; | |
bookWriterForClass.WriteBook(bd); | |
await context.Response.WriteAsync(bd.BookId); | |
}); | |
}); | |
await app.RunAsync(); | |
public class BookWriter | |
{ | |
private readonly string _fileName; | |
public BookWriter(string fileName) | |
{ | |
_fileName = fileName; | |
var s = File.Create(_fileName); | |
s.Close(); | |
} | |
public void WriteBook(BookData bd) | |
{ | |
File.AppendAllText(_fileName, $"{DateTime.UtcNow.ToFileTimeUtc()},{bd.BookId}, {bd.AuthorName}, {bd.Title}\n"); | |
} | |
} | |
public sealed class BookActor : ReceiveActor | |
{ | |
private readonly ILoggingAdapter _log = Context.GetLogger(); | |
private BookWriter _bookWriter; | |
public BookActor() | |
{ | |
_bookWriter = new BookWriter($"trash\\fromActor_{DateTime.UtcNow.Date.ToFileTimeUtc()}.txt"); | |
Receive<BookData>(_ => | |
{ | |
_bookWriter.WriteBook(_); | |
Sender.Tell(_.BookId); | |
}); | |
} | |
} | |
public class BookData | |
{ | |
public string BookId { get; set; } = string.Empty; | |
public string Title { get; set; } = string.Empty; | |
public string AuthorName { get; set; } = string.Empty; | |
} |
So let's focus on the code for a moment.
We have 2 post endpoints, that are receiving same data object with book details. One endpoint is passing data to actor and the other is processing it in a classic way.
Now please have a moment to think:
- what can go wrong with this code?
- how it will behave when we will have concurrent requests?
- do you understand the flow with an actor?
I forgot to downsize the requests amount, but after registry fix and restart we were able to run this 50k requests. As we can see, 5 users generated 2x throughput, so we hit a bottle-neck with our file writer.
The results:
What??? Slower, and errors? Are they server errors or JMeter tcp again?
Ach. that is a real 500 from the server,
Comments
Post a Comment