diff --git a/src/Managing.Api/Program.cs b/src/Managing.Api/Program.cs
index 86f4cc2c..f511b51d 100644
--- a/src/Managing.Api/Program.cs
+++ b/src/Managing.Api/Program.cs
@@ -287,6 +287,7 @@ builder.WebHost.SetupDiscordBot();
// App
var app = builder.Build();
+
app.UseSerilogRequestLogging();
app.UseOpenApi();
app.UseSwaggerUI(c =>
diff --git a/src/Managing.Application.Abstractions/Grains/IBotReminderInitializerGrain.cs b/src/Managing.Application.Abstractions/Grains/IBotReminderInitializerGrain.cs
new file mode 100644
index 00000000..fb1de52d
--- /dev/null
+++ b/src/Managing.Application.Abstractions/Grains/IBotReminderInitializerGrain.cs
@@ -0,0 +1,17 @@
+using Orleans;
+
+namespace Managing.Application.Abstractions.Grains;
+
+///
+/// Grain responsible for initializing bot reminders on application startup.
+/// This grain ensures that only one instance runs the initialization process
+/// even in multi-silo environments.
+///
+public interface IBotReminderInitializerGrain : IGrainWithIntegerKey
+{
+ ///
+ /// Initializes bot reminders by fetching all running bots and pinging them
+ /// to ensure their reminders are properly registered.
+ ///
+ Task InitializeBotRemindersAsync();
+}
diff --git a/src/Managing.Application.Abstractions/Grains/ILiveTradingBotGrain.cs b/src/Managing.Application.Abstractions/Grains/ILiveTradingBotGrain.cs
index 79b1a5d4..5f6ee58d 100644
--- a/src/Managing.Application.Abstractions/Grains/ILiveTradingBotGrain.cs
+++ b/src/Managing.Application.Abstractions/Grains/ILiveTradingBotGrain.cs
@@ -39,4 +39,11 @@ public interface ILiveTradingBotGrain : IGrainWithGuidKey
/// Deletes the bot and cleans up all associated resources
///
Task DeleteAsync();
+
+ ///
+ /// Pings the bot to reactivate it and ensure reminders are registered
+ /// Used during startup to reactivate bots that may have lost their reminders
+ /// Returns true if the ping was successful, false otherwise
+ ///
+ Task PingAsync();
}
\ No newline at end of file
diff --git a/src/Managing.Application/Bots/BotReminderInitializer.cs b/src/Managing.Application/Bots/BotReminderInitializer.cs
deleted file mode 100644
index 3a9fd264..00000000
--- a/src/Managing.Application/Bots/BotReminderInitializer.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-namespace Managing.Application.Bots;
-
-public class BotReminderInitializer
-{
-}
\ No newline at end of file
diff --git a/src/Managing.Application/Bots/Grains/BotReminderInitializerGrain.cs b/src/Managing.Application/Bots/Grains/BotReminderInitializerGrain.cs
new file mode 100644
index 00000000..97a8f741
--- /dev/null
+++ b/src/Managing.Application/Bots/Grains/BotReminderInitializerGrain.cs
@@ -0,0 +1,129 @@
+using Managing.Application.Abstractions;
+using Managing.Application.Abstractions.Grains;
+using Managing.Core;
+using Managing.Domain.Bots;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using static Managing.Common.Enums;
+
+namespace Managing.Application.Bots.Grains;
+
+///
+/// Grain that initializes bot reminders on application startup and periodically checks for running bots.
+/// Fetches all running bots and pings them to ensure their reminders are properly registered.
+/// This grain ensures that only one instance runs the initialization process
+/// even in multi-silo environments.
+///
+public class BotReminderInitializerGrain : Grain, IBotReminderInitializerGrain, IRemindable
+{
+ private readonly IServiceScopeFactory _scopeFactory;
+ private readonly ILogger _logger;
+
+ private const string CheckRunningBotsReminderName = "CheckRunningBotsReminder";
+
+ public BotReminderInitializerGrain(
+ IServiceScopeFactory scopeFactory,
+ ILogger logger)
+ {
+ _scopeFactory = scopeFactory;
+ _logger = logger;
+ }
+
+ ///
+ /// Initializes bot reminders by fetching all running bots and pinging them
+ /// to ensure their reminders are properly registered.
+ ///
+ public async Task InitializeBotRemindersAsync()
+ {
+ try
+ {
+ _logger.LogInformation("BotReminderInitializerGrain starting - fetching running bots to reactivate reminders");
+
+ // Get all running bots from the database
+ var runningBots = await GetRunningBotsAsync();
+
+ if (!runningBots.Any())
+ {
+ _logger.LogInformation("No running bots found to reactivate");
+ return;
+ }
+
+ _logger.LogInformation("Found {Count} running bots to reactivate", runningBots.Count());
+
+ // Ping each running bot to reactivate it and ensure reminders are registered
+ var tasks = runningBots.Select(async bot =>
+ {
+ try
+ {
+ _logger.LogDebug("Pinging bot {BotId} ({BotName}) to reactivate", bot.Identifier, bot.Name);
+
+ var grain = GrainFactory.GetGrain(bot.Identifier);
+ var success = await grain.PingAsync();
+
+ if (success)
+ {
+ _logger.LogDebug("Successfully pinged bot {BotId} ({BotName})", bot.Identifier, bot.Name);
+ }
+ else
+ {
+ _logger.LogWarning("Ping failed for bot {BotId} ({BotName})", bot.Identifier, bot.Name);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to ping bot {BotId} ({BotName})", bot.Identifier, bot.Name);
+ SentrySdk.CaptureException(ex);
+ }
+ });
+
+ // Wait for all pings to complete
+ await Task.WhenAll(tasks);
+
+ // Register a reminder to check running bots every hour
+ // Start immediately and repeat every hour
+ await this.RegisterOrUpdateReminder(
+ CheckRunningBotsReminderName,
+ TimeSpan.FromHours(1), // Start in 1 hour
+ TimeSpan.FromHours(1)); // Repeat every hour
+
+ _logger.LogInformation("BotReminderInitializerGrain completed - processed {Count} running bots", runningBots.Count());
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error during BotReminderInitializerGrain initialization");
+ SentrySdk.CaptureException(ex);
+ }
+ }
+
+ ///
+ /// Handles reminder callbacks for periodic bot checking
+ ///
+ public Task ReceiveReminder(string reminderName, TickStatus status)
+ {
+ if (reminderName == CheckRunningBotsReminderName)
+ {
+ _logger.LogDebug("Received hourly reminder to check running bots");
+ return InitializeBotRemindersAsync();
+ }
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Fetches all bots with Running status from the database
+ ///
+ private async Task> GetRunningBotsAsync()
+ {
+ try
+ {
+ return await ServiceScopeHelpers.WithScopedService>(
+ _scopeFactory,
+ async botService => await botService.GetBotsByStatusAsync(BotStatus.Running));
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to fetch running bots from database");
+ return Enumerable.Empty();
+ }
+ }
+}
diff --git a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs
index 30c5cd8d..0d924e3f 100644
--- a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs
+++ b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs
@@ -43,8 +43,8 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
public override async Task OnActivateAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("LiveTradingBotGrain {GrainId} activated", this.GetPrimaryKey());
- await base.OnActivateAsync(cancellationToken);
await ResumeBotIfRequiredAsync();
+ await base.OnActivateAsync(cancellationToken);
}
public override async Task OnDeactivateAsync(DeactivationReason reason, CancellationToken cancellationToken)
@@ -795,4 +795,27 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
_logger.LogError(ex, "Failed to send swap notification for bot {BotId}", _tradingBot?.Identifier);
}
}
+
+ ///
+ /// Pings the bot to reactivate it and ensure reminders are registered
+ /// Used during startup to reactivate bots that may have lost their reminders
+ /// The grain activation will automatically handle reminder registration
+ ///
+ public Task PingAsync()
+ {
+ try
+ {
+ _logger.LogInformation("Ping received for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
+
+ // The grain activation (OnActivateAsync) will automatically call ResumeBotIfRequiredAsync()
+ // which handles checking the registry status and re-registering reminders if needed
+ // So we just need to return true to indicate the ping was received
+ return Task.FromResult(true);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error during ping for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
+ return Task.FromResult(false);
+ }
+ }
}
\ No newline at end of file
diff --git a/src/Managing.Application/Grains/PriceFetcherInitializer.cs b/src/Managing.Application/Grains/GrainInitializer.cs
similarity index 67%
rename from src/Managing.Application/Grains/PriceFetcherInitializer.cs
rename to src/Managing.Application/Grains/GrainInitializer.cs
index f04cacb6..f1f81568 100644
--- a/src/Managing.Application/Grains/PriceFetcherInitializer.cs
+++ b/src/Managing.Application/Grains/GrainInitializer.cs
@@ -4,17 +4,35 @@ using static Managing.Common.Enums;
namespace Managing.Application.Grains;
-public class PriceFetcherInitializer : IHostedService
+public class GrainInitializer : IHostedService
{
private readonly IGrainFactory _grainFactory;
- public PriceFetcherInitializer(IClusterClient grainFactory)
+ public GrainInitializer(IClusterClient grainFactory)
{
_grainFactory = grainFactory;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
+ await InitializePriceFetcherAsync();
+ await InitializeBotRemindersAsync();
+ }
+
+ public async Task InitializeBotRemindersAsync()
+ {
+ if (Environment.GetEnvironmentVariable("TASK_SLOT") != "1")
+ return;
+
+ var grain = _grainFactory.GetGrain(0);
+ await grain.InitializeBotRemindersAsync();
+ }
+
+ public async Task InitializePriceFetcherAsync()
+ {
+ if (Environment.GetEnvironmentVariable("TASK_SLOT") != "1")
+ return;
+
// Initialize grains for different timeframes
var timeframes = new[]
{
diff --git a/src/Managing.Bootstrap/ApiBootstrap.cs b/src/Managing.Bootstrap/ApiBootstrap.cs
index b94bc360..00ed00cf 100644
--- a/src/Managing.Bootstrap/ApiBootstrap.cs
+++ b/src/Managing.Bootstrap/ApiBootstrap.cs
@@ -72,7 +72,7 @@ public static class ApiBootstrap
public static IServiceCollection AddHostedServices(this IServiceCollection services)
{
- services.AddHostedService();
+ services.AddHostedService();
return services;
}
@@ -342,7 +342,6 @@ public static class ApiBootstrap
;
}
-
private static IServiceCollection AddApplication(this IServiceCollection services)
{
services.AddScoped();