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();