Skip to content

Latest commit

 

History

History
106 lines (82 loc) · 3.55 KB

AK1005.md

File metadata and controls

106 lines (82 loc) · 3.55 KB
uid title
AK1005
Akka.Analyzers Rule AK1005 - "Must close over `Sender` or `Self`"

AK1005 - Warning

When accessing Sender or Self inside a lambda expression passed as an asynchronous method argument, you must always close over Sender or Self to ensure that the property is captured before the method is invoked, because there is no guarantee that asynchronous context will be preserved when the lambda is invoked inside the method.

Cause

Akka.NET tries its best to preserve asynchronous context if asynchronous codes were to run inside the actor. However, there are times where this is impossible to enforce, especially when a code that accesses variables that were thread execution specific such as the actor Context, Sender, and Self is being invoked outside the actor threading context.

For example, lets assume that you're using an asynchronous third party API package that invokes a callback when it completes its operation:

using System;
using System.Threading.Tasks;

namespace ThirdPartyApi;

public sealed class JobManager
{
    public async Task SubmitJobAsync(string job, Func<Task> asyncCallback)
    {
        // This breaks asynchronous context
        await ConnectToServer().ConfigureAwait(false); 
        // other codes here
        
        await asyncCallback();
    }
    
    private async Task ConnectToServer()
    {
        // codes here
    }
}

In the code above, the ConfigureAwait(false) call breaks the asynchronous context before the callback method were invoked. If this third party API code were to be used inside Akka.NET and Context, Sender, or Self were accessed inside the callback, we will see an error during runtime:

using System;
using System.Threading.Tasks;
using Akka.Actor;
using ThirdPartyApi;

namespace MyApplication.Actors;

public sealed class MyActor : ReceiveActor
{
    private readonly JobManager _jobManager = new();
    
    public MyActor()
    {
        ReceiveAsync<string>(async job => {
            _jobManager.SubmitJobAsync(job, async () => 
            {
                // This callback will not work
                Context.Sender.Tell($"{job} submitted.", Self);
            })
        });
    }
}
NotSupportedException
There is no active ActorContext, this is most likely due to use of async operations from within this actor.
  at MyApplication.Actors.MyActor`1<>c__DisplayClass48_0.<<InvokeTestMethodAsync>b__1>d.MoveNext() in \src\MyApplication.Actors\MyActor.cs:line 16

Resolution

While working with a third party API, it is most likely that you will not have a way to modify their codes to make them compatible with your asynchronous flow. To avoid this entire category of problem, we should close over any access to Sontext.Self, Context.Sender, Self, and Sender property in a local variable.

For the non-working example above, we can fix it by modifying our code:

using System;
using System.Threading.Tasks;
using Akka.Actor;
using ThirdPartyApi;

namespace MyApplication.Actors;

public sealed class MyActor : ReceiveActor
{
    private readonly JobManager _jobManager = new();
    
    public MyActor()
    {
        ReceiveAsync<string>(async job => {
            // Store Context.Sender and Self inside local variable
            var sender = Context.Sender;
            var self = Self;
            
            _jobManager.SubmitJobAsync(job, async () => 
            {
                // Use local variables instead of accessing Context directly
                sender.Tell($"{job} submitted.", self);
            })
        });
    }
}