Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"There is no active ActorContext" exception in Context.System while running tests #343

Closed
gunters63 opened this issue Jul 28, 2023 · 12 comments · Fixed by #346
Closed

"There is no active ActorContext" exception in Context.System while running tests #343

gunters63 opened this issue Jul 28, 2023 · 12 comments · Fixed by #346

Comments

@gunters63
Copy link

gunters63 commented Jul 28, 2023

Version Information
Version of Akka.NET? 1.5.10
Which Akka.NET Modules? Akka.Hosting

Describe the bug
There is no active ActorContext exception in Context.System while running tests
(when trying to create actor props with the DependencyResolver)

To Reproduce
Steps to reproduce the behavior:

  1. Clone https://github.com/gunters63/DIPropsFail
  2. Run tests

Expected behavior
Test is green

Actual behavior

There is no active ActorContext, this is most likely due to use of async operations from within this actor.
   at Akka.Actor.ActorBase.get_Context()
   at Akka.Actor.UntypedActor.get_Context()
   at DiPropsFail.DiPropsFailTest.NonRootActorWithDi.Props() in D:\temp\DIPropsFail\DIPropsFail\DiPropsFailTest.cs:line 36
   at DiPropsFail.DiPropsFailTest.NonRootActorWithDiTest() in D:\temp\DIPropsFail\DIPropsFail\DiPropsFailTest.cs:line 20

Screenshots
image

Environment
Rider unter Windows 11

Additional context
The strange things is:

  • sometimes the test is green when I run it in a bigger test suite together with other tests
  • when I copy this exact unit test file into the Akka.Hosting.TestKit.Tests folder of a cloned repo it works reliably.

I suspect this is maybe a racing condition.

@gunters63
Copy link
Author

It gets more strange,

if I add a new unit test project to my local clone of Akka.Hosting with exactly this lonely test, it also fails there. The test is green where I added it in Akka.Hosting.TestKit.Tests.

image

@gunters63
Copy link
Author

If I put this in the test:

    protected override async Task BeforeTestStart()
    {
        await Task.Delay(100);
    }

then sometimes the test is green. Making the delay larger doesn't help.

@gunters63
Copy link
Author

One thing I am wondering:

If I understand the code correctly, the currently executing actor sets the global static Context (InternalCurrentActorCellKeeper). That means if no Actor is currently executing, it will be null and generates this exception.

As long as you use Props inside an Actor to create child actors this cannot happen. But in Tests like this the code is normally executed while no Actor is currently executing.

This would explain why in my actual code the test is sometimes green when I execute all Tests because some run in parallel and they share the static class InternalCurrentActorCellKeeper.

I am currently refactoring our code base to use the new Akka.Hosting features. We have lots of tests where we use Props of an Actor to create (non-root) Actors during tests and the Props use the pattern DependencyResolver.For(Context.System).Props. Before the refactoring this was never a problem, even when I switched to Akka.Net 1.5.9 without refactoring yet.

@gunters63
Copy link
Author

gunters63 commented Jul 28, 2023

I added another test class in the reproduction repo:

This code does not use Akka.Hosting and Akka.Hosting.TestKit, it uses the TestKit from the main Akka.Net package.

using Akka.Actor;
using Akka.Actor.Setup;
using Akka.DependencyInjection;
using Akka.TestKit.Xunit2;
using Xunit.Abstractions;

namespace DiPropsFail;

public class DiPropsSuccessTest : TestKit, IClassFixture<AkkaDiFixture>
{
    public DiPropsSuccessTest(AkkaDiFixture fixture, ITestOutputHelper output) : base(FromActorSystemSetup(
        DependencyResolverSetup
            .Create(fixture.Provider)
            .And(BootstrapSetup.Create().WithConfig(DefaultConfig))), "TestActorSystem", output) { }

    private static ActorSystemSetup FromActorSystemSetup(ActorSystemSetup setup)
    {
        var bootstrapOptions = setup.Get<BootstrapSetup>();
        var bootstrap = bootstrapOptions.HasValue ? bootstrapOptions.Value : BootstrapSetup.Create();
        return setup.And(bootstrap);
    }

    [Fact]
    public void NonRootActorWithDiTest()
    {
        Sys.ActorOf(NonRootActorWithDi.Props());
    }

    // ReSharper disable once ClassNeverInstantiated.Local
    private class NonRootActorWithDi : ReceiveActor
    {
        public static Props Props() =>
            DependencyResolver.For(Context.System).Props<NonRootActorWithDi>();
    }
}

I borrowed some code for this from the main Akka repo (AkkaDiFixture, FromActorSystemSetup).
This is a similar setup I was using before the refactoring (I had my own TestKit base class).

This test is always green.

@gunters63
Copy link
Author

I simplified the repro now to this:

Working (using Akka.TestKit.XUnit2):

using Akka.Actor;
using Akka.TestKit.Xunit2;
using Xunit.Abstractions;

