baseline: OpenMU upstream b5a0961 (fresh source)

This commit is contained in:
Acentech Dev
2026-07-14 19:00:35 +03:00
parent 450402e47d
commit 36fc125d5c
11968 changed files with 748705 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
// <copyright file="AssemblyInfo.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
using System.Reflection;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("MUnique.OpenMU.Dapr.Common")]

View File

@@ -0,0 +1,10 @@
// <copyright file="ConfigurationChangeArguments.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Dapr.Common;
/// <summary>
/// Arguments for the change notifications of <see cref="ConfigurationChangePublisher"/>.
/// </summary>
public record class ConfigurationChangeArguments(Type Type, Guid Id, object? Configuration);

View File

@@ -0,0 +1,67 @@
// <copyright file="ConfigurationChangePublisher.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Dapr.Common;
using global::Dapr.Client;
using MUnique.OpenMU.Interfaces;
/// <summary>
/// Publisher for configuration changes.
/// Changes are published to the configured pub/sub Dapr component.
/// </summary>
public class ConfigurationChangePublisher : IConfigurationChangePublisher
{
private readonly DaprClient _daprClient;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigurationChangePublisher"/> class.
/// </summary>
/// <param name="daprClient">The dapr client.</param>
public ConfigurationChangePublisher(DaprClient daprClient)
{
this._daprClient = daprClient;
}
/// <summary>
/// Publishes a changed, previously existing configuration.
/// </summary>
/// <param name="type">The type of the configuration.</param>
/// <param name="id">The identifier of the changed configuration.</param>
/// <param name="configuration">The changed configuration.</param>
public Task ConfigurationChangedAsync(Type type, Guid id, object configuration)
{
return this._daprClient.PublishEventAsync(
"pubsub",
nameof(this.ConfigurationChangedAsync),
new ConfigurationChangeArguments(type, id, configuration));
}
/// <summary>
/// Publishes an added configuration.
/// </summary>
/// <param name="type">The type of the configuration.</param>
/// <param name="id">The identifier of the added configuration.</param>
/// <param name="configuration">The added configuration.</param>
public Task ConfigurationAddedAsync(Type type, Guid id, object configuration)
{
return this._daprClient.PublishEventAsync(
"pubsub",
nameof(this.ConfigurationAddedAsync),
new ConfigurationChangeArguments(type, id, configuration));
}
/// <summary>
/// Publishes a removed, previously existing configuration.
/// </summary>
/// <param name="type">The type of the configuration.</param>
/// <param name="id">The identifier of the removed configuration.</param>
public Task ConfigurationRemovedAsync(Type type, Guid id)
{
return this._daprClient.PublishEventAsync(
"pubsub",
nameof(this.ConfigurationRemovedAsync),
new ConfigurationChangeArguments(type, id, null));
}
}

View File

@@ -0,0 +1,70 @@
// <copyright file="DaprService.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Dapr.Common;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.Persistence.EntityFramework;
using MUnique.OpenMU.Persistence.EntityFramework.Json;
using MUnique.OpenMU.PlugIns;
using OpenTelemetry.Logs;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
/// <summary>
/// Helper class to create an <see cref="WebApplicationBuilder"/> which predefined common
/// services for all of our service applications.
/// </summary>
public static class DaprService
{
/// <summary>
/// Initializes a new instance of the <see cref="WebApplicationBuilder" /> class with preconfigured defaults.
/// </summary>
/// <param name="serviceName">Name of the service, used for the OpenAPI and OpenTelemetry tracing.</param>
/// <param name="args">Command line arguments.</param>
/// <returns>
/// The <see cref="WebApplicationBuilder" />.
/// </returns>
public static WebApplicationBuilder CreateBuilder(string serviceName, string[] args)
{
JsonConverterRegistry.RegisterConverter(new LocalizedStringJsonConverter());
JsonConverterRegistry.RegisterConverter(new BinaryAsHexJsonConverter());
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls($"http://*:8080");
var services = builder.Services;
services.AddControllers();
services.AddDaprClient();
services.AddOpenTelemetry()
.WithTracing(t =>
{
t.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(serviceName))
.AddAspNetCoreInstrumentation()
.AddZipkinExporter(o => o.Endpoint = new Uri("http://zipkin:9411/api/v2/spans"));
});
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
services.AddEndpointsApiExplorer();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = serviceName, Version = "v1" });
});
services.AddSingleton<IDatabaseConnectionSettingProvider, SecretStoreDatabaseConnectionSettingsProvider>();
// Logging:
builder.UseLoki(serviceName);
builder.Logging.AddOpenTelemetry(options => options.AddOtlpExporter());
return builder;
}
}

View File

