Skip to content

Commit

Permalink
Span<char>-ifying ActorPath and Address parsing
Browse files Browse the repository at this point in the history
Abstraction of ServiceProvider, Improving Akka.DependencyInjection (#4814)

* Abstraction of ServiceProvider

* introduced non-breaking Akka.DependencyInjection API changes

* fixed unit tests / Props bug

* fixed up DelegateInjectionSpecs

* Added type checking for `Props(Type type, params object[] args)`

* fixed non-generic `Props()` method

Co-authored-by: Aaron Stannard <aaron@petabridge.com>

completed work on ActorPath parsing
  • Loading branch information
Aaronontheweb committed May 28, 2021
1 parent 99afc0e commit 6b2240e
Show file tree
Hide file tree
Showing 23 changed files with 740 additions and 134 deletions.
22 changes: 19 additions & 3 deletions src/benchmark/Akka.Benchmarks/Actor/ActorPathBenchmarks.cs
Expand Up @@ -16,12 +16,16 @@ public class ActorPathBenchmarks
{
private ActorPath x;
private ActorPath y;
private ActorPath _childPath;
private Address _sysAdr = new Address("akka.tcp", "system", "127.0.0.1", 1337);
private Address _otherAdr = new Address("akka.tcp", "system", "127.0.0.1", 1338);

[GlobalSetup]
public void Setup()
{
x = new RootActorPath(new Address("akka.tcp", "system", "127.0.0.1", 1337), "user");
y = new RootActorPath(new Address("akka.tcp", "system", "127.0.0.1", 1337), "system");
x = new RootActorPath(_sysAdr, "user");
y = new RootActorPath(_sysAdr, "system");
_childPath = x / "parent" / "child";
}

[Benchmark]
Expand All @@ -45,7 +49,19 @@ public bool ActorPath_Equals()
[Benchmark]
public string ActorPath_ToString()
{
return x.ToString();
return _childPath.ToString();
}

[Benchmark]
public string ActorPath_ToSerializationFormat()
{
return _childPath.ToSerializationFormat();
}

[Benchmark]
public string ActorPath_ToSerializationFormatWithAddress()
{
return _childPath.ToSerializationFormatWithAddress(_otherAdr);
}
}
}
34 changes: 34 additions & 0 deletions src/benchmark/Akka.Benchmarks/Actor/NameAndUidBenchmarks.cs
@@ -0,0 +1,34 @@
//-----------------------------------------------------------------------
// <copyright file="NameAndUidBenchmarks.cs" company="Akka.NET Project">
// Copyright (C) 2009-2021 Lightbend Inc. <http://www.lightbend.com>
// Copyright (C) 2013-2021 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
//-----------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Text;
using Akka.Actor;
using Akka.Benchmarks.Configurations;
using BenchmarkDotNet.Attributes;

namespace Akka.Benchmarks.Actor
{
[Config(typeof(MicroBenchmarkConfig))]
public class NameAndUidBenchmarks
{
public const string ActorPath = "foo#11241311";

[Benchmark]
public NameAndUid ActorCell_SplitNameAndUid()
{
return ActorCell.SplitNameAndUid(ActorPath);
}

[Benchmark]
public (string name, int uid) ActorCell_GetNameAndUid()
{
return ActorCell.GetNameAndUid(ActorPath);
}
}
}
Expand Up @@ -21,7 +21,7 @@ namespace Akka.DependencyInjection.Tests

