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