using System.Net; using System.Reflection; using Discord.Commands; using Discord.WebSocket; using FluentValidation; using Managing.Application; using Managing.Application.Abstractions; using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Application.Accounts; using Managing.Application.Agents; using Managing.Application.Backtests; using Managing.Application.Grains; using Managing.Application.ManageBot; using Managing.Application.ManageBot.Commands; using Managing.Application.MoneyManagements; using Managing.Application.Orleans; using Managing.Application.Scenarios; using Managing.Application.Shared; using Managing.Application.Shared.Behaviours; using Managing.Application.Synth; using Managing.Application.Trading; using Managing.Application.Trading.Commands; using Managing.Application.Trading.Handlers; using Managing.Application.Users; using Managing.Application.Workers; using Managing.Domain.Trades; using Managing.Infrastructure.Databases; using Managing.Infrastructure.Databases.InfluxDb; using Managing.Infrastructure.Databases.InfluxDb.Abstractions; using Managing.Infrastructure.Databases.InfluxDb.Models; using Managing.Infrastructure.Databases.PostgreSql; using Managing.Infrastructure.Databases.PostgreSql.Configurations; using Managing.Infrastructure.Evm; using Managing.Infrastructure.Evm.Services; using Managing.Infrastructure.Evm.Subgraphs; using Managing.Infrastructure.Exchanges; using Managing.Infrastructure.Exchanges.Abstractions; using Managing.Infrastructure.Exchanges.Exchanges; using Managing.Infrastructure.Messengers.Discord; using Managing.Infrastructure.Storage; using MediatR; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Orleans.Configuration; namespace Managing.Bootstrap; public static class ApiBootstrap { private static readonly Assembly ApplicationAssembly = typeof(StartBotCommand).GetTypeInfo().Assembly; public static IServiceCollection RegisterApiDependencies(this IServiceCollection services, IConfiguration configuration) { services.Configure(configuration.GetSection("Web3Proxy")); return services .AddApplication() .AddInfrastructure(configuration) .AddWorkers(configuration) .AddFluentValidation() .AddMediatR() ; } public static IServiceCollection AddHostedServices(this IServiceCollection services) { services.AddHostedService(); return services; } // Note: IClusterClient is automatically available in co-hosting scenarios // through IGrainFactory. Services should inject IGrainFactory instead of IClusterClient // to avoid circular dependency issues during DI container construction. public static IHostBuilder ConfigureOrleans(this IHostBuilder hostBuilder, IConfiguration configuration, bool isProduction) { // Check if Orleans grains should be active using root-level configuration var runOrleansGrains = configuration.GetValue("RunOrleansGrains", true); // Check environment variable as override var runOrleansGrainsEnv = Environment.GetEnvironmentVariable("RUN_ORLEANS_GRAINS"); if (!string.IsNullOrEmpty(runOrleansGrainsEnv) && bool.TryParse(runOrleansGrainsEnv, out var runOrleansGrainsFromEnv)) { runOrleansGrains = runOrleansGrainsFromEnv; } // Allow disabling Orleans clustering entirely in case of issues var disableOrleansClusteringEnv = Environment.GetEnvironmentVariable("DISABLE_ORLEANS_CLUSTERING"); var disableOrleansClustering = !string.IsNullOrEmpty(disableOrleansClusteringEnv) && bool.TryParse(disableOrleansClusteringEnv, out var disabled) && disabled; // Get TASK_SLOT for multi-instance configuration var taskSlotEnv = Environment.GetEnvironmentVariable("TASK_SLOT"); var taskSlot = 1; // Default to 1 if not provided if (!string.IsNullOrEmpty(taskSlotEnv) && int.TryParse(taskSlotEnv, out var parsedTaskSlot)) { taskSlot = parsedTaskSlot; } // Calculate unique ports based on task slot var siloPort = 11111 + (taskSlot - 1) * 10; // 11111, 11121, 11131, etc. var gatewayPort = 30000 + (taskSlot - 1) * 10; // 30000, 30010, 30020, etc. var dashboardPort = 9999 + (taskSlot - 1); // 9999, 10000, 10001, etc. // Get hostname for clustering - prioritize external IP for multi-server setups var hostname = Environment.GetEnvironmentVariable("CAPROVER_SERVER_IP") ?? // CapRover server IP Environment.GetEnvironmentVariable("EXTERNAL_IP") ?? // Custom external IP Environment.GetEnvironmentVariable("HOSTNAME") ?? // Container hostname Environment.GetEnvironmentVariable("COMPUTERNAME") ?? // Windows hostname "localhost"; // For Docker containers, always use localhost for same-server clustering IPAddress advertisedIP = IPAddress.Loopback; // Advertise as localhost for same-server clustering // Only use external IP if specifically provided for multi-server scenarios var externalIP = Environment.GetEnvironmentVariable("CAPROVER_SERVER_IP") ?? Environment.GetEnvironmentVariable("EXTERNAL_IP"); if (!string.IsNullOrEmpty(externalIP) && IPAddress.TryParse(externalIP, out var parsedExternalIP)) { advertisedIP = parsedExternalIP; Console.WriteLine($"Using external IP for multi-server clustering: {advertisedIP}"); } else { Console.WriteLine($"Using localhost for same-server clustering: {advertisedIP}"); } var postgreSqlConnectionString = configuration.GetSection("PostgreSql")["Orleans"]; var siloRole = Environment.GetEnvironmentVariable("SILO_ROLE") ?? "Trading"; Console.WriteLine($"Task Slot: {taskSlot}"); Console.WriteLine($"Hostname: {hostname}"); Console.WriteLine($"Advertised IP: {advertisedIP}"); Console.WriteLine($"Role: {siloRole}"); Console.WriteLine($"Silo port: {siloPort}"); Console.WriteLine($"Gateway port: {gatewayPort}"); Console.WriteLine($"Dashboard port: {dashboardPort}"); return hostBuilder.UseOrleans(siloBuilder => { // Configure clustering with improved networking or use localhost clustering if disabled if (!disableOrleansClustering && !string.IsNullOrEmpty(postgreSqlConnectionString)) { siloBuilder .UseAdoNetClustering(options => { options.ConnectionString = postgreSqlConnectionString; options.Invariant = "Npgsql"; }) .Configure(options => { // Advertise the specific IP for clustering options.AdvertisedIPAddress = advertisedIP; options.SiloPort = siloPort; options.GatewayPort = gatewayPort; options.SiloListeningEndpoint = new IPEndPoint(IPAddress.Any, siloPort); options.GatewayListeningEndpoint = new IPEndPoint(IPAddress.Any, gatewayPort); }) .Configure(options => { options.ServiceId = "ManagingApp"; options.ClusterId = configuration["ASPNETCORE_ENVIRONMENT"] ?? "Development"; }) .Configure(options => { // Configure silo address for multi-server clustering options.SiloName = $"ManagingApi-{taskSlot}-{siloRole}"; Console.WriteLine($"Configuring silo with role: {siloRole}"); }) .Configure(options => { // Increase timeout for grain deactivation during shutdown options.ResponseTimeout = TimeSpan.FromSeconds(30); }) .Configure(options => { // Configure grain collection timeouts options.CollectionAge = TimeSpan.FromMinutes(10); }); } else { // Fallback to localhost clustering for testing or when database is unavailable siloBuilder.UseLocalhostClustering(siloPort, gatewayPort) .Configure(options => { // Increase timeout for grain deactivation during shutdown options.ResponseTimeout = TimeSpan.FromSeconds(30); }) .Configure(options => { // Configure grain collection timeouts options.CollectionAge = TimeSpan.FromMinutes(10); }); } // Conditionally configure reminder service based on flag if (runOrleansGrains && !disableOrleansClustering && !string.IsNullOrEmpty(postgreSqlConnectionString)) { siloBuilder.UseAdoNetReminderService(options => { options.ConnectionString = postgreSqlConnectionString; options.Invariant = "Npgsql"; }); } // Configure networking - use specific IP for Docker containerized environments if (disableOrleansClustering) { // Use localhost clustering when clustering is disabled siloBuilder.ConfigureEndpoints(IPAddress.Loopback, siloPort, gatewayPort); } else if (string.IsNullOrEmpty(postgreSqlConnectionString)) { // In Docker/containerized environments, use endpoint configuration siloBuilder.Configure(options => { // Advertise the specific IP for clustering options.AdvertisedIPAddress = advertisedIP; options.SiloPort = siloPort; options.GatewayPort = gatewayPort; options.SiloListeningEndpoint = new IPEndPoint(IPAddress.Any, siloPort); options.GatewayListeningEndpoint = new IPEndPoint(IPAddress.Any, gatewayPort); }); } siloBuilder .Configure(options => { // Configure messaging for better reliability with increased timeouts options.ResponseTimeout = TimeSpan.FromSeconds(60); options.DropExpiredMessages = true; }) .Configure(options => { // Configure cluster membership for better resilience options.EnableIndirectProbes = true; options.ProbeTimeout = TimeSpan.FromSeconds(10); options.IAmAliveTablePublishTimeout = TimeSpan.FromSeconds(30); options.MaxJoinAttemptTime = TimeSpan.FromSeconds(120); // Improved settings for development environments with stale members options.DefunctSiloCleanupPeriod = TimeSpan.FromMinutes(1); options.DefunctSiloExpiration = TimeSpan.FromMinutes(2); }) .Configure(options => { // Configure gateway with improved timeouts options.GatewayListRefreshPeriod = TimeSpan.FromSeconds(60); }); // Conditionally configure grain execution based on flag if (runOrleansGrains) { siloBuilder.Configure(options => { // Enable grain collection for active grains options.CollectionAge = TimeSpan.FromMinutes(10); options.CollectionQuantum = TimeSpan.FromMinutes(1); }); } else { // Disable grain execution completely siloBuilder.Configure(options => { // Disable grain collection to prevent grains from running options.CollectionAge = TimeSpan.FromDays(365); // Very long collection age options.CollectionQuantum = TimeSpan.FromDays(1); // Very long quantum }); } siloBuilder .ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Information)); // Only enable dashboard in development to avoid shutdown issues if (!isProduction) { siloBuilder.UseDashboard(options => { // Configure dashboard with proper shutdown handling and unique ports per instance options.Port = dashboardPort; options.HostSelf = true; options.CounterUpdateIntervalMs = 10000; // 10 seconds options.HideTrace = true; // Hide trace to reduce dashboard overhead options.Host = "0.0.0.0"; // Allow external connections options.Username = "admin"; options.Password = "admin"; }); } // Configure grain storage - use ADO.NET for production or memory for fallback if (!disableOrleansClustering && !string.IsNullOrEmpty(postgreSqlConnectionString)) { siloBuilder .AddAdoNetGrainStorage("bot-store", options => { options.ConnectionString = postgreSqlConnectionString; options.Invariant = "Npgsql"; }) .AddAdoNetGrainStorage("registry-store", options => { options.ConnectionString = postgreSqlConnectionString; options.Invariant = "Npgsql"; }) .AddAdoNetGrainStorage("agent-store", options => { options.ConnectionString = postgreSqlConnectionString; options.Invariant = "Npgsql"; }) .AddAdoNetGrainStorage("platform-summary-store", options => { options.ConnectionString = postgreSqlConnectionString; options.Invariant = "Npgsql"; }) .AddAdoNetGrainStorage("candle-store", options => { options.ConnectionString = postgreSqlConnectionString; options.Invariant = "Npgsql"; }); } else { // Fallback to memory storage when database is unavailable siloBuilder .AddMemoryGrainStorage("bot-store") .AddMemoryGrainStorage("registry-store") .AddMemoryGrainStorage("agent-store") .AddMemoryGrainStorage("platform-summary-store") .AddMemoryGrainStorage("candle-store"); } // Configure Orleans Streams for price data distribution siloBuilder.AddMemoryStreams("ManagingStreamProvider") .AddMemoryGrainStorage("PubSubStore"); siloBuilder .ConfigureServices(services => { // Register custom placement directors for role-based placement services.AddPlacementDirector(); services.AddPlacementDirector(); // Register existing services for Orleans DI // These will be available to grains through dependency injection services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); // Use Singleton for InfluxDB repositories to prevent connection disposal issues in Orleans grains services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); }); }) ; } private static IServiceCollection AddApplication(this IServiceCollection services) { services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddTransient, OpenPositionCommandHandler>(); services.AddTransient, ClosePositionCommandHandler>(); // Processors services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddSingleton(); services.AddSingleton(); // Admin services services.AddSingleton(); return services; } private static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) { // Database services.AddSingleton(sp => sp.GetRequiredService>().Value); services.AddSingleton(sp => sp.GetRequiredService>().Value); services.Configure(configuration.GetSection("Kaigen")); // Evm services.AddGbcFeed(); services.AddUniswapV2(); services.AddChainlink(); services.AddChainlinkGmx(); services.AddSingleton(); // PostgreSql Repositories services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); // InfluxDb Repositories - Use Singleton for proper connection management in Orleans grains services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // Cache services.AddDistributedMemoryCache(); services.AddTransient(); services.AddSingleton(); // Services services.AddTransient(); services.AddSingleton(); return services; } private static IServiceCollection AddWorkers(this IServiceCollection services, IConfiguration configuration) { if (configuration.GetValue("WorkerNotifyBundleBacktest", false)) { services.AddHostedService(); } // Price Workers if (configuration.GetValue("WorkerPricesFifteenMinutes", false)) { services.AddHostedService(); } if (configuration.GetValue("WorkerPricesOneHour", false)) { services.AddHostedService(); } if (configuration.GetValue("WorkerPricesFourHours", false)) { services.AddHostedService(); } if (configuration.GetValue("WorkerPricesOneDay", false)) { services.AddHostedService(); } if (configuration.GetValue("WorkerPricesFiveMinutes", false)) { services.AddHostedService(); } // Other Workers if (configuration.GetValue("WorkerSpotlight", false)) { services.AddHostedService(); } if (configuration.GetValue("WorkerTraderWatcher", false)) { services.AddHostedService(); } if (configuration.GetValue("WorkerLeaderboard", false)) { services.AddHostedService(); } if (configuration.GetValue("WorkerFundingRatesWatcher", false)) { services.AddHostedService(); } if (configuration.GetValue("WorkerGeneticAlgorithm", false)) { services.AddHostedService(); } // DEPRECATED: BundleBacktestWorker has been replaced by BundleBacktestGrain // Bundle backtest processing is now handled by Orleans grain triggered directly from Backtester.cs // if (configuration.GetValue("WorkerBundleBacktest", false)) // { // services.AddHostedService(); // } return services; } private static IServiceCollection AddMediatR(this IServiceCollection services) { return services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(AppDomain.CurrentDomain.GetAssemblies())) .AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>)) .AddTransient(typeof(IUnhandledExceptionBehaviour<,>), typeof(UnhandledExceptionBehaviour<,>)); } private static IServiceCollection AddFluentValidation(this IServiceCollection services) { return services.AddValidatorsFromAssembly(ApplicationAssembly); } public static IWebHostBuilder SetupDiscordBot(this IWebHostBuilder builder) { return builder.ConfigureServices(services => { services .AddSingleton() .AddSingleton() .AddSingleton() .AddHostedService(); }); } }