diff --git a/src/Dapr/ChatServer.Host/ChatServerHostedServiceWrapper.cs b/src/Dapr/ChatServer.Host/ChatServerHostedServiceWrapper.cs new file mode 100644 index 000000000..e478362e8 --- /dev/null +++ b/src/Dapr/ChatServer.Host/ChatServerHostedServiceWrapper.cs @@ -0,0 +1,76 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.ChatServer.Host; + +using System.Collections.Generic; +using System.Threading; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using MUnique.OpenMU.Dapr.Common; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.PlugIns; +using ChatServer = MUnique.OpenMU.ChatServer.ChatServer; + +/// +/// A wrapper which takes a and wraps it as , +/// so that additional initialization can be done before actually starting it. +/// The actual server start is deferred to 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. +/// TODO: listen to configuration changes/database reinit. +/// See also: ServerContainerBase. +/// +public class ChatServerHostedServiceWrapper : IHostedLifecycleService +{ + private readonly IServiceProvider _serviceProvider; + private ChatServer? _chatServer; + + /// + /// Initializes a new instance of the class. + /// + /// The service provider. + public ChatServerHostedServiceWrapper(IServiceProvider serviceProvider) + { + this._serviceProvider = serviceProvider; + } + + /// + public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public async Task StartedAsync(CancellationToken cancellationToken) + { + await this._serviceProvider.WaitForDatabaseInitializationAsync(cancellationToken).ConfigureAwait(false); + + if (this._serviceProvider.GetService>() is { } plugInCollection) + { + if (plugInCollection is not List plugInConfigurations) + { + throw new InvalidOperationException($"The registered {nameof(ICollection)} must be a {nameof(List)} to be able to load plugin configurations."); + } + + await this._serviceProvider.TryLoadPlugInConfigurationsAsync(plugInConfigurations).ConfigureAwait(false); + } + + var settings = this._serviceProvider.GetRequiredService().ConvertToSettings(); + this._chatServer = this._serviceProvider.GetRequiredService(); + this._chatServer.Initialize(settings); + await this._chatServer.StartAsync(cancellationToken).ConfigureAwait(false); + } + + /// + public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public Task StopAsync(CancellationToken cancellationToken) + { + return this._chatServer?.StopAsync(cancellationToken) ?? Task.CompletedTask; + } + + /// + public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/src/Dapr/ChatServer.Host/Program.cs b/src/Dapr/ChatServer.Host/Program.cs index 42392a3f4..612fdebe7 100644 --- a/src/Dapr/ChatServer.Host/Program.cs +++ b/src/Dapr/ChatServer.Host/Program.cs @@ -4,12 +4,11 @@ using Microsoft.Extensions.DependencyInjection; -using MUnique.OpenMU.ChatServer; using MUnique.OpenMU.ChatServer.Host; using MUnique.OpenMU.Dapr.Common; using MUnique.OpenMU.DataModel.Configuration; -using MUnique.OpenMU.Network; using MUnique.OpenMU.PlugIns; +using ChatServer = MUnique.OpenMU.ChatServer.ChatServer; var plugInConfigurations = new List(); var builder = DaprService.CreateBuilder("ChatServer", args); @@ -22,13 +21,7 @@ .AddIpResolver(args) .AddPersistentSingleton(); -services.AddHostedService(p => -{ - var settings = p.GetService()?.ConvertToSettings() ?? throw new Exception($"{nameof(ChatServerSettings)} not registered."); - var chatServer = p.GetService()!; - chatServer.Initialize(settings); - return chatServer; -}); +services.AddHostedService(); services.PublishManageableServer(); @@ -37,9 +30,6 @@ builder.AddOpenTelemetryMetrics(metricsRegistry); var app = builder.BuildAndConfigure(); - -await app.WaitForUpdatedDatabaseAsync().ConfigureAwait(false); - -await app.Services.TryLoadPlugInConfigurationsAsync(plugInConfigurations).ConfigureAwait(false); +await app.WaitForDatabaseConnectionInitializationAsync().ConfigureAwait(false); app.Run(); diff --git a/src/Dapr/Common/Extensions.cs b/src/Dapr/Common/Extensions.cs index 56ce959d4..54b38cb29 100644 --- a/src/Dapr/Common/Extensions.cs +++ b/src/Dapr/Common/Extensions.cs @@ -4,8 +4,8 @@ namespace MUnique.OpenMU.Dapr.Common; -using System.Threading; using System.Text.Json.Serialization; +using System.Threading; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -295,6 +295,28 @@ await app.Services.GetService()! .ConfigureAwait(false); } + /// + /// 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 + /// 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. + /// + /// The service provider. + /// The cancellation token. + public static async Task WaitForDatabaseInitializationAsync(this IServiceProvider serviceProvider, CancellationToken cancellationToken = default) + { + var dbConnectionProvider = serviceProvider.GetRequiredService(); + if (dbConnectionProvider.Initialization is { } initTask) + { + await initTask.WaitAsync(cancellationToken).ConfigureAwait(false); + } + + await serviceProvider.GetRequiredService() + .WaitForUpdatedDatabaseAsync(cancellationToken) + .ConfigureAwait(false); + } + /// /// Adds the ip resolver to the collection, depending on the command line arguments /// and the in the database. diff --git a/src/Dapr/Common/ManagableServerStatePublisher.cs b/src/Dapr/Common/ManagableServerStatePublisher.cs index 1b63850ae..c7094bb37 100644 --- a/src/Dapr/Common/ManagableServerStatePublisher.cs +++ b/src/Dapr/Common/ManagableServerStatePublisher.cs @@ -6,22 +6,24 @@ namespace MUnique.OpenMU.Dapr.Common; using System; using System.ComponentModel; -using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using global::Dapr.Client; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Nito.AsyncEx; -using Nito.AsyncEx.Synchronous; using MUnique.OpenMU.Interfaces; using MUnique.OpenMU.PlugIns; +using Nito.AsyncEx; +using Nito.AsyncEx.Synchronous; /// /// A state publisher for a , /// which can be handled with a corresponding . +/// The server registration is deferred to 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. /// -public sealed class ManagableServerStatePublisher : IHostedService, IDisposable +public sealed class ManagableServerStatePublisher : IHostedLifecycleService, IDisposable { /// /// The topic name for the state updates. @@ -30,10 +32,11 @@ public sealed class ManagableServerStatePublisher : IHostedService, IDisposable private readonly ILogger _logger; private readonly DaprClient _daprClient; - private readonly IManageableServer _server; + private readonly IServiceProvider _serviceProvider; private readonly AsyncLock _lock = new(); - private readonly ServerStateData _data; + private IManageableServer? _server; + private ServerStateData? _data; private Task? _heartbeatTask; private CancellationTokenSource? _heartbeatCancellationTokenSource; @@ -42,15 +45,13 @@ public sealed class ManagableServerStatePublisher : IHostedService, IDisposable /// Initializes a new instance of the class. /// /// The dapr client. - /// The server. + /// The service provider used to lazily resolve . /// The logger. - public ManagableServerStatePublisher(DaprClient daprClient, IManageableServer server, ILogger logger) + public ManagableServerStatePublisher(DaprClient daprClient, IServiceProvider serviceProvider, ILogger logger) { this._daprClient = daprClient; - this._server = server; + this._serviceProvider = serviceProvider; this._logger = logger; - this._server.PropertyChanged += this.OnPropertyChanged; - this._data = new ServerStateData(this._server); } /// @@ -60,7 +61,13 @@ public void Dispose() } /// - public Task StartAsync(CancellationToken cancellationToken) + public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public Task StartedAsync(CancellationToken cancellationToken) { this._heartbeatCancellationTokenSource = new CancellationTokenSource(); @@ -80,6 +87,9 @@ async Task RunHeartbeatTask() return Task.CompletedTask; } + /// + public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + /// public async Task StopAsync(CancellationToken cancellationToken) { @@ -92,10 +102,12 @@ public async Task StopAsync(CancellationToken cancellationToken) } } + /// + public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + private async Task HeartbeatLoopAsync(CancellationToken cancellationToken) { - var stopWatch = new Stopwatch(); - stopWatch.Start(); + await this.InitializeServerAsync(cancellationToken).ConfigureAwait(false); while (!cancellationToken.IsCancellationRequested) { @@ -104,8 +116,33 @@ private async Task HeartbeatLoopAsync(CancellationToken cancellationToken) } } + private async Task InitializeServerAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested && this._server is null) + { + try + { + var server = this._serviceProvider.GetRequiredService(); + 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) { diff --git a/src/Dapr/ConnectServer.Host/ConnectServerHostedServiceWrapper.cs b/src/Dapr/ConnectServer.Host/ConnectServerHostedServiceWrapper.cs index 2e6b30c20..513d3cb24 100644 --- a/src/Dapr/ConnectServer.Host/ConnectServerHostedServiceWrapper.cs +++ b/src/Dapr/ConnectServer.Host/ConnectServerHostedServiceWrapper.cs @@ -5,36 +5,55 @@ namespace MUnique.OpenMU.ConnectServer.Host; using System.Threading; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using MUnique.OpenMU.Dapr.Common; /// -/// A wrapper which takes a and wraps it as , +/// A wrapper which takes a and wraps it as , /// so that additional initialization can be done before actually starting it. +/// The actual server start is deferred to 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. /// TODO: listen to configuration changes/database reinit. /// See also: ServerContainerBase. /// -public class ConnectServerHostedServiceWrapper : IHostedService +public class ConnectServerHostedServiceWrapper : IHostedLifecycleService { - private readonly ConnectServer _connectServer; + private readonly IServiceProvider _serviceProvider; + private ConnectServer? _connectServer; /// /// Initializes a new instance of the class. /// - /// The connect server. - public ConnectServerHostedServiceWrapper(ConnectServer connectServer) + /// The service provider. + public ConnectServerHostedServiceWrapper(IServiceProvider serviceProvider) { - this._connectServer = connectServer; + this._serviceProvider = serviceProvider; } /// - public async Task StartAsync(CancellationToken cancellationToken) + public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public async Task StartedAsync(CancellationToken cancellationToken) { + await this._serviceProvider.WaitForDatabaseInitializationAsync(cancellationToken).ConfigureAwait(false); + this._connectServer = this._serviceProvider.GetRequiredService(); await this._connectServer.StartAsync(cancellationToken).ConfigureAwait(false); } + /// + public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + /// public Task StopAsync(CancellationToken cancellationToken) { - return this._connectServer.StopAsync(cancellationToken); + return this._connectServer?.StopAsync(cancellationToken) ?? Task.CompletedTask; } + + /// + public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; } \ No newline at end of file diff --git a/src/Dapr/ConnectServer.Host/Program.cs b/src/Dapr/ConnectServer.Host/Program.cs index da7772d19..eb0a849df 100644 --- a/src/Dapr/ConnectServer.Host/Program.cs +++ b/src/Dapr/ConnectServer.Host/Program.cs @@ -26,6 +26,6 @@ builder.AddOpenTelemetryMetrics(metricsRegistry); var app = builder.BuildAndConfigure(); -await app.WaitForUpdatedDatabaseAsync().ConfigureAwait(false); +await app.WaitForDatabaseConnectionInitializationAsync().ConfigureAwait(false); app.Run(); diff --git a/src/Dapr/FriendServer.Host/Program.cs b/src/Dapr/FriendServer.Host/Program.cs index d62a9d7c3..3fa5d3fb8 100644 --- a/src/Dapr/FriendServer.Host/Program.cs +++ b/src/Dapr/FriendServer.Host/Program.cs @@ -25,6 +25,6 @@ var app = builder.BuildAndConfigure(); -await app.WaitForUpdatedDatabaseAsync().ConfigureAwait(false); +await app.WaitForDatabaseConnectionInitializationAsync().ConfigureAwait(false); app.Run(); diff --git a/src/Dapr/GameServer.Host/GameServerHostedServiceWrapper.cs b/src/Dapr/GameServer.Host/GameServerHostedServiceWrapper.cs index 71d02fc1f..6c5da8255 100644 --- a/src/Dapr/GameServer.Host/GameServerHostedServiceWrapper.cs +++ b/src/Dapr/GameServer.Host/GameServerHostedServiceWrapper.cs @@ -4,48 +4,77 @@ namespace MUnique.OpenMU.GameServer.Host; +using System.Collections.Generic; using System.Threading; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using MUnique.OpenMU.Dapr.Common; using MUnique.OpenMU.Interfaces; +using MUnique.OpenMU.PlugIns; +using MUnique.OpenMU.Web.Map; /// -/// A wrapper which takes a and wraps it as , +/// A wrapper which takes a and wraps it as , /// so that additional initialization can be done before actually starting it. +/// The actual server start is deferred to 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. /// TODO: listen to configuration changes/database reinit. /// See also: ServerContainerBase. /// -public class GameServerHostedServiceWrapper : IHostedService +public class GameServerHostedServiceWrapper : IHostedLifecycleService { - private readonly IGameServer _gameServer; - private readonly GameServerInitializer _initializer; - private bool _isInitialized; + private readonly IServiceProvider _serviceProvider; + private IGameServer? _gameServer; /// /// Initializes a new instance of the class. /// - /// The game server. - /// The initializer. - public GameServerHostedServiceWrapper(IGameServer gameServer, GameServerInitializer initializer) + /// The service provider. + public GameServerHostedServiceWrapper(IServiceProvider serviceProvider) { - this._gameServer = gameServer; - this._initializer = initializer; + this._serviceProvider = serviceProvider; } /// - public async Task StartAsync(CancellationToken cancellationToken) + public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public async Task StartedAsync(CancellationToken cancellationToken) { - if (!this._isInitialized) + await this._serviceProvider.WaitForDatabaseInitializationAsync(cancellationToken).ConfigureAwait(false); + + if (this._serviceProvider.GetService>() is { } plugInCollection) { - await this._initializer.InitializeAsync().ConfigureAwait(false); - this._isInitialized = true; + if (plugInCollection is not List plugInConfigurations) + { + throw new InvalidOperationException($"The registered {nameof(ICollection)} must be a {nameof(List)} to be able to load plugin configurations."); + } + + await this._serviceProvider.TryLoadPlugInConfigurationsAsync(plugInConfigurations).ConfigureAwait(false); } + this._gameServer = this._serviceProvider.GetRequiredService(); + var initializer = this._serviceProvider.GetRequiredService(); + await initializer.InitializeAsync().ConfigureAwait(false); + + await ((ObservableGameServerAdapter)this._serviceProvider.GetRequiredService()) + .InitializeAsync().ConfigureAwait(false); + await this._gameServer.StartAsync(cancellationToken).ConfigureAwait(false); } + /// + public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + /// public Task StopAsync(CancellationToken cancellationToken) { - return this._gameServer.StopAsync(cancellationToken); + return this._gameServer?.StopAsync(cancellationToken) ?? Task.CompletedTask; } + + /// + public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; } \ No newline at end of file diff --git a/src/Dapr/GameServer.Host/Program.cs b/src/Dapr/GameServer.Host/Program.cs index 056e5f3bc..dd22a8bdb 100644 --- a/src/Dapr/GameServer.Host/Program.cs +++ b/src/Dapr/GameServer.Host/Program.cs @@ -58,8 +58,6 @@ app.UseAntiforgery(); app.MapRazorComponents() .AddInteractiveServerRenderMode(); -await app.WaitForUpdatedDatabaseAsync().ConfigureAwait(false); +await app.WaitForDatabaseConnectionInitializationAsync().ConfigureAwait(false); -await app.Services.TryLoadPlugInConfigurationsAsync(plugInConfigurations).ConfigureAwait(false); -await ((ObservableGameServerAdapter)app.Services.GetRequiredService()).InitializeAsync().ConfigureAwait(false); app.Run(); diff --git a/src/Dapr/GuildServer.Host/Program.cs b/src/Dapr/GuildServer.Host/Program.cs index abca760a3..768431bbb 100644 --- a/src/Dapr/GuildServer.Host/Program.cs +++ b/src/Dapr/GuildServer.Host/Program.cs @@ -22,6 +22,6 @@ builder.AddOpenTelemetryMetrics(metricsRegistry); var app = builder.BuildAndConfigure(); -await app.WaitForUpdatedDatabaseAsync().ConfigureAwait(false); +await app.WaitForDatabaseConnectionInitializationAsync().ConfigureAwait(false); app.Run();