@@ -0,0 +1,352 @@
// <copyright file="Extensions.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Dapr.Common;
using System.Text.Json.Serialization;
using System.Threading;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Persistence;
using MUnique.OpenMU.Persistence.EntityFramework;
using MUnique.OpenMU.PlugIns;
using Nito.AsyncEx.Synchronous;
using OpenTelemetry.Exporter;
using OpenTelemetry.Metrics;
using Prometheus;
using Serilog;
using Serilog.Debugging;
using Serilog.Events;
using Serilog.Filters;
using Serilog.Sinks.Grafana.Loki;
/// <summary>
/// Common extensions for the building of daprized services.
/// </summary>
public static class Extensions
{
/// <summary>
/// Adds the <see cref="PersistenceContextProvider"/>.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="publishConfigChanges">If set to <c>true</c>, configuration changes are published to other Dapr services.</param>
/// <returns>The modified service collection.</returns>
public static IServiceCollection AddPeristenceProvider(this IServiceCollection services, bool publishConfigChanges = false)
{
services.AddSingleton<IConfigurationChangeListener, ConfigurationChangeListener>();
if (publishConfigChanges)
{
services.AddSingleton<IConfigurationChangePublisher, ConfigurationChangePublisher>();
}
else
{
services.AddSingleton(e => IConfigurationChangePublisher.None);
}
return services
.AddSingleton<IMigratableDatabaseContextProvider, PersistenceContextProvider>()
.AddSingleton(s => (PersistenceContextProvider)s.GetService<IMigratableDatabaseContextProvider>()!)
.AddSingleton(s => (IPersistenceContextProvider)s.GetService<IMigratableDatabaseContextProvider>()!)
.AddSingleton(s => new Lazy<IPersistenceContextProvider>(s.GetRequiredService<IPersistenceContextProvider>));
}
/// <summary>
/// Adds the plug in manager.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="plugInConfigurations">The plug in configurations.</param>
/// <returns>The modified service collection.</returns>
public static IServiceCollection AddPlugInManager(this IServiceCollection services, ICollection<PlugInConfiguration> plugInConfigurations)
{
return services
.AddSingleton(plugInConfigurations)
.AddSingleton<PlugInManager>()
.AddTransient<ReferenceHandler, ByDataSourceReferenceHandler>(provider =>
{
var persistenceContextProvider = provider.GetService<IPersistenceContextProvider>();
var dataSource = new GameConfigurationDataSource(
provider.GetService<ILogger<GameConfigurationDataSource>>()!,
persistenceContextProvider!);
var configId = persistenceContextProvider!.CreateNewConfigurationContext().GetDefaultGameConfigurationIdAsync(default).AsTask().WaitAndUnwrapException();
dataSource.GetOwnerAsync(configId!.Value).AsTask().WaitAndUnwrapException();
var referenceHandler = new ByDataSourceReferenceHandler(dataSource);
return referenceHandler;
});
}
/// <summary>
/// Tries to load the plug in configurations.
/// </summary>
/// <param name="serviceProvider">The service provider.</param>
/// <param name="plugInConfigurations">The list of plug in configurations, where the loaded configurations will be added.</param>
public static async ValueTask TryLoadPlugInConfigurationsAsync(this IServiceProvider serviceProvider, List<PlugInConfiguration> plugInConfigurations)
{
if (serviceProvider.GetService<IMigratableDatabaseContextProvider>() is not { } persistenceContextProvider)
{
throw new Exception($"{nameof(IPersistenceContextProvider)} not registered.");
}
try
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
if (!await persistenceContextProvider.CanConnectToDatabaseAsync(cts.Token).ConfigureAwait(false)
|| !await persistenceContextProvider.DatabaseExistsAsync(cts.Token).ConfigureAwait(false))
{
return;
}
var configs = await persistenceContextProvider.CreateNewTypedContext(typeof(PlugInConfiguration), false).GetAsync<PlugInConfiguration>().ConfigureAwait(false);
plugInConfigurations.AddRange(configs);
}
catch
{
// If we can't load it yet, because the database is not initialized, we just return
}
}
/// <summary>
/// Adds a persistent object as singleton to the services.
/// </summary>
/// <typeparam name="T">The base type of the persistent object.</typeparam>
/// <param name="services">The service collection.</param>
/// <param name="predicate">The predicate to select actual object.</param>
/// <returns>The modified service collection.</returns>
public static IServiceCollection AddPersistentSingleton<T>(this IServiceCollection services, Func<T, bool>? predicate = null)
where T : class
{
return services.AddPersistentSingleton<T, T>(predicate);
}
/// <summary>
/// Adds the persistent object as singleton to the services.
/// </summary>
/// <typeparam name="TTarget">The target, exposed type of the persistent object, usually an interface.</typeparam>
/// <typeparam name="TActual">The actual base type of the persistent object.</typeparam>
/// <param name="services">The service collection.</param>
/// <param name="predicate">The predicate to select the actual object.</param>
/// <returns>The modified service collection.</returns>
public static IServiceCollection AddPersistentSingleton<TTarget, TActual>(this IServiceCollection services, Func<TActual, bool>? predicate = null)
where TActual : class, TTarget
where TTarget : class
{
return services.AddSingleton(s =>
{
if (s.GetService<IPersistenceContextProvider>() is not { } persistenceContextProvider)
{
throw new Exception($"{nameof(IPersistenceContextProvider)} not registered.");
}
var objects = persistenceContextProvider.CreateNewConfigurationContext().GetAsync<TActual>().AsTask().WaitAndUnwrapException();
return (TTarget)objects.First(predicate ?? (_ => true))!;
});
}
/// <summary>
/// Adds the <see cref="ManagableServerRegistry"/> to the services.
/// </summary>
/// <param name="services">The service collection.</param>
/// <returns>The modified service collection.</returns>
public static IServiceCollection AddManageableServerRegistry(this IServiceCollection services)
{
services.AddSingleton<ManagableServerRegistry>()
.AddSingleton<IServerProvider>(s => s.GetService<ManagableServerRegistry>()!);
return services;
}
/// <summary>
/// Publishes the server to other daprized services by registering a <see cref="ManagableServerStatePublisher"/>.
/// </summary>
/// <typeparam name="TServer">The type of the server.</typeparam>
/// <param name="services">The service collection.</param>
/// <returns>The modified service collection.</returns>
public static IServiceCollection PublishManageableServer<TServer>(this IServiceCollection services)
where TServer : IManageableServer
{
services.AddSingleton<IManageableServer>(s => s.GetService<TServer>()!)
.AddHostedService<ManagableServerStatePublisher>()
.AddControllers().AddApplicationPart(typeof(ManageableServerController).Assembly);
return services;
}
/// <summary>
/// Configures the usage of logging to loki.
/// </summary>
/// <param name="builder">The web application builder.</param>
/// <param name="serviceName">Name of the service.</param>
/// <returns>The configured web application builder.</returns>
public static WebApplicationBuilder UseLoki(this WebApplicationBuilder builder, string serviceName)
{
// We just want to transmit some static labels, as suggested in the best practice in the Loki documentation
var includeLabels = new[] { "Account", "Character", "Connection", "ServiceName", "SourceContext" };
var logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.Enrich.WithProperty("ServiceName", serviceName)
.Enrich.FromLogContext()
.WriteTo
.GrafanaLoki(
uri: "http://loki:3100",
propertiesAsLabels: includeLabels)
.WriteTo
.Console(LogEventLevel.Information)
.Filter.ByExcluding(Matching.FromSource("Microsoft")) // We don't want all of the ASP.NET logging, because that really keeps loki and the console pretty busy
.CreateLogger();
SelfLog.Enable(Console.Error);
builder.Host.ConfigureLogging((_, loggingBuilder) => loggingBuilder.ClearProviders());
builder.Host.UseSerilog(logger);
return builder;
}
/// <summary>
/// Adds the open telemetry metrics.
/// </summary>
/// <param name="builder">The web application builder.</param>
/// <param name="registry">The registry.</param>
/// <returns>The configured web application builder.</returns>
public static WebApplicationBuilder AddOpenTelemetryMetrics(this WebApplicationBuilder builder, MetricsRegistry registry)
{
builder.Services.AddOpenTelemetry()
.WithMetrics(x =>
{
x.AddMeter(registry.Meters.ToArray());
x.AddPrometheusExporter();
x.AddOtlpExporter();
});
builder.Services.AddHealthChecks().ForwardToPrometheus();
return builder;
}
/// <summary>
/// Builds and configures the web application.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="addBlazor">If set to <c>true</c>, it configures the application to provide a blazor server app.</param>
/// <returns>The built and configured web application.</returns>
public static WebApplication BuildAndConfigure(this WebApplicationBuilder builder, bool addBlazor = false)
{
var pathBase = Environment.GetEnvironmentVariable("PATH_BASE");
var useReverseProxy = !string.IsNullOrWhiteSpace(pathBase);
var app = builder.Build();
if (useReverseProxy)
{
app.UsePathBase(pathBase!.TrimEnd('/'));
app.UseForwardedHeaders();
}
app.ConfigureDaprService(addBlazor);
app.MapPrometheusScrapingEndpoint();
return app;
}
/// <summary>
/// Configures the web application as dapr service.
/// </summary>
/// <param name="app">The application.</param>
/// <param name="addBlazor">If set to <c>true</c>, it configures the application to provide a blazor server app.</param>
/// <returns>The configured web application.</returns>
public static WebApplication ConfigureDaprService(this WebApplication app, bool addBlazor = false)
{
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseCloudEvents();
app.MapControllers();
app.MapSubscribeHandler();
return app;
}
/// <summary>
/// Waits for the completion of outstanding database updates.
/// </summary>
/// <param name="app">The application.</param>
public static async Task WaitForUpdatedDatabaseAsync(this WebApplication app)
{
await app.WaitForDatabaseConnectionInitializationAsync().ConfigureAwait(false);
await app.Services.GetService<PersistenceContextProvider>()!
.WaitForUpdatedDatabaseAsync()
.ConfigureAwait(false);
}
/// <summary>
/// Waits for database connection (settings) initialization.
/// </summary>
/// <param name="app">The application.</param>
public static async Task WaitForDatabaseConnectionInitializationAsync(this WebApplication app)
{
await app.Services.GetService<IDatabaseConnectionSettingProvider>()!
.InitializeAsync(default)
.ConfigureAwait(false);
}
/// <summary>
/// Waits for the database secrets to be loaded and then for the database to be up-to-date.
/// This is intended to be called from <see cref="Microsoft.Extensions.Hosting.IHostedLifecycleService.StartedAsync"/>
/// which is called after the web server has already started, breaking the circular dependency where:
/// the Dapr sidecar needs the app HTTP API to be up before it initializes its secret store,
/// but the app needs Dapr secrets to connect to the database.
/// </summary>
/// <param name="serviceProvider">The service provider.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public static async Task WaitForDatabaseInitializationAsync(this IServiceProvider serviceProvider, CancellationToken cancellationToken = default)
{
var dbConnectionProvider = serviceProvider.GetRequiredService<IDatabaseConnectionSettingProvider>();
if (dbConnectionProvider.Initialization is { } initTask)
{
await initTask.WaitAsync(cancellationToken).ConfigureAwait(false);
}
await serviceProvider.GetRequiredService<PersistenceContextProvider>()
.WaitForUpdatedDatabaseAsync(cancellationToken)
.ConfigureAwait(false);
}
/// <summary>
/// Adds the ip resolver to the collection, depending on the command line arguments
/// and the <see cref="SystemConfiguration"/> in the database.
/// </summary>
/// <param name="serviceCollection">The service collection.</param>
/// <param name="args">The arguments.</param>
/// <returns>The <paramref name="serviceCollection"/>.</returns>
public static IServiceCollection AddIpResolver(this IServiceCollection serviceCollection, string[] args)
{
return serviceCollection.AddSingleton(serviceProvider =>
{
(IpResolverType IpResolver, string? IpResolverParameter)? settings = default;
try
{
var persistenceContextProvider = serviceProvider.GetService<IPersistenceContextProvider>() ?? throw new Exception($"{nameof(IPersistenceContextProvider)} not registered.");
using var context = persistenceContextProvider.CreateNewTypedContext(typeof(SystemConfiguration), false);
// TODO: this may lead to a deadlock?
var configuration = context.GetAsync<SystemConfiguration>().AsTask().WaitAndUnwrapException().FirstOrDefault();
if (configuration is not null)
{
settings = (configuration.IpResolver, configuration.IpResolverParameter);
}
}
catch (Exception ex)
{
serviceProvider.GetService<ILogger<IIpAddressResolver>>()?.LogError(ex, "Unexpected error when trying to load the system configuration during ip resolver creation.");
}
return IpAddressResolverFactory.CreateIpResolver(args, settings, serviceProvider.GetService<ILoggerFactory>()!);
});
}
}