public class ActorServiceProviderPropsWithScopesSpecs : AkkaSpec, IClassFixture<AkkaDiFixture>
{
public ActorServiceProviderPropsWithScopesSpecs(AkkaDiFixture fixture, ITestOutputHelper output) : base(ServiceProviderSetup.Create(fixture.Provider)
public ActorServiceProviderPropsWithScopesSpecs(AkkaDiFixture fixture, ITestOutputHelper output) : base(DependencyResolverSetup.Create(fixture.Provider)
.And(BootstrapSetup.Create().WithConfig(TestKitBase.DefaultConfig)), output)
{

Expand All @@ -30,7 +30,7 @@ public ActorServiceProviderPropsWithScopesSpecs(AkkaDiFixture fixture, ITestOutp
[Fact(DisplayName = "DI: actors who receive an IServiceScope through Props should dispose of their dependencies upon termination")]
public void ActorsWithScopedDependenciesShouldDisposeUponStop()
{
var spExtension = ServiceProvider.For(Sys);
var spExtension = DependencyResolver.For(Sys);
var props = spExtension.Props<ScopedActor>();

// create a scoped actor using the props from Akka.DependencyInjection
Expand Down Expand Up @@ -60,11 +60,23 @@ public void ActorsWithScopedDependenciesShouldDisposeUponStop()
deps2.Dependencies.All(x => x.Disposed).Should().BeFalse();
}

[Fact(DisplayName = "DI: should be able to start actors with untyped Props")]
public void ShouldStartActorWithUntypedProps()
{
var spExtension = DependencyResolver.For(Sys);
var props = spExtension.Props(typeof(ScopedActor));

// create a scoped actor using the props from Akka.DependencyInjection
var scoped1 = Sys.ActorOf(props, "scoped1");
scoped1.Tell(new FetchDependencies());
var deps1 = ExpectMsg<CurrentDependencies>();
}

[Fact(DisplayName =
"DI: actors who receive an IServiceScope through Props should dispose of their dependencies and recreate upon restart")]
public void ActorsWithScopedDependenciesShouldDisposeAndRecreateUponRestart()
{
var spExtension = ServiceProvider.For(Sys);
var spExtension = DependencyResolver.For(Sys);
var props = spExtension.Props<ScopedActor>();

// create a scoped actor using the props from Akka.DependencyInjection
Expand Down Expand Up @@ -95,7 +107,7 @@ public void ActorsWithScopedDependenciesShouldDisposeAndRecreateUponRestart()
"DI: actors who receive a mix of dependencies via IServiceScope should dispose ONLY of their scoped dependencies and recreate upon restart")]
public void ActorsWithMixedDependenciesShouldDisposeAndRecreateScopedUponRestart()
{
var spExtension = ServiceProvider.For(Sys);
var spExtension = DependencyResolver.For(Sys);
var props = spExtension.Props<MixedActor>();

// create a scoped actor using the props from Akka.DependencyInjection
Expand Down Expand Up @@ -134,7 +146,7 @@ public void ActorsWithMixedDependenciesShouldDisposeAndRecreateScopedUponRestart
public void ActorsWithNonDiDependenciesShouldStart()
{
// <CreateNonDiActor>
var spExtension = ServiceProvider.For(Sys);
var spExtension = DependencyResolver.For(Sys);
var arg1 = "foo";
var arg2 = "bar";
var props = spExtension.Props<NonDiArgsActor>(arg1, arg2);
Expand Down Expand Up @@ -182,7 +194,7 @@ public void ActorsWithNonDiDependenciesShouldStart()
public void ServiceProvider_Props_should_support_copying()
{
// <CreateNonDiActor>
var spExtension = ServiceProvider.For(Sys);
var spExtension = DependencyResolver.For(Sys);
var arg1 = "foo";
var arg2 = "bar";
var props = spExtension.Props<NonDiArgsActor>(arg1, arg2).WithRouter(new RoundRobinPool(10).WithSupervisorStrategy(new OneForOneStrategy(
Expand Down
@@ -1,4 +1,10 @@
using System;
//-----------------------------------------------------------------------
// <copyright file="DelegateInjectionSpecs.cs" company="Akka.NET Project">
// Copyright (C) 2009-2021 Lightbend Inc. <http://www.lightbend.com>
// Copyright (C) 2013-2021 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
//-----------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
Expand Down Expand Up @@ -73,7 +79,7 @@ public async Task DI_should_be_able_to_retrieve_singleton_using_delegate_from_in
internal class ParentActor : UntypedActor
{
public static Props Props(ActorSystem system) =>
ServiceProvider.For(system).Props<ParentActor>();
DependencyResolver.For(system).Props<ParentActor>();

private readonly IActorRef _echoActor;

Expand Down Expand Up @@ -114,7 +120,7 @@ public AkkaService(IServiceProvider serviceProvider)

public Task StartAsync(CancellationToken cancellationToken)
{
var setup = ServiceProviderSetup.Create(_serviceProvider)
var setup = DependencyResolverSetup.Create(_serviceProvider)
.And(BootstrapSetup.Create().WithConfig(TestKitBase.DefaultConfig));

ActorSystem = ActorSystem.Create("TestSystem", setup);
Expand Down
Expand Up @@ -18,7 +18,7 @@ namespace Akka.DependencyInjection.Tests
{
public class ServiceProviderSetupSpecs : AkkaSpec, IClassFixture<AkkaDiFixture>
{
public ServiceProviderSetupSpecs(AkkaDiFixture fixture, ITestOutputHelper output) : base(ServiceProviderSetup.Create(fixture.Provider)
public ServiceProviderSetupSpecs(AkkaDiFixture fixture, ITestOutputHelper output) : base(DependencyResolverSetup.Create(fixture.Provider)
.And(BootstrapSetup.Create().WithConfig(TestKitBase.DefaultConfig)), output)
{

Expand All @@ -27,29 +27,29 @@ public ServiceProviderSetupSpecs(AkkaDiFixture fixture, ITestOutputHelper output
[Fact(DisplayName = "DI: Should access Microsoft.Extensions.DependencyInjection.IServiceProvider from ServiceProvider ActorSystem extension")]
public void ShouldAccessServiceProviderFromActorSystemExtension()
{
var sp = ServiceProvider.For(Sys);
var dep = sp.Provider.GetService<AkkaDiFixture.ITransientDependency>();
var sp = DependencyResolver.For(Sys);
var dep = sp.Resolver.GetService<AkkaDiFixture.ITransientDependency>();
dep.Should().BeOfType<AkkaDiFixture.Transient>();

var dep2 = sp.Provider.GetService<AkkaDiFixture.ITransientDependency>();
var dep2 = sp.Resolver.GetService<AkkaDiFixture.ITransientDependency>();
dep2.Should().NotBe(dep); // the two transient instances should be different

// scoped services should be the same
var scoped1 = sp.Provider.GetService<AkkaDiFixture.IScopedDependency>();
var scoped2 = sp.Provider.GetService<AkkaDiFixture.IScopedDependency>();
var scoped1 = sp.Resolver.GetService<AkkaDiFixture.IScopedDependency>();
var scoped2 = sp.Resolver.GetService<AkkaDiFixture.IScopedDependency>();

scoped1.Should().Be(scoped2);

// create a new scope
using (var newScope = sp.Provider.CreateScope())
using (var newScope = sp.Resolver.CreateScope())
{
var scoped3 = newScope.ServiceProvider.GetService<AkkaDiFixture.IScopedDependency>();
var scoped3 = newScope.Resolver.GetService<AkkaDiFixture.IScopedDependency>();
scoped1.Should().NotBe(scoped3);
}

// singleton services should be the same
var singleton1 = sp.Provider.GetService<AkkaDiFixture.ISingletonDependency>();
var singleton2 = sp.Provider.GetService<AkkaDiFixture.ISingletonDependency>();
var singleton1 = sp.Resolver.GetService<AkkaDiFixture.ISingletonDependency>();
var singleton2 = sp.Resolver.GetService<AkkaDiFixture.ISingletonDependency>();

singleton1.Should().Be(singleton2);
}
Expand All @@ -67,7 +67,7 @@ public void ShouldAccessServiceProviderFromActorSystemExtension()
{
Action getSp = () =>
{
var sp = ServiceProvider.For(Sys);
var sp = DependencyResolver.For(Sys);
};

getSp.Should().Throw<ConfigurationException>();
Expand Down
@@ -0,0 +1,125 @@
//-----------------------------------------------------------------------
// <copyright file="ServiceProvider.cs" company="Akka.NET Project">
// Copyright (C) 2009-2021 Lightbend Inc. <http://www.lightbend.com>
// Copyright (C) 2013-2021 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
//-----------------------------------------------------------------------

using System;
using Akka.Actor;
using Akka.Configuration;
using Akka.Event;
using Microsoft.Extensions.DependencyInjection;

namespace Akka.DependencyInjection
{
/// <summary>
/// Provides users with immediate access to the <see cref="IDependencyResolver"/> bound to
/// this <see cref="ActorSystem"/>, if any.
/// </summary>
public sealed class DependencyResolver : IExtension
{
public DependencyResolver(IDependencyResolver resolver)
{
Resolver = resolver;
}

/// <summary>
/// The globally scoped <see cref="IDependencyResolver"/>.
/// </summary>
/// <remarks>
/// Per https://docs.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-guidelines - please use
/// the appropriate <see cref="IServiceScope"/> for your actors and the dependencies they consume. DI is typically
/// not used for long-lived, stateful objects such as actors.
///
/// Therefore, injecting transient dependencies via constructors is a bad idea in most cases. You'd be far better off
/// creating a local "request scope" each time your actor processes a message that depends on a transient dependency,
/// such as a database connection, and disposing that scope once the operation is complete.
///
/// Actors are not MVC Controllers. Actors can live forever, have the ability to restart, and are often stateful.
/// Be mindful of this as you use this feature or bad things will happen. Akka.NET does not magically manage scopes
/// for you.
/// </remarks>
public IDependencyResolver Resolver { get; }

public static DependencyResolver For(ActorSystem actorSystem)
{
return actorSystem.WithExtension<DependencyResolver, DependencyResolverExtension>();
}

/// <summary>
/// Uses a delegate to dynamically instantiate an actor where some of the constructor arguments are populated via dependency injection
/// and others are not.
/// </summary>
/// <remarks>
/// YOU ARE RESPONSIBLE FOR MANAGING THE LIFECYCLE OF YOUR OWN DEPENDENCIES. AKKA.NET WILL NOT ATTEMPT TO DO IT FOR YOU.
/// </remarks>
/// <typeparam name="T">The type of actor to instantiate.</typeparam>
/// <param name="args">Optional. Any constructor arguments that will be passed into the actor's constructor directly without being resolved by DI first.</param>
/// <returns>A new <see cref="Akka.Actor.Props"/> instance which uses DI internally.</returns>
public Props Props<T>(params object[] args) where T : ActorBase
{
return Resolver.Props<T>(args);
}

/// <summary>
/// Used to dynamically instantiate an actor where some of the constructor arguments are populated via dependency injection
/// and others are not.
/// </summary>
/// <remarks>
/// YOU ARE RESPONSIBLE FOR MANAGING THE LIFECYCLE OF YOUR OWN DEPENDENCIES. AKKA.NET WILL NOT ATTEMPT TO DO IT FOR YOU.
/// </remarks>
/// <typeparam name="T">The type of actor to instantiate.</typeparam>
/// <returns>A new <see cref="Akka.Actor.Props"/> instance which uses DI internally.</returns>
public Props Props<T>() where T : ActorBase
{
return Resolver.Props<T>();
}

/// <summary>
/// Used to dynamically instantiate an actor where some of the constructor arguments are populated via dependency injection
/// and others are not.
/// </summary>
/// <remarks>
/// YOU ARE RESPONSIBLE FOR MANAGING THE LIFECYCLE OF YOUR OWN DEPENDENCIES. AKKA.NET WILL NOT ATTEMPT TO DO IT FOR YOU.
/// </remarks>
/// <param name="type">The type of actor to instantiate.</param>
/// <returns>A new <see cref="Akka.Actor.Props"/> instance which uses DI internally.</returns>
public Props Props(Type type)
{
return Resolver.Props(type);
}

/// <summary>
/// Used to dynamically instantiate an actor where some of the constructor arguments are populated via dependency injection
/// and others are not.
/// </summary>
/// <remarks>
/// YOU ARE RESPONSIBLE FOR MANAGING THE LIFECYCLE OF YOUR OWN DEPENDENCIES. AKKA.NET WILL NOT ATTEMPT TO DO IT FOR YOU.
/// </remarks>
/// <param name="type">The type of actor to instantiate.</param>
/// <param name="args">Optional. Any constructor arguments that will be passed into the actor's constructor directly without being resolved by DI first.</param>
/// <returns>A new <see cref="Akka.Actor.Props"/> instance which uses DI internally.</returns>
public Props Props(Type type, params object[] args)
{
return Resolver.Props(type, args);
}
}

/// <summary>
/// INTERNAL API
/// </summary>
public sealed class DependencyResolverExtension : ExtensionIdProvider<DependencyResolver>
{
public override DependencyResolver CreateExtension(ExtendedActorSystem system)
{
var setup = system.Settings.Setup.Get<DependencyResolverSetup>();
if (setup.HasValue) return new DependencyResolver(setup.Value.DependencyResolver);

var exception = new ConfigurationException("Unable to find [DependencyResolverSetup] included in ActorSystem settings." +
" Please specify one before attempting to use dependency injection inside Akka.NET.");
system.EventStream.Publish(new Error(exception, "Akka.DependencyInjection", typeof(DependencyResolverExtension), exception.Message));
throw exception;
}
}
}

0 comments on commit 6b2240e

Please sign in to comment.