diff --git a/src/Dockerfile-worker-api-dev b/src/Dockerfile-worker-api-dev index 3fa78b3b..c921aec4 100644 --- a/src/Dockerfile-worker-api-dev +++ b/src/Dockerfile-worker-api-dev @@ -5,7 +5,7 @@ WORKDIR /app # Use the official Microsoft .NET SDK image to build the code. FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /buildapp -COPY ["/src/Managing.Workers.Api/Managing.Workers.Api.csproj", "Managing.Workers.Api/"] +COPY ["/src/Managing.Workers/Managing.Workers.csproj", "Managing.Workers/"] COPY ["/src/Managing.Bootstrap/Managing.Bootstrap.csproj", "Managing.Bootstrap/"] COPY ["/src/Managing.Application/Managing.Application.csproj", "Managing.Application/"] COPY ["/src/Managing.Application.Abstractions/Managing.Application.Abstractions.csproj", "Managing.Application.Abstractions/"] @@ -17,15 +17,15 @@ COPY ["/src/Managing.Infrastructure.Exchanges/Managing.Infrastructure.Exchanges. COPY ["/src/Managing.Infrastructure.Messengers/Managing.Infrastructure.Messengers.csproj", "Managing.Infrastructure.Messengers/"] COPY ["/src/Managing.Infrastructure.Storage/Managing.Infrastructure.Storage.csproj", "Managing.Infrastructure.Storage/"] COPY ["/src/Managing.Infrastructure.Web3/Managing.Infrastructure.Evm.csproj", "Managing.Infrastructure.Web3/"] -RUN dotnet restore "/buildapp/Managing.Workers.Api/Managing.Workers.Api.csproj" +RUN dotnet restore "/buildapp/Managing.Workers/Managing.Workers.csproj" COPY . . -WORKDIR "/buildapp/src/Managing.Workers.Api" -RUN dotnet build "Managing.Workers.Api.csproj" -c Release -o /app/build +WORKDIR "/buildapp/src/Managing.Workers" +RUN dotnet build "Managing.Workers.csproj" -c Release -o /app/build FROM build AS publish -RUN dotnet publish "Managing.Workers.Api.csproj" -c Release -o /app/publish +RUN dotnet publish "Managing.Workers.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "Managing.Workers.Api.dll"] +ENTRYPOINT ["dotnet", "Managing.Workers.dll"] diff --git a/src/Managing.Infrastructure.Database/Migrations/20251108203747_RenameJobsTableToUppercase.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20251108203747_RenameJobsTableToUppercase.Designer.cs new file mode 100644 index 00000000..4b2a883d --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20251108203747_RenameJobsTableToUppercase.Designer.cs @@ -0,0 +1,1720 @@ +// +using System; +using Managing.Infrastructure.Databases.PostgreSql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + [DbContext(typeof(ManagingDbContext))] + [Migration("20251108203747_RenameJobsTableToUppercase")] + partial class RenameJobsTableToUppercase + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exchange") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsGmxInitialized") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Secret") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActiveStrategiesCount") + .HasColumnType("integer"); + + b.Property("AgentName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("BacktestCount") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Losses") + .HasColumnType("integer"); + + b.Property("NetPnL") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Runtime") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalBalance") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("TotalFees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("TotalPnL") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalROI") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalVolume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Wins") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AgentName") + .IsUnique(); + + b.HasIndex("TotalPnL"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("AgentSummaries"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Duration") + .ValueGeneratedOnAdd() + .HasColumnType("interval") + .HasDefaultValue(new TimeSpan(0, 0, 0, 0, 0)); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasColumnType("decimal(18,8)"); + + b.Property("FinalPnl") + .HasColumnType("decimal(18,8)"); + + b.Property("GrowthPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("HodlPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndicatorsCount") + .HasColumnType("integer"); + + b.Property("IndicatorsCsv") + .IsRequired() + .HasColumnType("text"); + + b.Property("InitialBalance") + .HasColumnType("decimal(18,8)"); + + b.Property("MaxDrawdown") + .ValueGeneratedOnAdd() + .HasColumnType("decimal(18,8)") + .HasDefaultValue(0m); + + b.Property("MaxDrawdownRecoveryTime") + .ValueGeneratedOnAdd() + .HasColumnType("interval") + .HasDefaultValue(new TimeSpan(0, 0, 0, 0, 0)); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("MoneyManagementJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("NetPnl") + .HasColumnType("decimal(18,8)"); + + b.Property("PositionsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RequestId") + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("Score") + .HasColumnType("double precision"); + + b.Property("ScoreMessage") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("text"); + + b.Property("SharpeRatio") + .ValueGeneratedOnAdd() + .HasColumnType("decimal(18,8)") + .HasDefaultValue(0m); + + b.Property("SignalsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StatisticsJson") + .HasColumnType("jsonb"); + + b.Property("Ticker") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Timeframe") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("WinRate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("RequestId"); + + b.HasIndex("Score"); + + b.HasIndex("UserId"); + + b.HasIndex("RequestId", "Score"); + + b.HasIndex("UserId", "Name"); + + b.HasIndex("UserId", "Score"); + + b.HasIndex("UserId", "Ticker"); + + b.HasIndex("UserId", "Timeframe"); + + b.ToTable("Backtests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("AccumulatedRunTimeSeconds") + .HasColumnType("bigint"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("LastStartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastStopTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LongPositionCount") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("NetPnL") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("ShortPositionCount") + .HasColumnType("integer"); + + b.Property("StartupTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("TradeLosses") + .HasColumnType("integer"); + + b.Property("TradeWins") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.HasKey("Identifier"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("Bots"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CompletedBacktests") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentBacktest") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("DateTimeRangesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("EstimatedTimeRemainingSeconds") + .HasColumnType("integer"); + + b.Property("FailedBacktests") + .HasColumnType("integer"); + + b.Property("MoneyManagementVariantsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProgressInfo") + .HasColumnType("text"); + + b.Property("RequestId") + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("ResultsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TickerVariantsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TotalBacktests") + .HasColumnType("integer"); + + b.Property("UniversalConfigJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.HasKey("Id"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "CreatedAt"); + + b.HasIndex("UserId", "Name", "Version"); + + b.ToTable("BundleBacktestRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FundingRateEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("OpenInterest") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Rate") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Exchange", "Date"); + + b.HasIndex("Ticker", "Exchange"); + + b.HasIndex("Ticker", "Exchange", "Date") + .IsUnique(); + + b.ToTable("FundingRates"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Balance") + .HasColumnType("decimal(18,8)"); + + b.Property("BestChromosome") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("BestFitness") + .HasColumnType("double precision"); + + b.Property("BestFitnessSoFar") + .HasColumnType("double precision"); + + b.Property("BestIndividual") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CrossoverMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentGeneration") + .HasColumnType("integer"); + + b.Property("EligibleIndicatorsJson") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ElitismPercentage") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Generations") + .HasColumnType("integer"); + + b.Property("MaxTakeProfit") + .HasColumnType("double precision"); + + b.Property("MutationMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("MutationRate") + .HasColumnType("double precision"); + + b.Property("PopulationSize") + .HasColumnType("integer"); + + b.Property("ProgressInfo") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SelectionMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("GeneticRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CyclePeriods") + .HasColumnType("integer"); + + b.Property("FastPeriods") + .HasColumnType("integer"); + + b.Property("MinimumHistory") + .HasColumnType("integer"); + + b.Property("Multiplier") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Period") + .HasColumnType("integer"); + + b.Property("SignalPeriods") + .HasColumnType("integer"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("SlowPeriods") + .HasColumnType("integer"); + + b.Property("SmoothPeriods") + .HasColumnType("integer"); + + b.Property("StochPeriods") + .HasColumnType("integer"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Name"); + + b.ToTable("Indicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.JobEntity", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssignedWorkerId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("BundleRequestId") + .HasColumnType("uuid"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("FailureCategory") + .HasColumnType("integer"); + + b.Property("GeneticRequestId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsRetryable") + .HasColumnType("boolean"); + + b.Property("JobType") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("LastHeartbeat") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxRetries") + .HasColumnType("integer"); + + b.Property("Priority") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("ProgressPercentage") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("RequestId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ResultJson") + .HasColumnType("jsonb"); + + b.Property("RetryAfter") + .HasColumnType("timestamp with time zone"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BundleRequestId") + .HasDatabaseName("idx_bundle_request"); + + b.HasIndex("GeneticRequestId") + .HasDatabaseName("idx_genetic_request"); + + b.HasIndex("AssignedWorkerId", "Status") + .HasDatabaseName("idx_assigned_worker"); + + b.HasIndex("UserId", "Status") + .HasDatabaseName("idx_user_status"); + + b.HasIndex("Status", "JobType", "Priority", "CreatedAt") + .HasDatabaseName("idx_status_jobtype_priority_created"); + + b.ToTable("Jobs", (string)null); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("StopLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("TakeProfit") + .HasColumnType("decimal(18,8)"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("UserName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "Name"); + + b.ToTable("MoneyManagements"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("GasFees") + .HasColumnType("decimal(18,8)"); + + b.Property("Initiator") + .IsRequired() + .HasColumnType("text"); + + b.Property("InitiatorIdentifier") + .HasColumnType("uuid"); + + b.Property("MoneyManagementJson") + .HasColumnType("text"); + + b.Property("NetPnL") + .HasColumnType("decimal(18,8)"); + + b.Property("OpenTradeId") + .HasColumnType("integer"); + + b.Property("OriginDirection") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProfitAndLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("SignalIdentifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("StopLossTradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit1TradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit2TradeId") + .HasColumnType("integer"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("UiFees") + .HasColumnType("decimal(18,8)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Identifier"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("InitiatorIdentifier"); + + b.HasIndex("OpenTradeId"); + + b.HasIndex("Status"); + + b.HasIndex("StopLossTradeId"); + + b.HasIndex("TakeProfit1TradeId"); + + b.HasIndex("TakeProfit2TradeId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Identifier"); + + b.ToTable("Positions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LoopbackPeriod") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Name"); + + b.ToTable("Scenarios"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IndicatorId") + .HasColumnType("integer"); + + b.Property("ScenarioId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IndicatorId"); + + b.HasIndex("ScenarioId", "IndicatorId") + .IsUnique(); + + b.ToTable("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandleJson") + .HasColumnType("text"); + + b.Property("Confidence") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndicatorName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Identifier"); + + b.HasIndex("Status"); + + b.HasIndex("Ticker"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Date"); + + b.HasIndex("Identifier", "Date", "UserId") + .IsUnique(); + + b.ToTable("Signals"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SpotlightOverviewEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Identifier") + .HasColumnType("uuid"); + + b.Property("ScenarioCount") + .HasColumnType("integer"); + + b.Property("SpotlightsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DateTime"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("DateTime", "ScenarioCount"); + + b.ToTable("SpotlightOverviews"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthMinersLeaderboardEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinersData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.ToTable("SynthMinersLeaderboards"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthPredictionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinerUid") + .HasColumnType("integer"); + + b.Property("PredictionData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.Property("TimeLength") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.ToTable("SynthPredictions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TopVolumeTickerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Date", "Rank"); + + b.HasIndex("Exchange", "Date"); + + b.ToTable("TopVolumeTickers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExchangeOrderId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("decimal(18,8)"); + + b.Property("Quantity") + .HasColumnType("decimal(18,8)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("TradeType") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("ExchangeOrderId"); + + b.HasIndex("Status"); + + b.ToTable("Trades"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TraderEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AverageLoss") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("AverageWin") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBestTrader") + .HasColumnType("boolean"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("TradeCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Winrate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("IsBestTrader"); + + b.HasIndex("Pnl"); + + b.HasIndex("Roi"); + + b.HasIndex("Winrate"); + + b.HasIndex("Address", "IsBestTrader") + .IsUnique(); + + b.HasIndex("IsBestTrader", "Roi"); + + b.HasIndex("IsBestTrader", "Winrate"); + + b.ToTable("Traders"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgentName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TelegramChannel") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("AgentName"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WhitelistAccountEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmbeddedWallet") + .IsRequired() + .HasMaxLength(42) + .HasColumnType("character varying(42)"); + + b.Property("ExternalEthereumAccount") + .HasMaxLength(42) + .HasColumnType("character varying(42)"); + + b.Property("IsWhitelisted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("PrivyCreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivyId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TwitterAccount") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("EmbeddedWallet") + .IsUnique(); + + b.HasIndex("ExternalEthereumAccount"); + + b.HasIndex("PrivyId") + .IsUnique(); + + b.HasIndex("TwitterAccount"); + + b.ToTable("WhitelistAccounts"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WorkerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DelayTicks") + .HasColumnType("bigint"); + + b.Property("ExecutionCount") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastRunTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkerType") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("WorkerType") + .IsUnique(); + + b.ToTable("Workers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany("Accounts") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.JobEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "OpenTrade") + .WithMany() + .HasForeignKey("OpenTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "StopLossTrade") + .WithMany() + .HasForeignKey("StopLossTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit1Trade") + .WithMany() + .HasForeignKey("TakeProfit1TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit2Trade") + .WithMany() + .HasForeignKey("TakeProfit2TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("OpenTrade"); + + b.Navigation("StopLossTrade"); + + b.Navigation("TakeProfit1Trade"); + + b.Navigation("TakeProfit2Trade"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", "Indicator") + .WithMany("ScenarioIndicators") + .HasForeignKey("IndicatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", "Scenario") + .WithMany("ScenarioIndicators") + .HasForeignKey("ScenarioId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Indicator"); + + b.Navigation("Scenario"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => + { + b.Navigation("Accounts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/20251108203747_RenameJobsTableToUppercase.cs b/src/Managing.Infrastructure.Database/Migrations/20251108203747_RenameJobsTableToUppercase.cs new file mode 100644 index 00000000..116ac871 --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20251108203747_RenameJobsTableToUppercase.cs @@ -0,0 +1,77 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + /// + public partial class RenameJobsTableToUppercase : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_jobs_Users_UserId", + schema: "public", + table: "jobs"); + + migrationBuilder.DropPrimaryKey( + name: "PK_jobs", + schema: "public", + table: "jobs"); + + migrationBuilder.RenameTable( + name: "jobs", + schema: "public", + newName: "Jobs"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Jobs", + table: "Jobs", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Jobs_Users_UserId", + table: "Jobs", + column: "UserId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Jobs_Users_UserId", + table: "Jobs"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Jobs", + table: "Jobs"); + + migrationBuilder.EnsureSchema( + name: "public"); + + migrationBuilder.RenameTable( + name: "Jobs", + newName: "jobs", + newSchema: "public"); + + migrationBuilder.AddPrimaryKey( + name: "PK_jobs", + schema: "public", + table: "jobs", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_jobs_Users_UserId", + schema: "public", + table: "jobs", + column: "UserId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs b/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs index 32188f55..e84626fb 100644 --- a/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs +++ b/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs @@ -807,7 +807,7 @@ namespace Managing.Infrastructure.Databases.Migrations b.HasIndex("Status", "JobType", "Priority", "CreatedAt") .HasDatabaseName("idx_status_jobtype_priority_created"); - b.ToTable("jobs", "public"); + b.ToTable("Jobs", (string)null); }); modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => diff --git a/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs b/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs index 577f369d..3cd81cf4 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs @@ -269,6 +269,9 @@ public class ManagingDbContext : DbContext .WithMany() .HasForeignKey(e => e.UserId) .OnDelete(DeleteBehavior.SetNull); + + // Explicitly set table name to "Jobs" (uppercase) + entity.ToTable("Jobs"); }); // Configure Scenario entity diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlJobRepository.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlJobRepository.cs index e1af51e6..1edb92aa 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlJobRepository.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlJobRepository.cs @@ -2,7 +2,10 @@ using Managing.Application.Abstractions.Repositories; using Managing.Domain.Backtests; using Managing.Infrastructure.Databases.PostgreSql.Entities; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Logging; +using Npgsql; +using NpgsqlTypes; using static Managing.Common.Enums; namespace Managing.Infrastructure.Databases.PostgreSql; @@ -32,43 +35,96 @@ public class PostgreSqlJobRepository : IJobRepository { // Use execution strategy to support retry with transactions var strategy = _context.Database.CreateExecutionStrategy(); - + return await strategy.ExecuteAsync(async () => { await using var transaction = await _context.Database.BeginTransactionAsync(); - + try { // Build SQL query with optional job type filter + // Use raw ADO.NET to avoid EF Core wrapping the query (which breaks FOR UPDATE SKIP LOCKED) var sql = @" - SELECT * FROM ""Jobs"" - WHERE ""Status"" = {0}"; - - var parameters = new List { (int)JobStatus.Pending }; - + SELECT ""Id"", ""BundleRequestId"", ""UserId"", ""Status"", ""JobType"", ""Priority"", + ""ConfigJson"", ""StartDate"", ""EndDate"", ""ProgressPercentage"", + ""AssignedWorkerId"", ""LastHeartbeat"", ""CreatedAt"", ""StartedAt"", + ""CompletedAt"", ""ResultJson"", ""ErrorMessage"", ""RequestId"", + ""GeneticRequestId"", ""RetryCount"", ""MaxRetries"", ""RetryAfter"", + ""IsRetryable"", ""FailureCategory"" + FROM ""Jobs"" + WHERE ""Status"" = @status"; + + var parameters = new List + { + new NpgsqlParameter("status", NpgsqlDbType.Integer) { Value = (int)JobStatus.Pending } + }; + if (jobType.HasValue) { - sql += @" AND ""JobType"" = {1}"; - parameters.Add((int)jobType.Value); + sql += @" AND ""JobType"" = @jobType"; + parameters.Add(new NpgsqlParameter("jobType", NpgsqlDbType.Integer) { Value = (int)jobType.Value }); } - + sql += @" ORDER BY ""Priority"" DESC, ""CreatedAt"" ASC LIMIT 1 FOR UPDATE SKIP LOCKED"; + + _logger.LogDebug("Claiming job with SQL: {Sql}, Parameters: Status={Status}, JobType={JobType}", + sql, (int)JobStatus.Pending, jobType.HasValue ? (int)jobType.Value : (int?)null); + + // Execute raw SQL using ADO.NET to get the job with row-level locking + var connection = _context.Database.GetDbConnection(); + await using var command = connection.CreateCommand(); + command.Transaction = transaction.GetDbTransaction(); + command.CommandText = sql; + command.Parameters.AddRange(parameters.ToArray()); + + JobEntity? job = null; + await using var reader = await command.ExecuteReaderAsync(); - // Use raw SQL with FromSqlRaw to get the next job with row-level locking - var job = await _context.Jobs - .FromSqlRaw(sql, parameters.ToArray()) - .FirstOrDefaultAsync(); + if (await reader.ReadAsync()) + { + job = new JobEntity + { + Id = reader.GetGuid(reader.GetOrdinal("Id")), + BundleRequestId = reader.IsDBNull(reader.GetOrdinal("BundleRequestId")) ? null : reader.GetGuid(reader.GetOrdinal("BundleRequestId")), + UserId = reader.GetInt32(reader.GetOrdinal("UserId")), + Status = reader.GetInt32(reader.GetOrdinal("Status")), + JobType = reader.GetInt32(reader.GetOrdinal("JobType")), + Priority = reader.GetInt32(reader.GetOrdinal("Priority")), + ConfigJson = reader.GetString(reader.GetOrdinal("ConfigJson")), + StartDate = reader.GetDateTime(reader.GetOrdinal("StartDate")), + EndDate = reader.GetDateTime(reader.GetOrdinal("EndDate")), + ProgressPercentage = reader.GetInt32(reader.GetOrdinal("ProgressPercentage")), + AssignedWorkerId = reader.IsDBNull(reader.GetOrdinal("AssignedWorkerId")) ? null : reader.GetString(reader.GetOrdinal("AssignedWorkerId")), + LastHeartbeat = reader.IsDBNull(reader.GetOrdinal("LastHeartbeat")) ? null : reader.GetDateTime(reader.GetOrdinal("LastHeartbeat")), + CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")), + StartedAt = reader.IsDBNull(reader.GetOrdinal("StartedAt")) ? null : reader.GetDateTime(reader.GetOrdinal("StartedAt")), + CompletedAt = reader.IsDBNull(reader.GetOrdinal("CompletedAt")) ? null : reader.GetDateTime(reader.GetOrdinal("CompletedAt")), + ResultJson = reader.IsDBNull(reader.GetOrdinal("ResultJson")) ? null : reader.GetString(reader.GetOrdinal("ResultJson")), + ErrorMessage = reader.IsDBNull(reader.GetOrdinal("ErrorMessage")) ? null : reader.GetString(reader.GetOrdinal("ErrorMessage")), + RequestId = reader.IsDBNull(reader.GetOrdinal("RequestId")) ? null : reader.GetString(reader.GetOrdinal("RequestId")), + GeneticRequestId = reader.IsDBNull(reader.GetOrdinal("GeneticRequestId")) ? null : reader.GetString(reader.GetOrdinal("GeneticRequestId")), + RetryCount = reader.GetInt32(reader.GetOrdinal("RetryCount")), + MaxRetries = reader.GetInt32(reader.GetOrdinal("MaxRetries")), + RetryAfter = reader.IsDBNull(reader.GetOrdinal("RetryAfter")) ? null : reader.GetDateTime(reader.GetOrdinal("RetryAfter")), + IsRetryable = reader.GetBoolean(reader.GetOrdinal("IsRetryable")), + FailureCategory = reader.IsDBNull(reader.GetOrdinal("FailureCategory")) ? null : reader.GetInt32(reader.GetOrdinal("FailureCategory")) + }; + } + + await reader.CloseAsync(); if (job == null) { - await transaction.RollbackAsync(); + _logger.LogDebug("No job found to claim for worker {WorkerId}", workerId); + await transaction.CommitAsync(); return null; } - // Update the job status atomically + // Attach and update the job entity + _context.Jobs.Attach(job); job.Status = (int)JobStatus.Running; job.AssignedWorkerId = workerId; job.StartedAt = DateTime.UtcNow; @@ -77,6 +133,7 @@ public class PostgreSqlJobRepository : IJobRepository await _context.SaveChangesAsync(); await transaction.CommitAsync(); + _logger.LogInformation("Claimed job {JobId} for worker {WorkerId}", job.Id, workerId); return MapToDomain(job); } catch (Exception ex) @@ -250,10 +307,10 @@ public class PostgreSqlJobRepository : IJobRepository public async Task> GetStaleJobsAsync(int timeoutMinutes = 5) { var timeoutThreshold = DateTime.UtcNow.AddMinutes(-timeoutMinutes); - + var entities = await _context.Jobs .Where(j => j.Status == (int)JobStatus.Running && - (j.LastHeartbeat == null || j.LastHeartbeat < timeoutThreshold)) + (j.LastHeartbeat == null || j.LastHeartbeat < timeoutThreshold)) .ToListAsync(); return entities.Select(MapToDomain); @@ -262,12 +319,12 @@ public class PostgreSqlJobRepository : IJobRepository public async Task ResetStaleJobsAsync(int timeoutMinutes = 5) { var timeoutThreshold = DateTime.UtcNow.AddMinutes(-timeoutMinutes); - + // Use AsTracking() to enable change tracking since DbContext uses NoTracking by default var staleJobs = await _context.Jobs .AsTracking() .Where(j => j.Status == (int)JobStatus.Running && - (j.LastHeartbeat == null || j.LastHeartbeat < timeoutThreshold)) + (j.LastHeartbeat == null || j.LastHeartbeat < timeoutThreshold)) .ToListAsync(); foreach (var job in staleJobs) @@ -495,5 +552,4 @@ public class PostgreSqlJobRepository : IJobRepository FailureCategory = entity.FailureCategory.HasValue ? (FailureCategory)entity.FailureCategory.Value : null }; } -} - +} \ No newline at end of file diff --git a/src/Managing.Workers.Api/Dockerfile b/src/Managing.Workers.Api/Dockerfile index 48fe0d1c..9a12b008 100644 --- a/src/Managing.Workers.Api/Dockerfile +++ b/src/Managing.Workers.Api/Dockerfile @@ -7,7 +7,7 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src # Copy project files for dependency restoration -COPY ["Managing.Workers.Api/Managing.Workers.Api.csproj", "Managing.Workers.Api/"] +COPY ["Managing.Workers/Managing.Workers.csproj", "Managing.Workers/"] COPY ["Managing.Bootstrap/Managing.Bootstrap.csproj", "Managing.Bootstrap/"] COPY ["Managing.Application/Managing.Application.csproj", "Managing.Application/"] COPY ["Managing.Application.Abstractions/Managing.Application.Abstractions.csproj", "Managing.Application.Abstractions/"] @@ -21,19 +21,19 @@ COPY ["Managing.Infrastructure.Storage/Managing.Infrastructure.Storage.csproj", COPY ["Managing.Infrastructure.Web3/Managing.Infrastructure.Evm.csproj", "Managing.Infrastructure.Web3/"] # Restore dependencies for all projects -RUN dotnet restore "Managing.Workers.Api/Managing.Workers.Api.csproj" +RUN dotnet restore "Managing.Workers/Managing.Workers.csproj" # Copy everything else and build COPY . . -WORKDIR "/src/Managing.Workers.Api" -RUN dotnet build "Managing.Workers.Api.csproj" -c Release -o /app/build +WORKDIR "/src/Managing.Workers" +RUN dotnet build "Managing.Workers.csproj" -c Release -o /app/build FROM build AS publish -RUN dotnet publish "Managing.Workers.Api.csproj" -c Release -o /app/publish +RUN dotnet publish "Managing.Workers.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "Managing.Workers.Api.dll"] +ENTRYPOINT ["dotnet", "Managing.Workers.dll"] diff --git a/src/Managing.Workers.Api/Managing.Workers.Api.csproj b/src/Managing.Workers.Api/Managing.Workers.csproj similarity index 92% rename from src/Managing.Workers.Api/Managing.Workers.Api.csproj rename to src/Managing.Workers.Api/Managing.Workers.csproj index 3539f4ae..90fe78bb 100644 --- a/src/Managing.Workers.Api/Managing.Workers.Api.csproj +++ b/src/Managing.Workers.Api/Managing.Workers.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - dotnet-Managing.Workers.Api-ff3f3987-4da4-4140-9180-b84c9e07b25f + dotnet-Managing.Workers-ff3f3987-4da4-4140-9180-b84c9e07b25f @@ -24,3 +24,4 @@ + diff --git a/src/Managing.Workers.Api/Program.cs b/src/Managing.Workers.Api/Program.cs deleted file mode 100644 index 287e34f8..00000000 --- a/src/Managing.Workers.Api/Program.cs +++ /dev/null @@ -1,171 +0,0 @@ -using Managing.Application.Workers; -using Managing.Bootstrap; -using Managing.Common; -using Managing.Infrastructure.Databases.InfluxDb.Models; -using Managing.Infrastructure.Databases.PostgreSql; -using Managing.Infrastructure.Databases.PostgreSql.Configurations; -using Microsoft.EntityFrameworkCore; - -var host = Host.CreateDefaultBuilder(args) - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(AppContext.BaseDirectory); - config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", optional: true) - .AddJsonFile("appsettings.SandboxLocal.json", optional: true, reloadOnChange: true) - .AddJsonFile("appsettings.ProductionLocal.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables() - .AddUserSecrets(); - }) - .ConfigureServices((hostContext, services) => - { - var configuration = hostContext.Configuration; - - // Initialize Sentry - SentrySdk.Init(options => - { - options.Dsn = configuration["Sentry:Dsn"]; - options.Debug = false; - options.SendDefaultPii = true; - options.AutoSessionTracking = true; - options.IsGlobalModeEnabled = false; - options.TracesSampleRate = 0.1; - options.Environment = hostContext.HostingEnvironment.EnvironmentName; - }); - - // Configure database - var postgreSqlConnectionString = configuration.GetSection(Constants.Databases.PostgreSql)["ConnectionString"]; - - services.Configure(configuration.GetSection(Constants.Databases.PostgreSql)); - services.Configure(configuration.GetSection(Constants.Databases.InfluxDb)); - - // Add DbContext - services.AddDbContext((serviceProvider, options) => - { - options.UseNpgsql(postgreSqlConnectionString, npgsqlOptions => - { - npgsqlOptions.CommandTimeout(60); - npgsqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(10), errorCodesToAdd: null); - }); - - if (hostContext.HostingEnvironment.IsDevelopment()) - { - options.EnableDetailedErrors(); - options.EnableSensitiveDataLogging(); - options.LogTo(Console.WriteLine, LogLevel.Information); // Enable SQL logging to debug table name issues - } - - options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); - options.EnableServiceProviderCaching(); - }, ServiceLifetime.Scoped); - - // Register compute dependencies (no Orleans) - services.RegisterComputeDependencies(configuration); - - // Configure BacktestComputeWorker options - services.Configure( - configuration.GetSection(BacktestComputeWorkerOptions.SectionName)); - - // Get task slot from CapRover ({{.Task.Slot}}) or environment variable - // This identifies which instance of the worker is running - var taskSlot = Environment.GetEnvironmentVariable("TASK_SLOT") ?? - Environment.GetEnvironmentVariable("CAPROVER_TASK_SLOT") ?? - "0"; - - // Override WorkerId from environment variable if provided, otherwise use task slot - var workerId = Environment.GetEnvironmentVariable("WORKER_ID") ?? - configuration["BacktestComputeWorker:WorkerId"] ?? - $"{Environment.MachineName}-{taskSlot}"; - services.Configure(options => - { - options.WorkerId = workerId; - }); - - // Configure GeneticComputeWorker options - services.Configure( - configuration.GetSection(GeneticComputeWorkerOptions.SectionName)); - - // Override Genetic WorkerId from environment variable if provided, otherwise use task slot - var geneticWorkerId = Environment.GetEnvironmentVariable("GENETIC_WORKER_ID") ?? - configuration["GeneticComputeWorker:WorkerId"] ?? - $"{Environment.MachineName}-genetic-{taskSlot}"; - services.Configure(options => - { - options.WorkerId = geneticWorkerId; - }); - - // Register the backtest compute worker if enabled - var isBacktestWorkerEnabled = configuration.GetValue("WorkerBacktestCompute", false); - if (isBacktestWorkerEnabled) - { - services.AddHostedService(); - } - - // Register the genetic compute worker if enabled - var isGeneticWorkerEnabled = configuration.GetValue("WorkerGeneticCompute", false); - if (isGeneticWorkerEnabled) - { - services.AddHostedService(); - } - }) - .ConfigureLogging((hostingContext, logging) => - { - logging.ClearProviders(); - logging.AddConsole(); - logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); - }) - .Build(); - -// Log worker status -var logger = host.Services.GetRequiredService>(); -var config = host.Services.GetRequiredService(); - -var isBacktestWorkerEnabled = config.GetValue("WorkerBacktestCompute", false); -var isGeneticWorkerEnabled = config.GetValue("WorkerGeneticCompute", false); - -if (isBacktestWorkerEnabled) -{ - var taskSlot = Environment.GetEnvironmentVariable("TASK_SLOT") ?? - Environment.GetEnvironmentVariable("CAPROVER_TASK_SLOT") ?? - "0"; - var backtestWorkerId = Environment.GetEnvironmentVariable("WORKER_ID") ?? - config["BacktestComputeWorker:WorkerId"] ?? - $"{Environment.MachineName}-{taskSlot}"; - logger.LogInformation("BacktestComputeWorker is enabled and will be started."); - logger.LogInformation("Backtest Worker ID: {WorkerId} (Task Slot: {TaskSlot})", backtestWorkerId, taskSlot); -} -else -{ - logger.LogWarning("BacktestComputeWorker is disabled via configuration. No backtest jobs will be processed."); -} - -if (isGeneticWorkerEnabled) -{ - var taskSlot = Environment.GetEnvironmentVariable("TASK_SLOT") ?? - Environment.GetEnvironmentVariable("CAPROVER_TASK_SLOT") ?? - "0"; - var geneticWorkerId = Environment.GetEnvironmentVariable("GENETIC_WORKER_ID") ?? - config["GeneticComputeWorker:WorkerId"] ?? - $"{Environment.MachineName}-genetic-{taskSlot}"; - logger.LogInformation("GeneticComputeWorker is enabled and will be started."); - logger.LogInformation("Genetic Worker ID: {WorkerId} (Task Slot: {TaskSlot})", geneticWorkerId, taskSlot); -} -else -{ - logger.LogWarning("GeneticComputeWorker is disabled via configuration. No genetic jobs will be processed."); -} - -try -{ - await host.RunAsync(); -} -catch (Exception ex) -{ - logger.LogCritical(ex, "Application terminated unexpectedly"); - SentrySdk.CaptureException(ex); - throw; -} -finally -{ - SentrySdk.FlushAsync(TimeSpan.FromSeconds(2)).Wait(); -} diff --git a/src/Managing.Workers.Api/Worker.cs b/src/Managing.Workers.Api/Worker.cs index bd089848..be0622e8 100644 --- a/src/Managing.Workers.Api/Worker.cs +++ b/src/Managing.Workers.Api/Worker.cs @@ -1,4 +1,4 @@ -namespace Managing.Workers.Api; +namespace Managing.Workers; public class Worker : BackgroundService { diff --git a/src/Managing.Workers/Dockerfile b/src/Managing.Workers/Dockerfile new file mode 100644 index 00000000..9a12b008 --- /dev/null +++ b/src/Managing.Workers/Dockerfile @@ -0,0 +1,39 @@ +# Use the official Microsoft .NET runtime as the base image (no ASP.NET needed for console worker) +FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base +WORKDIR /app + +# Use the official Microsoft .NET SDK image to build the code. +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src + +# Copy project files for dependency restoration +COPY ["Managing.Workers/Managing.Workers.csproj", "Managing.Workers/"] +COPY ["Managing.Bootstrap/Managing.Bootstrap.csproj", "Managing.Bootstrap/"] +COPY ["Managing.Application/Managing.Application.csproj", "Managing.Application/"] +COPY ["Managing.Application.Abstractions/Managing.Application.Abstractions.csproj", "Managing.Application.Abstractions/"] +COPY ["Managing.Common/Managing.Common.csproj", "Managing.Common/"] +COPY ["Managing.Core/Managing.Core.csproj", "Managing.Core/"] +COPY ["Managing.Domain/Managing.Domain.csproj", "Managing.Domain/"] +COPY ["Managing.Infrastructure.Database/Managing.Infrastructure.Databases.csproj", "Managing.Infrastructure.Database/"] +COPY ["Managing.Infrastructure.Exchanges/Managing.Infrastructure.Exchanges.csproj", "Managing.Infrastructure.Exchanges/"] +COPY ["Managing.Infrastructure.Messengers/Managing.Infrastructure.Messengers.csproj", "Managing.Infrastructure.Messengers/"] +COPY ["Managing.Infrastructure.Storage/Managing.Infrastructure.Storage.csproj", "Managing.Infrastructure.Storage/"] +COPY ["Managing.Infrastructure.Web3/Managing.Infrastructure.Evm.csproj", "Managing.Infrastructure.Web3/"] + +# Restore dependencies for all projects +RUN dotnet restore "Managing.Workers/Managing.Workers.csproj" + +# Copy everything else and build +COPY . . +WORKDIR "/src/Managing.Workers" +RUN dotnet build "Managing.Workers.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Managing.Workers.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . + +ENTRYPOINT ["dotnet", "Managing.Workers.dll"] + diff --git a/src/Managing.Workers/Managing.Workers.csproj b/src/Managing.Workers/Managing.Workers.csproj new file mode 100644 index 00000000..90fe78bb --- /dev/null +++ b/src/Managing.Workers/Managing.Workers.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + dotnet-Managing.Workers-ff3f3987-4da4-4140-9180-b84c9e07b25f + + + + + + + + + + + + + + + + + + + + diff --git a/src/Managing.Workers/Program.cs b/src/Managing.Workers/Program.cs new file mode 100644 index 00000000..44551dda --- /dev/null +++ b/src/Managing.Workers/Program.cs @@ -0,0 +1,295 @@ +using Managing.Application.Workers; +using Managing.Bootstrap; +using Managing.Common; +using Managing.Infrastructure.Databases.InfluxDb.Models; +using Managing.Infrastructure.Databases.PostgreSql; +using Managing.Infrastructure.Databases.PostgreSql.Configurations; +using Microsoft.EntityFrameworkCore; + +// Explicitly set the environment before creating the host builder +var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") + ?? Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") + ?? "Development"; + +// Log environment detection before host creation +var dotnetEnv = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT"); +var aspnetcoreEnv = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + +Console.WriteLine("═══════════════════════════════════════════════════════════"); +Console.WriteLine("🔧 Environment Configuration"); +Console.WriteLine("═══════════════════════════════════════════════════════════"); +Console.WriteLine($" DOTNET_ENVIRONMENT: {dotnetEnv ?? "(not set)"}"); +Console.WriteLine($" ASPNETCORE_ENVIRONMENT: {aspnetcoreEnv ?? "(not set)"}"); +Console.WriteLine($" Selected Environment: {environment}"); +Console.WriteLine("═══════════════════════════════════════════════════════════"); + +var host = Host.CreateDefaultBuilder(args) + .UseEnvironment(environment) // Explicitly set the environment + .ConfigureAppConfiguration((hostingContext, config) => + { + var detectedEnv = hostingContext.HostingEnvironment.EnvironmentName; + Console.WriteLine($" Detected Environment: {detectedEnv}"); + + if (detectedEnv != environment) + { + Console.WriteLine($" ⚠️ WARNING: Environment mismatch! Expected: {environment}, Got: {detectedEnv}"); + } + + config.SetBasePath(AppContext.BaseDirectory); + + // Load configuration files in order (later files override earlier ones) + // 1. Base appsettings.json (always loaded) + config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); + Console.WriteLine(" ✓ Loaded: appsettings.json"); + + // 2. Load ONLY the environment-specific file (not other environments) + if (!string.IsNullOrEmpty(detectedEnv)) + { + var envFile = $"appsettings.{detectedEnv}.json"; + config.AddJsonFile(envFile, optional: true, reloadOnChange: true); + Console.WriteLine($" ✓ Loaded: {envFile} (optional)"); + } + + // 3. Environment variables and user secrets (highest priority) + config.AddEnvironmentVariables() + .AddUserSecrets(); + + Console.WriteLine("═══════════════════════════════════════════════════════════"); + }) + .ConfigureServices((hostContext, services) => + { + var configuration = hostContext.Configuration; + + // Initialize Sentry + SentrySdk.Init(options => + { + options.Dsn = configuration["Sentry:Dsn"]; + options.Debug = false; + options.SendDefaultPii = true; + options.AutoSessionTracking = true; + options.IsGlobalModeEnabled = false; + options.TracesSampleRate = 0.1; + options.Environment = hostContext.HostingEnvironment.EnvironmentName; + }); + + // Configure database + var postgreSqlConnectionString = configuration.GetSection(Constants.Databases.PostgreSql)["ConnectionString"]; + + // Log database connection details (mask password for security) + if (!string.IsNullOrEmpty(postgreSqlConnectionString)) + { + var connectionParts = postgreSqlConnectionString.Split(';') + .Where(p => !string.IsNullOrWhiteSpace(p)) + .Select(p => p.Trim()) + .ToDictionary( + p => p.Split('=')[0].Trim(), + p => p.Contains('=') ? p.Substring(p.IndexOf('=') + 1).Trim() : string.Empty, + StringComparer.OrdinalIgnoreCase); + + var host = connectionParts.GetValueOrDefault("Host", "unknown"); + var port = connectionParts.GetValueOrDefault("Port", "unknown"); + var database = connectionParts.GetValueOrDefault("Database", "unknown"); + var username = connectionParts.GetValueOrDefault("Username", "unknown"); + var maskedConnectionString = postgreSqlConnectionString + .Replace(connectionParts.GetValueOrDefault("Password", ""), "***", StringComparison.OrdinalIgnoreCase); + + Console.WriteLine("═══════════════════════════════════════════════════════════"); + Console.WriteLine("📊 PostgreSQL Database Configuration"); + Console.WriteLine("═══════════════════════════════════════════════════════════"); + Console.WriteLine($" Host: {host}"); + Console.WriteLine($" Port: {port}"); + Console.WriteLine($" Database: {database}"); + Console.WriteLine($" Username: {username}"); + Console.WriteLine($" Connection String: {maskedConnectionString}"); + Console.WriteLine("═══════════════════════════════════════════════════════════"); + } + else + { + Console.WriteLine("⚠️ WARNING: PostgreSQL connection string is empty or not configured!"); + } + + services.Configure(configuration.GetSection(Constants.Databases.PostgreSql)); + services.Configure(configuration.GetSection(Constants.Databases.InfluxDb)); + + // Add DbContext + services.AddDbContext((serviceProvider, options) => + { + options.UseNpgsql(postgreSqlConnectionString, npgsqlOptions => + { + npgsqlOptions.CommandTimeout(60); + npgsqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(10), errorCodesToAdd: null); + }); + + if (hostContext.HostingEnvironment.IsDevelopment()) + { + options.EnableDetailedErrors(); + options.EnableSensitiveDataLogging(); + // SQL logging disabled - uncomment below line if needed for debugging + // options.LogTo(Console.WriteLine, LogLevel.Information); + } + + options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); + options.EnableServiceProviderCaching(); + }, ServiceLifetime.Scoped); + + // Register compute dependencies (no Orleans) + services.RegisterComputeDependencies(configuration); + + // Configure BacktestComputeWorker options + services.Configure( + configuration.GetSection(BacktestComputeWorkerOptions.SectionName)); + + // Get task slot from CapRover ({{.Task.Slot}}) or environment variable + // This identifies which instance of the worker is running + var taskSlot = Environment.GetEnvironmentVariable("TASK_SLOT") ?? + Environment.GetEnvironmentVariable("CAPROVER_TASK_SLOT") ?? + "0"; + + // Override WorkerId from environment variable if provided, otherwise use task slot + var workerId = Environment.GetEnvironmentVariable("WORKER_ID") ?? + configuration["BacktestComputeWorker:WorkerId"] ?? + $"{Environment.MachineName}-{taskSlot}"; + services.Configure(options => + { + options.WorkerId = workerId; + }); + + // Configure GeneticComputeWorker options + services.Configure( + configuration.GetSection(GeneticComputeWorkerOptions.SectionName)); + + // Override Genetic WorkerId from environment variable if provided, otherwise use task slot + var geneticWorkerId = Environment.GetEnvironmentVariable("GENETIC_WORKER_ID") ?? + configuration["GeneticComputeWorker:WorkerId"] ?? + $"{Environment.MachineName}-genetic-{taskSlot}"; + services.Configure(options => + { + options.WorkerId = geneticWorkerId; + }); + + // Register the backtest compute worker if enabled + var isBacktestWorkerEnabled = configuration.GetValue("WorkerBacktestCompute", false); + if (isBacktestWorkerEnabled) + { + services.AddHostedService(); + } + + // Register the genetic compute worker if enabled + var isGeneticWorkerEnabled = configuration.GetValue("WorkerGeneticCompute", false); + if (isGeneticWorkerEnabled) + { + services.AddHostedService(); + } + }) + .ConfigureLogging((hostingContext, logging) => + { + logging.ClearProviders(); + logging.AddConsole(); + logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + }) + .Build(); + +// Log worker status +var logger = host.Services.GetRequiredService>(); +var config = host.Services.GetRequiredService(); + +// Log PostgreSQL connection details +var postgreSqlConnectionString = config.GetSection(Constants.Databases.PostgreSql)["ConnectionString"]; +if (!string.IsNullOrEmpty(postgreSqlConnectionString)) +{ + var connectionParts = postgreSqlConnectionString.Split(';') + .Where(p => !string.IsNullOrWhiteSpace(p)) + .Select(p => p.Trim()) + .ToDictionary( + p => p.Split('=')[0].Trim(), + p => p.Contains('=') ? p.Substring(p.IndexOf('=') + 1).Trim() : string.Empty, + StringComparer.OrdinalIgnoreCase); + + var hostName = connectionParts.GetValueOrDefault("Host", "unknown"); + var port = connectionParts.GetValueOrDefault("Port", "unknown"); + var database = connectionParts.GetValueOrDefault("Database", "unknown"); + var username = connectionParts.GetValueOrDefault("Username", "unknown"); + + logger.LogInformation("═══════════════════════════════════════════════════════════"); + logger.LogInformation("📊 PostgreSQL Database Configuration"); + logger.LogInformation("═══════════════════════════════════════════════════════════"); + logger.LogInformation(" Host: {Host}", hostName); + logger.LogInformation(" Port: {Port}", port); + logger.LogInformation(" Database: {Database}", database); + logger.LogInformation(" Username: {Username}", username); + logger.LogInformation("═══════════════════════════════════════════════════════════"); + + // Test database connection + try + { + using var scope = host.Services.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + var canConnect = await dbContext.Database.CanConnectAsync(); + if (canConnect) + { + logger.LogInformation("✅ Database connection test: SUCCESS"); + } + else + { + logger.LogWarning("⚠️ Database connection test: FAILED - Cannot connect to database"); + } + } + catch (Exception ex) + { + logger.LogError(ex, "❌ Database connection test: FAILED - {Error}", ex.Message); + } +} +else +{ + logger.LogWarning("⚠️ WARNING: PostgreSQL connection string is empty or not configured!"); +} + +var isBacktestWorkerEnabled = config.GetValue("WorkerBacktestCompute", false); +var isGeneticWorkerEnabled = config.GetValue("WorkerGeneticCompute", false); + +if (isBacktestWorkerEnabled) +{ + var taskSlot = Environment.GetEnvironmentVariable("TASK_SLOT") ?? + Environment.GetEnvironmentVariable("CAPROVER_TASK_SLOT") ?? + "0"; + var backtestWorkerId = Environment.GetEnvironmentVariable("WORKER_ID") ?? + config["BacktestComputeWorker:WorkerId"] ?? + $"{Environment.MachineName}-{taskSlot}"; + logger.LogInformation("BacktestComputeWorker is enabled and will be started."); + logger.LogInformation("Backtest Worker ID: {WorkerId} (Task Slot: {TaskSlot})", backtestWorkerId, taskSlot); +} +else +{ + logger.LogWarning("BacktestComputeWorker is disabled via configuration. No backtest jobs will be processed."); +} + +if (isGeneticWorkerEnabled) +{ + var taskSlot = Environment.GetEnvironmentVariable("TASK_SLOT") ?? + Environment.GetEnvironmentVariable("CAPROVER_TASK_SLOT") ?? + "0"; + var geneticWorkerId = Environment.GetEnvironmentVariable("GENETIC_WORKER_ID") ?? + config["GeneticComputeWorker:WorkerId"] ?? + $"{Environment.MachineName}-genetic-{taskSlot}"; + logger.LogInformation("GeneticComputeWorker is enabled and will be started."); + logger.LogInformation("Genetic Worker ID: {WorkerId} (Task Slot: {TaskSlot})", geneticWorkerId, taskSlot); +} +else +{ + logger.LogWarning("GeneticComputeWorker is disabled via configuration. No genetic jobs will be processed."); +} + +try +{ + await host.RunAsync(); +} +catch (Exception ex) +{ + logger.LogCritical(ex, "Application terminated unexpectedly"); + SentrySdk.CaptureException(ex); + throw; +} +finally +{ + SentrySdk.FlushAsync(TimeSpan.FromSeconds(2)).Wait(); +} diff --git a/src/Managing.Workers/Worker.cs b/src/Managing.Workers/Worker.cs new file mode 100644 index 00000000..be0622e8 --- /dev/null +++ b/src/Managing.Workers/Worker.cs @@ -0,0 +1,24 @@ +namespace Managing.Workers; + +public class Worker : BackgroundService +{ + private readonly ILogger _logger; + + public Worker(ILogger logger) + { + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + if (_logger.IsEnabled(LogLevel.Information)) + { + _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); + } + + await Task.Delay(1000, stoppingToken); + } + } +} \ No newline at end of file diff --git a/src/Managing.Workers.Api/appsettings.Development.json b/src/Managing.Workers/appsettings.Development.json similarity index 96% rename from src/Managing.Workers.Api/appsettings.Development.json rename to src/Managing.Workers/appsettings.Development.json index 52671f2a..4d5c399e 100644 --- a/src/Managing.Workers.Api/appsettings.Development.json +++ b/src/Managing.Workers/appsettings.Development.json @@ -7,6 +7,7 @@ }, "WorkerBacktestCompute": true, "BacktestComputeWorker": { + "WorkerId": "Oda-backtest-0", "MaxConcurrentBacktests": 6, "JobPollIntervalSeconds": 5, "HeartbeatIntervalSeconds": 30, diff --git a/src/Managing.Workers.Api/appsettings.ProductionLocal.json b/src/Managing.Workers/appsettings.ProductionLocal.json similarity index 100% rename from src/Managing.Workers.Api/appsettings.ProductionLocal.json rename to src/Managing.Workers/appsettings.ProductionLocal.json diff --git a/src/Managing.Workers.Api/appsettings.SandboxLocal.json b/src/Managing.Workers/appsettings.SandboxLocal.json similarity index 100% rename from src/Managing.Workers.Api/appsettings.SandboxLocal.json rename to src/Managing.Workers/appsettings.SandboxLocal.json diff --git a/src/Managing.Workers.Api/appsettings.json b/src/Managing.Workers/appsettings.json similarity index 74% rename from src/Managing.Workers.Api/appsettings.json rename to src/Managing.Workers/appsettings.json index 54613c53..eee55bfc 100644 --- a/src/Managing.Workers.Api/appsettings.json +++ b/src/Managing.Workers/appsettings.json @@ -19,14 +19,6 @@ "HeartbeatIntervalSeconds": 30, "StaleJobTimeoutMinutes": 10 }, - "PostgreSql": { - "ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37" - }, - "InfluxDb": { - "Url": "https://influx-db.apps.managing.live", - "Organization": "managing-org", - "Token": "eOuXcXhH7CS13Iw4CTiDDpRjIjQtEVPOloD82pLPOejI4n0BsEj1YzUw0g3Cs1mdDG5m-RaxCavCMsVTtS5wIQ==" - }, "Sentry": { "Dsn": "https://fe12add48c56419bbdfa86227c188e7a@glitch.kai.managing.live/1" }, diff --git a/src/Managing.Workers.Api/captain-definition b/src/Managing.Workers/captain-definition similarity index 100% rename from src/Managing.Workers.Api/captain-definition rename to src/Managing.Workers/captain-definition diff --git a/src/Managing.sln b/src/Managing.sln index a1b74b25..a849dc02 100644 --- a/src/Managing.sln +++ b/src/Managing.sln @@ -68,7 +68,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Managing.Aspire.ServiceDefa EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Managing.Nswag", "Managing.Nswag\Managing.Nswag.csproj", "{BE50F950-C1D4-4CE0-B32E-6AAC996770D5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Managing.Workers.Api", "Managing.Workers.Api\Managing.Workers.Api.csproj", "{B7D66A73-CA3A-4DE5-8E88-59D50C4018A6}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Managing.Workers", "Managing.Workers\Managing.Workers.csproj", "{B7D66A73-CA3A-4DE5-8E88-59D50C4018A6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution