| Microsoft Orleans / Aop / Technology / Dotnet Core / Distributed Systems

Interceptors and Aspect Oriented Programming in Microsoft Orleans

In today’s world, where services need to handle thousands of requests per second, a serious need for highly scalable systems arose, and as a solution, the actor model to meet it.

The idea of the actor model came from the 70’s, and since then, the world has seen many implementations and frameworks of it. The one I’m going to be using is Microsoft Orleans.

In the actor model, the actors are the building blocks of your system. In Orleans, they’re referred to as Grains.

They’re described as:

atomic units of isolation, distribution, and persistence

In Layman’s terms, your system is broken down into small executions, interacting with each other. There can be thousands of them. Millions.

Working with an actor framework requires a different outlook on how we build software. The logic in your code is going to be more fragmented, leading to the issue, that the number of places you’d potentially need to add metrics/logging or exception handling increases. Luckily, Orleans provides an easy way to implement the aspect oriented way of providing logging.

Aspect oriented programming addresses the problem of common patterns of logic being scattered across your codebase, providing a way of refactoring them and encapsulating them into a single well defined space.

Just based on the sheer amount of grain types one could possibly have in the system, the codebase could very soon show patterns of the following popping up:

try
{
  GrainFactory.GetGrain(0).PerformOperation();
}
catch(Exception e)
{
  _logger.Error($"Exception at call to {nameof(IMyGrain)}, message: {e.Message}");
  throw;
}

In Orleans, exceptions propagate through grain calls. Wouldn’t it be nice to catch them mid-call, and perform the logging in a unified place?

We can most certainly do that! Orleans has a variety of handy features to assist us with memory storage, telemetry or bootstrapping. What we’re going to need now though is an interceptor.

Interceptors, or otherwise known as grain call filters are called before- or after a call to a grain has been made, which makes it a perfect tool to execute common logic, for example: performing cleanup or logging.

Implementing the interceptor

In order to have a grain call filter in your project, all you really need to do, is to have a class implement the IGrainCallFilter interface and register it in the SiloHostBuilder.

IGrainCallFilter has a single method defined:

**Task Invoke(IGrainCallContext context)**

The IGrainCallContext looks like this:

MethodEffect
IAddressable GrainThe grain being invoked
MethodInfo MethodThe method being invoked
object ResultGet/Sets the results
Task Invoke()Invokes the request

First and foremost, let’s pass in the logger of our choice, via dependency injection. Dependency injection in Orleans is using the abstraction written by the developers of ASP.NET Core, so if you’re coming from that environment it’s going to be very familiar to you.

public GrainExceptionInterceptor(ILogger logger)
{
    _logger = logger;
}

Let’s implement the Invoke method. Really, all we’re doing is wrapping the invocation in a try-catch, and logging it if an exception happens. Straightforward stuff.

public Task Invoke(IGrainCallContext context)
{
    try
    {
        await context.Invoke();
    }
    catch (Exception ex)
    {
        logger.Error($"Exception at call to {nameof(context.Grain)}, message: {ex.Message}");
    }
    return context.Result;
}

All is left is to register the call filter in the SiloHostBuilder:

return new SiloHostBuilder()
  ...
  .ConfigureServices(
      services =>
      {
          ...
          services.AddGrainCallFilter();
      }).Build();

And we’re done! No more try-catches sprinkled in the code for the sole purpose of logging, all of that is now in one place, which makes for cleaner, easier to read code.

Grain calls from call filters

Let’s explore call filters a bit further. While in a call filter, you’re free to inject your preferred logger and, well, almost anything else:

Question

Taken from dotnet/orleans gitter

We can certainly make use of this. What if we’d like obtain information from a grain that needs to be logged? Let’s take our logger a step further, and make it report the number of grain activations the silo had for the grain type from which the exception was caught.

The GrainFactory can be used to invoke other grains. First of all, let’s make sure the project in which the call filter resides references the grain classes.

Then, we’ll inject the IGrainFactory into the call filter, like so:

public GrainExceptionInterceptor(ILogger logger, IGrainFactory grainFactory)
{
    _logger = logger;
    _grainFactory = grainFactory;
}

… then we’ll extend the invoke with the grain activation logging.

Getting this information might sound tricky. Fortunately Orleans keeps a track of a number of details of the silo in the ManagementGrain, including the number of activations of a given grain type.

public Task Invoke(IGrainCallContext context)
{
    try
    {
        await context.Invoke();
    }
    catch (Exception ex)
    {
        var typeNames = await _managementGrain.GetActiveGrainTypes();
        var grainName = typeNames.Where(x => x.EndsWith(nameof(context.Grain)));

        var grainStatistics = await _managementGrain.GetDetailedGrainStatistics(new [] {grainName});
        logger.Error($"Exception at call to {nameof(context.Grain)}, message: {ex.Message}. Number of {nameof(context.Grain)} activations: {grainStatistics.Length}");
    }
    return context.Result;
}

Nice and simple, isn’t it?

I should mention that this code is mainly for demonstration purposes, and possibly not what you’d use in production.

I hope this relatively easy example on call filters gave you an idea on how and why they are an important part of Orleans.

Akos Radler

Akos Radler

Full Stack .Net / React developer

Read More