namespace DiPropsFail;

public class DiPropsSuccessTest : TestKit
{
    public DiPropsSuccessTest(ITestOutputHelper output) : base(nameof(DiPropsSuccessTest), output)
    {
    }

    [Fact]
    public void NonRootActorWithDiTest()
    {
        Assert.NotNull(MyActor.Context);
    }

    private class MyActor : ReceiveActor
    {
        public new static IUntypedActorContext Context => ReceiveActor.Context;
    }

Failing (using Akka.Hosting.TestKit):

using Akka.Actor;
using Akka.Hosting;
using Akka.Hosting.TestKit;
using Xunit.Abstractions;

namespace DiPropsFail;

public class DiPropsFailTest : TestKit
{
    public DiPropsFailTest(ITestOutputHelper output) : base(nameof(DiPropsFailTest), output)
    {
    }

    protected override void ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider)
    {
    }

    [Fact]
    public void NonRootActorWithDiTest()
    {
        Assert.NotNull(MyActor.Context);
    }

    private class MyActor : ReceiveActor
    {
        public new static IUntypedActorContext Context => ReceiveActor.Context;
    }
}

I suspect this problem has the same underlying reason as issue #237, in the Akka.Net TestKit there is an implicit Sender Actor and with that a non-null Context, in Akka.Hosting.TestKit not.

@gunters63
Copy link
Author

Thanks for the fix.
I was curious about the solution and looked at the change set.

Now this code:

            if (!(this is INoImplicitSender))
            {
                InternalCurrentActorCellKeeper.Current = (ActorCell)((ActorRefWithCell)testActor).Underlying;
            }

exists in two places, in the InitializeTest of TestKitBase (Akka itself) and InitializeAsync of TestKit (Akka.Hosting).
It gets executed twice now for all Tests using Akka.Hosting which looks a little odd.

It looks like the underlying reason is that TestKit (Akka.Hosting) has the interface IAsyncLifetime, so the Testrunner is executing the constructor of TestKitBase and running the Test method (with running InitializeAsync() just before that) on two different threads, hence the problem with the ThreadStaticAttribute.

Wouldn't it be better to change TestKitBase itself to use IAsyncLifetime and move the relevant code in InitializeTest to InitializeAsync? TestKit (Akka.Hosting) could override the method and call the base method first.

I actually tried this, but had problems with TestProbe, which is derived from TestKitBase. Putting

            InitializeAsync().GetAwaiter().GetResult();

in the constructor there as a quick hack would at least make everything compile, but about 40 tests were still red.

Of course this would also make Akka.Net depending on Xunit.

@Aaronontheweb
Copy link
Member

Cc @Arkatufus

@Arkatufus
Copy link
Contributor

Arkatufus commented Aug 2, 2023

That was one of the original problem I encountered when I tried to code Akka.Hosting.TestKit, Akka.TestKit was designed to be unit test framework agnostic, you're supposed to be able to plug in any framework you want to it (Xunit, Nunit, MSTest, what have you).

IAsyncLifetime is, sadly, Xunit specific. So we can not use what you're suggesting.

Another approach would be to gut the simple context implementation and make it trully thread safe, but the change would create a systematic drop in performance for Akka.NET due to thread locks etc.

@gunters63
Copy link
Author

gunters63 commented Aug 2, 2023

@Arkatufus:

Maybe its possible to have the cake and eat it? A internal TestKit base implementation with an virtual async initialisation function (not using the IAsyncLifetime interface from XUnit). And two derived classes, one compatible with the current TestKit API (with blocking sync code in the constructor) and another one with the IAsyncLifetime interface which can be used by Akka.Hosting.TestKit.

I think even if a Testkit implements IAsyncLifetime, you can still use it with every Test framework. You just have to call the InitializeAsync function yourself because the frameworks doesn't do it for you. Most Test frameworks supports async tests and global hooks (BeforeAsync/AfterAsync).

@Arkatufus
Copy link
Contributor

That still doesn't address the possibility that the ActorSystem and the unit test framework test runner is running on different threads, bringing back the ThreadStatic issue we tried to avoid in the first place.

@gunters63
Copy link
Author

Yes, i also don't see how to avoid this. I was just wondering if you could get rid of the same workaround in two places :). But that's probably not worth the effort.

@gunters63
Copy link
Author

I still get null reference exceptions sometimes when accessing Context.System inside Tests running under the new Akka.Hosting testkit.

It is really hard to reproduce. I have a actor test project here with about 100 tests, and it happens maybe once every 5-10 times when I run all tests. When I run single tests, it doesn't seem to happen. It also happens with different tests (using DependencyResolver.For(Context.System).Props).

The test project has a xunit.runner.json file containing parallelizeAssembly and parallelizeTestCollections set to false and I also told Rider to use only a single test runner thread.

Maybe it could be reproduced by some kind of stress test but I don't really know whats the best way to do this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants