Skip to content

Commit

Permalink
Improve WithExtensions so it can be used universally (#92)
Browse files Browse the repository at this point in the history
  • Loading branch information
Arkatufus authored Aug 11, 2022
1 parent fd5fa2d commit d27f0e6
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 0 deletions.
99 changes: 99 additions & 0 deletions src/Akka.Hosting.Tests/ExtensionsSpecs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// -----------------------------------------------------------------------
// <copyright file="ExtensionsSpecs.cs" company="Akka.NET Project">
// Copyright (C) 2013-2022 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
// -----------------------------------------------------------------------

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<IHost> StartHost(Action<AkkaConfigurationBuilder, IServiceProvider> 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<ActorSystem>();
system.TryGetExtension<FakeExtensionOne>(out _).Should().BeTrue();
system.TryGetExtension<FakeExtensionTwo>(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<ActorSystem>();
system.TryGetExtension<FakeExtensionOne>(out _).Should().BeTrue();
system.TryGetExtension<FakeExtensionTwo>(out _).Should().BeTrue();
}

public class FakeExtensionOne: IExtension
{
}

public class FakeExtensionOneProvider : ExtensionIdProvider<FakeExtensionOne>
{
public override FakeExtensionOne CreateExtension(ExtendedActorSystem system)
{
return new FakeExtensionOne();
}
}

public class FakeExtensionTwo: IExtension
{
}

public class FakeExtensionTwoProvider : ExtensionIdProvider<FakeExtensionTwo>
{
public override FakeExtensionTwo CreateExtension(ExtendedActorSystem system)
{
return new FakeExtensionTwo();
}
}
}

84 changes: 84 additions & 0 deletions src/Akka.Hosting.Tests/XUnitLogger.cs
Original file line number Diff line number Diff line change
@@ -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<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> 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>(TState state)
{
throw new NotImplementedException();
}

private static bool TryFormatMessage<TState>(
TState state,
Exception exception,
Func<TState, Exception, string> 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;
}
}
26 changes: 26 additions & 0 deletions src/Akka.Hosting.Tests/XUnitLoggerProvider.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
62 changes: 62 additions & 0 deletions src/Akka.Hosting/AkkaConfigurationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public sealed class AkkaConfigurationBuilder
internal readonly IServiceCollection ServiceCollection;
internal readonly HashSet<SerializerRegistration> Serializers = new HashSet<SerializerRegistration>();
internal readonly HashSet<Setup> Setups = new HashSet<Setup>();
internal readonly HashSet<Type> Extensions = new HashSet<Type>();

/// <summary>
/// The currently configured <see cref="ProviderSelection"/>.
Expand Down Expand Up @@ -203,6 +204,33 @@ public AkkaConfigurationBuilder WithCustomSerializer(
return this;
}

/// <summary>
/// Adds a list of Akka.NET extensions that will be started automatically when the <see cref="ActorSystem"/>
/// starts up.
/// </summary>
/// <example>
/// <code>
/// // Starts distributed pub-sub, cluster metrics, and cluster bootstrap extensions at start-up
/// builder.WithExtensions(
/// typeof(DistributedPubSubExtensionProvider),
/// typeof(ClusterMetricsExtensionProvider),
/// typeof(ClusterBootstrapProvider));
/// </code>
/// </example>
/// <param name="extensions">An array of extension providers that will be automatically started
/// when the <see cref="ActorSystem"/> starts</param>
/// <returns>This <see cref="AkkaConfigurationBuilder"/> instance, for fluent building pattern</returns>
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
Expand All @@ -224,6 +252,37 @@ internal void Bind()
});
}

/// <summary>
/// Configure extensions
/// </summary>
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<IServiceProvider, ActorSystem> ActorSystemFactory()
{
return sp =>
Expand All @@ -234,6 +293,9 @@ private static Func<IServiceProvider, ActorSystem> 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)
Expand Down

0 comments on commit d27f0e6

Please sign in to comment.