From d27f0e62c3fbf46dc12dd56e42ffc7278be84ce0 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Fri, 12 Aug 2022 02:50:38 +0700 Subject: [PATCH] Improve WithExtensions so it can be used universally (#92) --- src/Akka.Hosting.Tests/ExtensionsSpecs.cs | 99 +++++++++++++++++++ src/Akka.Hosting.Tests/XUnitLogger.cs | 84 ++++++++++++++++ src/Akka.Hosting.Tests/XUnitLoggerProvider.cs | 26 +++++ src/Akka.Hosting/AkkaConfigurationBuilder.cs | 62 ++++++++++++ 4 files changed, 271 insertions(+) create mode 100644 src/Akka.Hosting.Tests/ExtensionsSpecs.cs create mode 100644 src/Akka.Hosting.Tests/XUnitLogger.cs create mode 100644 src/Akka.Hosting.Tests/XUnitLoggerProvider.cs diff --git a/src/Akka.Hosting.Tests/ExtensionsSpecs.cs b/src/Akka.Hosting.Tests/ExtensionsSpecs.cs new file mode 100644 index 00000000..35d0b935 --- /dev/null +++ b/src/Akka.Hosting.Tests/ExtensionsSpecs.cs @@ -0,0 +1,99 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2013-2022 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Event; +using Akka.TestKit.Xunit2.Internals; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace Akka.Hosting.Tests; + +public class ExtensionsSpecs +{ + private readonly ITestOutputHelper _helper; + + public ExtensionsSpecs(ITestOutputHelper helper) + { + _helper = helper; + } + + public async Task StartHost(Action testSetup) + { + var host = new HostBuilder() + .ConfigureLogging(builder => + { + builder.AddProvider(new XUnitLoggerProvider(_helper, LogLevel.Information)); + }) + .ConfigureServices(service => + { + service.AddAkka("TestActorSystem", testSetup); + }).Build(); + + await host.StartAsync(); + return host; + } + + [Fact(DisplayName = "WithExtensions should not override extensions declared in HOCON")] + public async Task ShouldNotOverrideHocon() + { + using var host = await StartHost((builder, _) => + { + builder.AddHocon("akka.extensions = [\"Akka.Hosting.Tests.ExtensionsSpecs+FakeExtensionOneProvider, Akka.Hosting.Tests\"]"); + builder.WithExtensions(typeof(FakeExtensionTwoProvider)); + }); + + var system = host.Services.GetRequiredService(); + system.TryGetExtension(out _).Should().BeTrue(); + system.TryGetExtension(out _).Should().BeTrue(); + } + + [Fact(DisplayName = "WithExtensions should be able to be called multiple times")] + public async Task CanBeCalledMultipleTimes() + { + using var host = await StartHost((builder, _) => + { + builder.WithExtensions(typeof(FakeExtensionOneProvider)); + builder.WithExtensions(typeof(FakeExtensionTwoProvider)); + }); + + var system = host.Services.GetRequiredService(); + system.TryGetExtension(out _).Should().BeTrue(); + system.TryGetExtension(out _).Should().BeTrue(); + } + + public class FakeExtensionOne: IExtension + { + } + + public class FakeExtensionOneProvider : ExtensionIdProvider + { + public override FakeExtensionOne CreateExtension(ExtendedActorSystem system) + { + return new FakeExtensionOne(); + } + } + + public class FakeExtensionTwo: IExtension + { + } + + public class FakeExtensionTwoProvider : ExtensionIdProvider + { + public override FakeExtensionTwo CreateExtension(ExtendedActorSystem system) + { + return new FakeExtensionTwo(); + } + } +} + diff --git a/src/Akka.Hosting.Tests/XUnitLogger.cs b/src/Akka.Hosting.Tests/XUnitLogger.cs new file mode 100644 index 00000000..eaf73ccc --- /dev/null +++ b/src/Akka.Hosting.Tests/XUnitLogger.cs @@ -0,0 +1,84 @@ +using System; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace Akka.Hosting.Tests; + +public class XUnitLogger: ILogger +{ + private const string NullFormatted = "[null]"; + + private readonly string _category; + private readonly ITestOutputHelper _helper; + private readonly LogLevel _logLevel; + + public XUnitLogger(string category, ITestOutputHelper helper, LogLevel logLevel) + { + _category = category; + _helper = helper; + _logLevel = logLevel; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (!IsEnabled(logLevel)) + return; + + if (!TryFormatMessage(state, exception, formatter, out var formattedMessage)) + return; + + WriteLogEntry(logLevel, eventId, formattedMessage, exception); + } + + private void WriteLogEntry(LogLevel logLevel, EventId eventId, string message, Exception exception) + { + var level = logLevel switch + { + LogLevel.Critical => "CRT", + LogLevel.Debug => "DBG", + LogLevel.Error => "ERR", + LogLevel.Information => "INF", + LogLevel.Warning => "WRN", + LogLevel.Trace => "DBG", + _ => "???" + }; + + var msg = $"{DateTime.Now}:{level}:{_category}:{eventId} {message}"; + if (exception != null) + msg += $"\n{exception.GetType()} {exception.Message}\n{exception.StackTrace}"; + _helper.WriteLine(msg); + } + + public bool IsEnabled(LogLevel logLevel) + { + return logLevel switch + { + LogLevel.None => false, + _ => logLevel >= _logLevel + }; + } + + public IDisposable BeginScope(TState state) + { + throw new NotImplementedException(); + } + + private static bool TryFormatMessage( + TState state, + Exception exception, + Func formatter, + out string result) + { + formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); + + var formattedMessage = formatter(state, exception); + if (formattedMessage == NullFormatted) + { + result = null; + return false; + } + + result = formattedMessage; + return true; + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.Tests/XUnitLoggerProvider.cs b/src/Akka.Hosting.Tests/XUnitLoggerProvider.cs new file mode 100644 index 00000000..c5f8d18c --- /dev/null +++ b/src/Akka.Hosting.Tests/XUnitLoggerProvider.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace Akka.Hosting.Tests; + +public class XUnitLoggerProvider : ILoggerProvider +{ + private readonly ITestOutputHelper _helper; + private readonly LogLevel _logLevel; + + public XUnitLoggerProvider(ITestOutputHelper helper, LogLevel logLevel) + { + _helper = helper; + _logLevel = logLevel; + } + + public void Dispose() + { + // no-op + } + + public ILogger CreateLogger(string categoryName) + { + return new XUnitLogger(categoryName, _helper, _logLevel); + } +} \ No newline at end of file diff --git a/src/Akka.Hosting/AkkaConfigurationBuilder.cs b/src/Akka.Hosting/AkkaConfigurationBuilder.cs index 6acf97c0..83adb37b 100644 --- a/src/Akka.Hosting/AkkaConfigurationBuilder.cs +++ b/src/Akka.Hosting/AkkaConfigurationBuilder.cs @@ -79,6 +79,7 @@ public sealed class AkkaConfigurationBuilder internal readonly IServiceCollection ServiceCollection; internal readonly HashSet Serializers = new HashSet(); internal readonly HashSet Setups = new HashSet(); + internal readonly HashSet Extensions = new HashSet(); /// /// The currently configured . @@ -203,6 +204,33 @@ public AkkaConfigurationBuilder WithCustomSerializer( return this; } + /// + /// Adds a list of Akka.NET extensions that will be started automatically when the + /// starts up. + /// + /// + /// + /// // Starts distributed pub-sub, cluster metrics, and cluster bootstrap extensions at start-up + /// builder.WithExtensions( + /// typeof(DistributedPubSubExtensionProvider), + /// typeof(ClusterMetricsExtensionProvider), + /// typeof(ClusterBootstrapProvider)); + /// + /// + /// An array of extension providers that will be automatically started + /// when the starts + /// This instance, for fluent building pattern + public AkkaConfigurationBuilder WithExtensions(params Type[] extensions) + { + foreach (var extension in extensions) + { + if (Extensions.Contains(extension)) + continue; + Extensions.Add(extension); + } + return this; + } + internal void Bind() { // register as singleton - not interested in supporting multi-Sys use cases @@ -224,6 +252,37 @@ internal void Bind() }); } + /// + /// Configure extensions + /// + private void AddExtensions() + { + if (Extensions.Count == 0) + return; + + // check to see if there are any existing extensions set up inside the current HOCON configuration + if (Configuration.HasValue) + { + var listedExtensions = Configuration.Value.GetStringList("akka.extensions"); + foreach (var listedExtension in listedExtensions) + { + var trimmed = listedExtension.Trim(); + + // sanity check, we should not get any empty entries + if (string.IsNullOrWhiteSpace(trimmed)) + continue; + + var type = Type.GetType(trimmed); + if (type != null) + Extensions.Add(type); + } + } + + AddHoconConfiguration( + $"akka.extensions = [{string.Join(", ", Extensions.Select(s => $"\"{s.AssemblyQualifiedName}\""))}]", + HoconAddMode.Prepend); + } + private static Func ActorSystemFactory() { return sp => @@ -234,6 +293,9 @@ private static Func ActorSystemFactory() * Build setups */ + // Add auto-started akka extensions, if any. + config.AddExtensions(); + // check to see if we need a LoggerSetup var hasLoggerSetup = config.Setups.Any(c => c is LoggerFactorySetup); if (!hasLoggerSetup)