View File

@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<DocumentationFile></DocumentationFile>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Properties\**" />
<EmbeddedResource Remove="Properties\**" />
<None Remove="Properties\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dapr.AspNetCore" />
<PackageReference Include="Dapr.Client" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.OpenApi" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
<PackageReference Include="OpenTelemetry.Exporter.Zipkin" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
<PackageReference Include="prometheus-net.AspNetCore.HealthChecks" />
<PackageReference Include="Serilog.Extensions.Hosting" />
<PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Serilog.Sinks.Grafana.Loki" />
<PackageReference Include="SharpAbp.Abp.OpenTelemetry.Exporter.Prometheus.AspNetCore" />
<PackageReference Include="Swashbuckle.AspNetCore" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Interfaces\MUnique.OpenMU.Interfaces.csproj" />
<ProjectReference Include="..\..\Persistence\EntityFramework\MUnique.OpenMU.Persistence.EntityFramework.csproj" />
<ProjectReference Include="..\ServerClients\MUnique.OpenMU.ServerClients.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,116 @@
// <copyright file="ManagableServerRegistry.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Dapr.Common;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
using global::Dapr.Client;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.Interfaces;
/// <summary>
/// A registry for all <see cref="IManageableServer"/>s in the system.
/// </summary>
public class ManagableServerRegistry : IServerProvider, IDisposable
{
private readonly TimeSpan _timeout = TimeSpan.FromSeconds(20);
private readonly CancellationTokenSource _disposeCts = new();
private readonly ILogger<ManagableServerRegistry> _logger;
private readonly DaprClient _daprClient;
private readonly ConcurrentDictionary<int, ManageableServerClient> _serverClients = new();
/// <summary>
/// Initializes a new instance of the <see cref="ManagableServerRegistry" /> class.
/// </summary>
/// <param name="daprClient">The dapr client.</param>
/// <param name="logger">The logger.</param>
public ManagableServerRegistry(DaprClient daprClient, ILogger<ManagableServerRegistry> logger)
{
this._daprClient = daprClient;
this._logger = logger;
async Task RunTimeoutLoop()
{
try
{
await this.TimeoutLoopAsync(this._disposeCts.Token).ConfigureAwait(false);
}
catch (Exception ex)
{
this._logger.LogError(ex, "Error in cleanup loop");
}
}
_ = RunTimeoutLoop();
}
/// <inheritdoc />
public event PropertyChangedEventHandler? PropertyChanged;
/// <inheritdoc />
public IList<IManageableServer> Servers => this._serverClients.Values.OfType<IManageableServer>().ToList();
/// <summary>
/// Handles an update of a server state.
/// </summary>
/// <param name="serverData">The server data.</param>
public void HandleUpdate(ServerStateData serverData)
{
var isNew = false;
this._serverClients.AddOrUpdate(
serverData.Id,
_ =>
{
isNew = true;
return new ManageableServerClient(this._daprClient, serverData);
},
(_, client) =>
{
client.Update(serverData);
return client;
});
if (isNew)
{
this.RaisePropertyChanged(nameof(this.Servers));
}
}
/// <inheritdoc />
public void Dispose()
{
this._disposeCts.Cancel();
this._disposeCts.Dispose();
}
/// <summary>
/// Raises the <see cref="PropertyChanged"/> event with the specified parameters.
/// </summary>
/// <param name="propertyName">Name of the property.</param>
protected virtual void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private async Task TimeoutLoopAsync(CancellationToken cancellationToken)
{
while (!this._disposeCts.IsCancellationRequested)
{
await Task.Delay(2000, cancellationToken).ConfigureAwait(false);
foreach (var server in this._serverClients.Values)
{
var diff = DateTime.UtcNow - server.LastUpdate;
if (diff > this._timeout)
{
this._logger.LogInformation("Difference of {0} higher than timeout for server {1}", diff, server.Id);
server.ServerState = ServerState.Timeout;
}
}
}
}
}

View File

@@ -0,0 +1,178 @@
// <copyright file="ManagableServerStatePublisher.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Dapr.Common;
using System;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using global::Dapr.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.PlugIns;
using Nito.AsyncEx;
using Nito.AsyncEx.Synchronous;
/// <summary>
/// A state publisher for a <see cref="IManageableServer"/>,
/// which can be handled with a corresponding <see cref="ManagableServerRegistry"/>.
/// The server registration is deferred to <see cref="StartedAsync"/> which is called after the web application
/// has started (i.e. the HTTP API is already available), breaking the circular startup dependency with the Dapr sidecar.
/// </summary>
public sealed class ManagableServerStatePublisher : IHostedLifecycleService, IDisposable
{
/// <summary>
/// The topic name for the state updates.
/// </summary>
public const string TopicName = "ServerState";
private readonly ILogger<ManagableServerStatePublisher> _logger;
private readonly DaprClient _daprClient;
private readonly IServiceProvider _serviceProvider;
private readonly AsyncLock _lock = new();
private IManageableServer? _server;
private ServerStateData? _data;
private Task? _heartbeatTask;
private CancellationTokenSource? _heartbeatCancellationTokenSource;
/// <summary>
/// Initializes a new instance of the <see cref="ManagableServerStatePublisher"/> class.
/// </summary>
/// <param name="daprClient">The dapr client.</param>
/// <param name="serviceProvider">The service provider used to lazily resolve <see cref="IManageableServer"/>.</param>
/// <param name="logger">The logger.</param>
public ManagableServerStatePublisher(DaprClient daprClient, IServiceProvider serviceProvider, ILogger<ManagableServerStatePublisher> logger)
{
this._daprClient = daprClient;
this._serviceProvider = serviceProvider;
this._logger = logger;
}
/// <inheritdoc />
public void Dispose()
{
this.StopAsync(default).WaitAndUnwrapException();
}
/// <inheritdoc />
public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
/// <inheritdoc />
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
/// <inheritdoc />
public Task StartedAsync(CancellationToken cancellationToken)
{
this._heartbeatCancellationTokenSource = new CancellationTokenSource();
async Task RunHeartbeatTask()
{
try
{
await this.HeartbeatLoopAsync(this._heartbeatCancellationTokenSource.Token).ConfigureAwait(false);
}
catch (Exception ex)
{
this._logger.LogError(ex, "Error in heartbeat loop.");
}
}
this._heartbeatTask = RunHeartbeatTask();
return Task.CompletedTask;
}
/// <inheritdoc />
public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
/// <inheritdoc />
public async Task StopAsync(CancellationToken cancellationToken)
{
await (this._heartbeatCancellationTokenSource?.CancelAsync() ?? Task.CompletedTask).ConfigureAwait(false);
this._heartbeatCancellationTokenSource?.Dispose();
if (this._heartbeatTask is { } heartbeatTask)
{
this._heartbeatTask = null;
await heartbeatTask.ConfigureAwait(false);
}
}
/// <inheritdoc />
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
private async Task HeartbeatLoopAsync(CancellationToken cancellationToken)
{
await this.InitializeServerAsync(cancellationToken).ConfigureAwait(false);
while (!cancellationToken.IsCancellationRequested)
{
await this.PublishCurrentStateAsync().ConfigureAwait(false);
await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
}
}
private async Task InitializeServerAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested && this._server is null)
{
try
{
var server = this._serviceProvider.GetRequiredService<IManageableServer>();
server.PropertyChanged -= this.OnPropertyChanged; // Ensure single subscription in case of retry
server.PropertyChanged += this.OnPropertyChanged;
this._data = new ServerStateData(server);
this._server = server;
}
catch (Exception ex)
{
this._logger.LogWarning(ex, "Could not resolve IManageableServer yet, retrying...");
await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
}
}
}
private async Task PublishCurrentStateAsync()
{
if (this._server is null || this._data is null)
{
return;
}
using var asyncLock = await this._lock.LockAsync(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
if (asyncLock is null)
{
return;
}
try
{
this._data.UpdateState(this._server);
await this._daprClient.PublishEventAsync("pubsub", TopicName, this._data).ConfigureAwait(false);
}
catch (Exception ex)
{
this._logger.LogError(ex, "Error sending server status update");
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Exceptions are catched.")]
private async void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
try
{
if (e.PropertyName == nameof(IManageableServer.ServerState))
{
await this.PublishCurrentStateAsync().ConfigureAwait(false);
}
}
catch (Exception ex)
{
this._logger.LogError(ex, "Error when publishing current state after property change");
}
}
}

View File

@@ -0,0 +1,138 @@
// <copyright file="ManageableServerClient.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Dapr.Common;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
using global::Dapr.Client;
using MUnique.OpenMU.Interfaces;
/// <summary>
/// A client to control a <seealso cref="IManageableServer"/>.
/// </summary>
/// <seealso cref="MUnique.OpenMU.Interfaces.IManageableServer" />
internal class ManageableServerClient : IManageableServer
{
private readonly DaprClient _daprClient;
private readonly string _targetAppId;
private ServerState _serverState;
private int _currentConnections;
/// <summary>
/// Initializes a new instance of the <see cref="ManageableServerClient"/> class.
/// </summary>
/// <param name="daprClient">The dapr client.</param>
/// <param name="serverData">The server data.</param>
public ManageableServerClient(DaprClient daprClient, ServerStateData serverData)
{
this._daprClient = daprClient;
this._targetAppId = serverData.AppId;
this.Id = serverData.Id;
this.Description = serverData.Description;
this.ConfigurationId = serverData.ConfigurationId;
this.CurrentConnections = serverData.CurrentConnections;
this.MaximumConnections = serverData.MaximumConnections;
this.ServerState = serverData.State;
this.Type = serverData.Type;
this.LastUpdate = DateTime.UtcNow;
}
/// <inheritdoc/>
public event PropertyChangedEventHandler? PropertyChanged;
/// <inheritdoc/>
public int Id { get; }
/// <inheritdoc/>
public Guid ConfigurationId { get; }
/// <inheritdoc/>
public string Description { get; }
/// <inheritdoc/>
public ServerType Type { get; }
/// <inheritdoc/>
public int MaximumConnections { get; }
/// <summary>
/// Gets the timestamp of the last update.
/// </summary>
public DateTime LastUpdate { get; private set; }
/// <inheritdoc/>
public ServerState ServerState
{
get => this._serverState;
set
{
if (this._serverState == value)
{
return;
}
this._serverState = value;
this.RaisePropertyChanged();
}
}
/// <inheritdoc/>
public int CurrentConnections
{
get => this._currentConnections;
set
{
if (this._currentConnections == value)
{
return;
}
this._currentConnections = value;
this.RaisePropertyChanged();
}
}
/// <inheritdoc/>
public Task StartAsync(CancellationToken cancellationToken)
{
return this._daprClient.InvokeMethodAsync(this._targetAppId, nameof(IManageableServer.StartAsync), cancellationToken);
}
/// <inheritdoc/>
public async ValueTask StartAsync()
{
await this.StartAsync(default).ConfigureAwait(false);
}
/// <inheritdoc/>
public Task StopAsync(CancellationToken cancellationToken)
{
return this._daprClient.InvokeMethodAsync(this._targetAppId, nameof(IManageableServer.ShutdownAsync), cancellationToken);
}
/// <inheritdoc/>
public async ValueTask ShutdownAsync()
{
await this.StopAsync(default).ConfigureAwait(false);
}
/// <summary>
/// Updates the specified server data.
/// </summary>
/// <param name="serverData">The server data.</param>
public void Update(ServerStateData serverData)
{
this.ServerState = serverData.State;
this.CurrentConnections = serverData.CurrentConnections;
this.LastUpdate = DateTime.UtcNow;
}
private void RaisePropertyChanged([CallerMemberName] string propertyName = "")
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

View File

@@ -0,0 +1,45 @@
// <copyright file="ManageableServerController.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Dapr.Common;
using Microsoft.AspNetCore.Mvc;
using MUnique.OpenMU.Interfaces;
/// <summary>
/// An api controller to control a <see cref="IManageableServer"/>.
/// </summary>
[ApiController]
[Route("")]
public class ManageableServerController
{
private readonly IManageableServer _manageableServer;
/// <summary>
/// Initializes a new instance of the <see cref="ManageableServerController"/> class.
/// </summary>
/// <param name="manageableServer">The manageable server.</param>
public ManageableServerController(IManageableServer manageableServer)
{
this._manageableServer = manageableServer;
}
/// <summary>
/// Shuts the manageable server down.
/// </summary>
[HttpPost(nameof(IManageableServer.ShutdownAsync))]
public async Task ShutdownAsync()
{
await this._manageableServer.ShutdownAsync().ConfigureAwait(false);
}
/// <summary>
/// Starts the manageable server.
/// </summary>
[HttpPost(nameof(IManageableServer.StartAsync))]
public async Task StartAsync()
{
await this._manageableServer.StartAsync().ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,47 @@
// <copyright file="MetricsRegistry.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Dapr.Common;
/// <summary>
/// Registry for metrics which should be exposed by the application.
/// </summary>
public class MetricsRegistry
{
private readonly HashSet<string> _meters = new HashSet<string>();
/// <summary>
/// Gets the registered meters.
/// </summary>
public IEnumerable<string> Meters => this._meters;
/// <summary>
/// Adds the meter with the specified name.
/// </summary>
/// <param name="meterName">Name of the meter.</param>
public void AddMeter(string meterName)
{
this._meters.Add(meterName);
}
/// <summary>
/// Adds the meters with the specified names.
/// </summary>
/// <param name="meterNames">The names of the meters.</param>
public void AddMeters(IEnumerable<string> meterNames)
{
foreach (var meter in meterNames)
{
this.AddMeter(meter);
}
}
/// <summary>
/// Adds the network meters.
/// </summary>
public void AddNetworkMeters()
{
this.AddMeters(MUnique.OpenMU.Network.Metrics.Meters);
}
}

View File

@@ -0,0 +1,107 @@
// <copyright file="SecretStoreDatabaseConnectionSettingsProvider.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Dapr.Common;
using System.Threading;
using global::Dapr;
using global::Dapr.Client;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.Persistence.EntityFramework;
/// <summary>
/// Implementation of <see cref="IDatabaseConnectionSettingProvider"/> which retrieves the settings from the
/// configured Dapr secret storage.
/// </summary>
public class SecretStoreDatabaseConnectionSettingsProvider : IDatabaseConnectionSettingProvider
{
private const string SecretStoreName = "secrets";
private readonly DaprClient _daprClient;
private readonly ILogger<SecretStoreDatabaseConnectionSettingsProvider> _logger;
private readonly Dictionary<string, ConnectionSetting> _connectionSettings = new(StringComparer.InvariantCultureIgnoreCase);
private bool _isInitialized;
/// <summary>
/// Initializes a new instance of the <see cref="SecretStoreDatabaseConnectionSettingsProvider" /> class.
/// </summary>
/// <param name="daprClient">The dapr client.</param>
/// <param name="logger">The logger.</param>
public SecretStoreDatabaseConnectionSettingsProvider(DaprClient daprClient, ILogger<SecretStoreDatabaseConnectionSettingsProvider> logger)
{
this._daprClient = daprClient;
this._logger = logger;
}
/// <inheritdoc />
public Task? Initialization { get; private set; }
/// <inheritdoc />
public Task InitializeAsync(CancellationToken cancellationToken)
{
if (this._isInitialized)
{
return Task.CompletedTask;
}
this.Initialization = Task.Run(
async () =>
{
this._isInitialized = false;
while (!this._isInitialized && !cancellationToken.IsCancellationRequested)
{
try
{
Console.WriteLine("trying to get secrets ...");
var secrets = await this._daprClient.GetBulkSecretAsync(SecretStoreName, cancellationToken: cancellationToken).ConfigureAwait(false);
foreach (var secret in secrets.Where(kvp => string.Equals(kvp.Key.Split(':')[0], "connectionStrings", StringComparison.InvariantCultureIgnoreCase)))
{
var contextTypeName = secret.Value.Keys.First().Split(':').Last();
var setting = new ConnectionSetting
{
ContextTypeName = contextTypeName,
ConnectionString = secret.Value.Values.First()!,
DatabaseEngine = DatabaseEngine.Npgsql,
};
this._connectionSettings.Add(contextTypeName, setting);
}
Console.WriteLine("secrets retrieved :)");
this._isInitialized = true;
}
catch (DaprException ex)
{
// This should never happen - however, it may happen when we are using a Dapr secret store.
// It may not be started yet, and the implementation to get it does retrieve it in the constructor already.
this._logger.LogWarning(ex, "Error occurred when retrieving the connection strings from the secrets store. Trying again in 3 seconds...");
Console.WriteLine("Error occurred when retrieving the connection strings from the secrets store. Trying again in 3 seconds...");
await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
}
}
});
ConnectionConfigurator.Initialize(this);
return Task.CompletedTask;
}
/// <inheritdoc />
public ConnectionSetting GetConnectionSetting<TContextType>()
where TContextType : DbContext
{
return this.GetConnectionSetting(typeof(TContextType));
}
/// <inheritdoc />
public ConnectionSetting GetConnectionSetting(Type contextType)
{
if (this._connectionSettings.TryGetValue(contextType.FullName ?? contextType.Name, out var result))
{
return result;
}
throw new InvalidOperationException($"No connection string for type '{contextType.FullName}' stored in the secret store.");
}
}

View File

@@ -0,0 +1,96 @@
// <copyright file="ServerStateData.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Dapr.Common;
using System.Diagnostics;
using MUnique.OpenMU.Interfaces;
/// <summary>
/// Data about the state of a server.
/// </summary>
public class ServerStateData
{
private readonly Stopwatch _stopwatch = new();
/// <summary>
/// Initializes a new instance of the <see cref="ServerStateData"/> class.
/// </summary>
public ServerStateData()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ServerStateData"/> class.
/// </summary>
/// <param name="server">The server.</param>
/// <exception cref="System.InvalidOperationException">Add the environment variable 'APPID' with the app-id of this dapr app.</exception>
public ServerStateData(IManageableServer server)
{
this.AppId = Environment.GetEnvironmentVariable("APPID") ?? throw new InvalidOperationException("Add the environment variable 'APPID' with the app-id of this dapr app.");
this.Id = server.Id;
this.Description = server.Description;
this.ConfigurationId = server.ConfigurationId;
this.Type = server.Type;
this.MaximumConnections = server.MaximumConnections;
this._stopwatch.Start();
this.UpdateState(server);
}
/// <summary>
/// Gets or sets the (dapr) application identifier.
/// </summary>
public string AppId { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the identifier of the server.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Gets or sets the description of the server.
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the identifier of the configuration of the server.
/// </summary>
public Guid ConfigurationId { get; set; }
/// <summary>
/// Gets or sets the type of the server.
/// </summary>
public ServerType Type { get; set; }
/// <summary>
/// Gets or sets the state of the server.
/// </summary>
public ServerState State { get; set; }
/// <summary>
/// Gets or sets the current connection count of the server.
/// </summary>
public int CurrentConnections { get; set; }
/// <summary>
/// Gets or sets the maximum connection count which the server supports.
/// </summary>
public int MaximumConnections { get; set; }
/// <summary>
/// Gets or sets the up time of the server.
/// </summary>
public TimeSpan UpTime { get; set; }
/// <summary>
/// Updates the state of the server.
/// </summary>
/// <param name="server">The server.</param>
public void UpdateState(IManageableServer server)
{
this.State = server.ServerState;
this.CurrentConnections = server.CurrentConnections;
this.UpTime = this._stopwatch.Elapsed;
}
}