Add genetic backtest to worker
This commit is contained in:
@@ -816,7 +816,7 @@ public class BacktestController : BaseController
|
||||
|
||||
// Get all jobs for this bundle
|
||||
using var serviceScope = _serviceScopeFactory.CreateScope();
|
||||
var jobRepository = serviceScope.ServiceProvider.GetRequiredService<IBacktestJobRepository>();
|
||||
var jobRepository = serviceScope.ServiceProvider.GetRequiredService<IJobRepository>();
|
||||
var jobs = await jobRepository.GetByBundleRequestIdAsync(bundleGuid);
|
||||
|
||||
var response = new BundleBacktestStatusResponse
|
||||
@@ -824,10 +824,10 @@ public class BacktestController : BaseController
|
||||
BundleRequestId = bundleGuid,
|
||||
Status = bundleRequest.Status.ToString(),
|
||||
TotalJobs = jobs.Count(),
|
||||
CompletedJobs = jobs.Count(j => j.Status == BacktestJobStatus.Completed),
|
||||
FailedJobs = jobs.Count(j => j.Status == BacktestJobStatus.Failed),
|
||||
RunningJobs = jobs.Count(j => j.Status == BacktestJobStatus.Running),
|
||||
PendingJobs = jobs.Count(j => j.Status == BacktestJobStatus.Pending),
|
||||
CompletedJobs = jobs.Count(j => j.Status == JobStatus.Completed),
|
||||
FailedJobs = jobs.Count(j => j.Status == JobStatus.Failed),
|
||||
RunningJobs = jobs.Count(j => j.Status == JobStatus.Running),
|
||||
PendingJobs = jobs.Count(j => j.Status == JobStatus.Pending),
|
||||
ProgressPercentage = bundleRequest.ProgressPercentage,
|
||||
CreatedAt = bundleRequest.CreatedAt,
|
||||
CompletedAt = bundleRequest.CompletedAt,
|
||||
|
||||
@@ -68,7 +68,7 @@ public class JobController : BaseController
|
||||
/// <param name="jobId">The job ID to query</param>
|
||||
/// <returns>The job status and result if completed</returns>
|
||||
[HttpGet("{jobId}")]
|
||||
public async Task<ActionResult<BacktestJobStatusResponse>> GetJobStatus(string jobId)
|
||||
public async Task<ActionResult<JobStatusResponse>> GetJobStatus(string jobId)
|
||||
{
|
||||
if (!await IsUserAdmin())
|
||||
{
|
||||
@@ -82,7 +82,7 @@ public class JobController : BaseController
|
||||
}
|
||||
|
||||
using var serviceScope = _serviceScopeFactory.CreateScope();
|
||||
var jobRepository = serviceScope.ServiceProvider.GetRequiredService<IBacktestJobRepository>();
|
||||
var jobRepository = serviceScope.ServiceProvider.GetRequiredService<IJobRepository>();
|
||||
var job = await jobRepository.GetByIdAsync(jobGuid);
|
||||
|
||||
if (job == null)
|
||||
@@ -90,7 +90,7 @@ public class JobController : BaseController
|
||||
return NotFound($"Job with ID {jobId} not found.");
|
||||
}
|
||||
|
||||
var response = new BacktestJobStatusResponse
|
||||
var response = new JobStatusResponse
|
||||
{
|
||||
JobId = job.Id,
|
||||
Status = job.Status.ToString(),
|
||||
@@ -99,7 +99,7 @@ public class JobController : BaseController
|
||||
StartedAt = job.StartedAt,
|
||||
CompletedAt = job.CompletedAt,
|
||||
ErrorMessage = job.ErrorMessage,
|
||||
Result = job.Status == BacktestJobStatus.Completed && !string.IsNullOrEmpty(job.ResultJson)
|
||||
Result = job.Status == JobStatus.Completed && !string.IsNullOrEmpty(job.ResultJson)
|
||||
? JsonSerializer.Deserialize<LightBacktest>(job.ResultJson)
|
||||
: null
|
||||
};
|
||||
@@ -156,16 +156,16 @@ public class JobController : BaseController
|
||||
}
|
||||
|
||||
// Parse status filter
|
||||
BacktestJobStatus? statusFilter = null;
|
||||
JobStatus? statusFilter = null;
|
||||
if (!string.IsNullOrEmpty(status))
|
||||
{
|
||||
if (Enum.TryParse<BacktestJobStatus>(status, true, out var parsedStatus))
|
||||
if (Enum.TryParse<JobStatus>(status, true, out var parsedStatus))
|
||||
{
|
||||
statusFilter = parsedStatus;
|
||||
}
|
||||
else
|
||||
{
|
||||
return BadRequest($"Invalid status value. Valid values are: {string.Join(", ", Enum.GetNames<BacktestJobStatus>())}");
|
||||
return BadRequest($"Invalid status value. Valid values are: {string.Join(", ", Enum.GetNames<JobStatus>())}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ public class JobController : BaseController
|
||||
}
|
||||
|
||||
using var serviceScope = _serviceScopeFactory.CreateScope();
|
||||
var jobRepository = serviceScope.ServiceProvider.GetRequiredService<IBacktestJobRepository>();
|
||||
var jobRepository = serviceScope.ServiceProvider.GetRequiredService<IJobRepository>();
|
||||
|
||||
var (jobs, totalCount) = await jobRepository.GetPaginatedAsync(
|
||||
page,
|
||||
@@ -257,7 +257,7 @@ public class JobController : BaseController
|
||||
}
|
||||
|
||||
using var serviceScope = _serviceScopeFactory.CreateScope();
|
||||
var jobRepository = serviceScope.ServiceProvider.GetRequiredService<IBacktestJobRepository>();
|
||||
var jobRepository = serviceScope.ServiceProvider.GetRequiredService<IJobRepository>();
|
||||
|
||||
var summary = await jobRepository.GetSummaryAsync();
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ using Managing.Domain.Backtests;
|
||||
namespace Managing.Api.Models.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// Response model for backtest job status
|
||||
/// Response model for job status
|
||||
/// </summary>
|
||||
public class BacktestJobStatusResponse
|
||||
public class JobStatusResponse
|
||||
{
|
||||
public Guid JobId { get; set; }
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
@@ -4,14 +4,14 @@ using static Managing.Common.Enums;
|
||||
namespace Managing.Application.Abstractions.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository interface for managing backtest jobs in the queue system
|
||||
/// Repository interface for managing jobs in the queue system
|
||||
/// </summary>
|
||||
public interface IBacktestJobRepository
|
||||
public interface IJobRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new backtest job
|
||||
/// Creates a new job
|
||||
/// </summary>
|
||||
Task<BacktestJob> CreateAsync(BacktestJob job);
|
||||
Task<Job> CreateAsync(Job job);
|
||||
|
||||
/// <summary>
|
||||
/// Claims the next available job using PostgreSQL advisory locks.
|
||||
@@ -19,33 +19,33 @@ public interface IBacktestJobRepository
|
||||
/// </summary>
|
||||
/// <param name="workerId">The ID of the worker claiming the job</param>
|
||||
/// <param name="jobType">Optional job type filter. If null, claims any job type.</param>
|
||||
Task<BacktestJob?> ClaimNextJobAsync(string workerId, JobType? jobType = null);
|
||||
Task<Job?> ClaimNextJobAsync(string workerId, JobType? jobType = null);
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing job
|
||||
/// </summary>
|
||||
Task UpdateAsync(BacktestJob job);
|
||||
Task UpdateAsync(Job job);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all jobs for a specific bundle request
|
||||
/// </summary>
|
||||
Task<IEnumerable<BacktestJob>> GetByBundleRequestIdAsync(Guid bundleRequestId);
|
||||
Task<IEnumerable<Job>> GetByBundleRequestIdAsync(Guid bundleRequestId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all jobs for a specific user
|
||||
/// </summary>
|
||||
Task<IEnumerable<BacktestJob>> GetByUserIdAsync(int userId);
|
||||
Task<IEnumerable<Job>> GetByUserIdAsync(int userId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a job by its ID
|
||||
/// </summary>
|
||||
Task<BacktestJob?> GetByIdAsync(Guid jobId);
|
||||
Task<Job?> GetByIdAsync(Guid jobId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets stale jobs (jobs that are Running but haven't sent a heartbeat in the specified timeout)
|
||||
/// </summary>
|
||||
/// <param name="timeoutMinutes">Number of minutes since last heartbeat to consider stale</param>
|
||||
Task<IEnumerable<BacktestJob>> GetStaleJobsAsync(int timeoutMinutes = 5);
|
||||
Task<IEnumerable<Job>> GetStaleJobsAsync(int timeoutMinutes = 5);
|
||||
|
||||
/// <summary>
|
||||
/// Resets stale jobs back to Pending status
|
||||
@@ -55,12 +55,12 @@ public interface IBacktestJobRepository
|
||||
/// <summary>
|
||||
/// Gets all running jobs assigned to a specific worker
|
||||
/// </summary>
|
||||
Task<IEnumerable<BacktestJob>> GetRunningJobsByWorkerIdAsync(string workerId);
|
||||
Task<IEnumerable<Job>> GetRunningJobsByWorkerIdAsync(string workerId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all jobs for a specific genetic request ID
|
||||
/// </summary>
|
||||
Task<IEnumerable<BacktestJob>> GetByGeneticRequestIdAsync(string geneticRequestId);
|
||||
Task<IEnumerable<Job>> GetByGeneticRequestIdAsync(string geneticRequestId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets paginated jobs with optional filters and sorting
|
||||
@@ -75,12 +75,12 @@ public interface IBacktestJobRepository
|
||||
/// <param name="workerId">Optional worker ID filter</param>
|
||||
/// <param name="bundleRequestId">Optional bundle request ID filter</param>
|
||||
/// <returns>Tuple of jobs and total count</returns>
|
||||
Task<(IEnumerable<BacktestJob> Jobs, int TotalCount)> GetPaginatedAsync(
|
||||
Task<(IEnumerable<Job> Jobs, int TotalCount)> GetPaginatedAsync(
|
||||
int page,
|
||||
int pageSize,
|
||||
string sortBy = "CreatedAt",
|
||||
string sortOrder = "desc",
|
||||
BacktestJobStatus? status = null,
|
||||
JobStatus? status = null,
|
||||
JobType? jobType = null,
|
||||
int? userId = null,
|
||||
string? workerId = null,
|
||||
@@ -109,7 +109,7 @@ public class JobSummary
|
||||
/// </summary>
|
||||
public class JobStatusCount
|
||||
{
|
||||
public BacktestJobStatus Status { get; set; }
|
||||
public JobStatus Status { get; set; }
|
||||
public int Count { get; set; }
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ public class JobTypeCount
|
||||
/// </summary>
|
||||
public class JobStatusTypeCount
|
||||
{
|
||||
public BacktestJobStatus Status { get; set; }
|
||||
public JobStatus Status { get; set; }
|
||||
public JobType JobType { get; set; }
|
||||
public int Count { get; set; }
|
||||
}
|
||||
@@ -43,12 +43,12 @@ namespace Managing.Application.Tests
|
||||
var tradingBotLogger = TradingBaseTests.CreateTradingBotLogger();
|
||||
var backtestLogger = TradingBaseTests.CreateBacktesterLogger();
|
||||
ILoggerFactory loggerFactory = new NullLoggerFactory();
|
||||
var backtestJobLogger = loggerFactory.CreateLogger<BacktestJobService>();
|
||||
var backtestJobLogger = loggerFactory.CreateLogger<JobService>();
|
||||
var botService = new Mock<IBotService>().Object;
|
||||
var agentService = new Mock<IAgentService>().Object;
|
||||
var _scopeFactory = new Mock<IServiceScopeFactory>();
|
||||
var backtestJobRepository = new Mock<IBacktestJobRepository>().Object;
|
||||
var backtestJobService = new BacktestJobService(backtestJobRepository, backtestRepository, kaigenService, backtestJobLogger);
|
||||
var backtestJobRepository = new Mock<IJobRepository>().Object;
|
||||
var backtestJobService = new JobService(backtestJobRepository, backtestRepository, kaigenService, backtestJobLogger);
|
||||
_backtester = new Backtester(_exchangeService, backtestRepository, backtestLogger,
|
||||
scenarioService, _accountService.Object, messengerService, kaigenService, hubContext, _scopeFactory.Object,
|
||||
backtestJobService);
|
||||
|
||||
122
src/Managing.Application/Backtests/BacktestExecutorAdapter.cs
Normal file
122
src/Managing.Application/Backtests/BacktestExecutorAdapter.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Abstractions.Shared;
|
||||
using Managing.Domain.Backtests;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Users;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Backtests;
|
||||
|
||||
/// <summary>
|
||||
/// Adapter that wraps BacktestExecutor to implement IBacktester interface.
|
||||
/// Used in compute workers where Backtester (with SignalR dependencies) is not available.
|
||||
/// Only implements methods needed for genetic algorithm execution.
|
||||
/// </summary>
|
||||
public class BacktestExecutorAdapter : IBacktester
|
||||
{
|
||||
private readonly BacktestExecutor _executor;
|
||||
private readonly IExchangeService _exchangeService;
|
||||
private readonly ILogger<BacktestExecutorAdapter> _logger;
|
||||
|
||||
public BacktestExecutorAdapter(
|
||||
BacktestExecutor executor,
|
||||
IExchangeService exchangeService,
|
||||
ILogger<BacktestExecutorAdapter> logger)
|
||||
{
|
||||
_executor = executor;
|
||||
_exchangeService = exchangeService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<LightBacktest> RunTradingBotBacktest(
|
||||
TradingBotConfig config,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
User user = null,
|
||||
bool save = false,
|
||||
bool withCandles = false,
|
||||
string requestId = null,
|
||||
object metadata = null)
|
||||
{
|
||||
// Load candles using ExchangeService
|
||||
var candles = await _exchangeService.GetCandlesInflux(
|
||||
TradingExchanges.Evm,
|
||||
config.Ticker,
|
||||
startDate,
|
||||
config.Timeframe,
|
||||
endDate);
|
||||
|
||||
if (candles == null || candles.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"No candles found for {config.Ticker} on {config.Timeframe} from {startDate} to {endDate}");
|
||||
}
|
||||
|
||||
// Execute using BacktestExecutor
|
||||
var result = await _executor.ExecuteAsync(
|
||||
config,
|
||||
candles,
|
||||
user,
|
||||
save,
|
||||
withCandles,
|
||||
requestId,
|
||||
metadata,
|
||||
progressCallback: null);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<LightBacktest> RunTradingBotBacktest(
|
||||
TradingBotConfig config,
|
||||
HashSet<Candle> candles,
|
||||
User user = null,
|
||||
bool withCandles = false,
|
||||
string requestId = null,
|
||||
object metadata = null)
|
||||
{
|
||||
// Execute using BacktestExecutor
|
||||
var result = await _executor.ExecuteAsync(
|
||||
config,
|
||||
candles,
|
||||
user,
|
||||
save: false,
|
||||
withCandles,
|
||||
requestId,
|
||||
metadata,
|
||||
progressCallback: null);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Methods not needed for compute worker - throw NotImplementedException
|
||||
public Task<bool> DeleteBacktestAsync(string id) => throw new NotImplementedException("Not available in compute worker");
|
||||
public bool DeleteBacktests() => throw new NotImplementedException("Not available in compute worker");
|
||||
public IEnumerable<Backtest> GetBacktestsByUser(User user) => throw new NotImplementedException("Not available in compute worker");
|
||||
public Task<IEnumerable<Backtest>> GetBacktestsByUserAsync(User user) => throw new NotImplementedException("Not available in compute worker");
|
||||
public IEnumerable<Backtest> GetBacktestsByRequestId(Guid requestId) => throw new NotImplementedException("Not available in compute worker");
|
||||
public Task<IEnumerable<Backtest>> GetBacktestsByRequestIdAsync(Guid requestId) => throw new NotImplementedException("Not available in compute worker");
|
||||
public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc") => throw new NotImplementedException("Not available in compute worker");
|
||||
public Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByRequestIdPaginatedAsync(Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc") => throw new NotImplementedException("Not available in compute worker");
|
||||
public Task<Backtest> GetBacktestByIdForUserAsync(User user, string id) => throw new NotImplementedException("Not available in compute worker");
|
||||
public Task<bool> DeleteBacktestByUserAsync(User user, string id) => throw new NotImplementedException("Not available in compute worker");
|
||||
public Task<bool> DeleteBacktestsByIdsForUserAsync(User user, IEnumerable<string> ids) => throw new NotImplementedException("Not available in compute worker");
|
||||
public bool DeleteBacktestsByUser(User user) => throw new NotImplementedException("Not available in compute worker");
|
||||
public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, int pageSize, BacktestSortableColumn sortBy, string sortOrder = "desc", BacktestsFilter? filter = null) => throw new NotImplementedException("Not available in compute worker");
|
||||
public Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByUserPaginatedAsync(User user, int page, int pageSize, BacktestSortableColumn sortBy, string sortOrder = "desc", BacktestsFilter? filter = null) => throw new NotImplementedException("Not available in compute worker");
|
||||
public Task<bool> DeleteBacktestsByRequestIdAsync(Guid requestId) => throw new NotImplementedException("Not available in compute worker");
|
||||
public Task<int> DeleteBacktestsByFiltersAsync(User user, BacktestsFilter filter) => throw new NotImplementedException("Not available in compute worker");
|
||||
public Task InsertBundleBacktestRequestForUserAsync(User user, BundleBacktestRequest bundleRequest, bool saveAsTemplate = false) => throw new NotImplementedException("Not available in compute worker");
|
||||
public IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByUser(User user) => throw new NotImplementedException("Not available in compute worker");
|
||||
public Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByUserAsync(User user) => throw new NotImplementedException("Not available in compute worker");
|
||||
public BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, Guid id) => throw new NotImplementedException("Not available in compute worker");
|
||||
public Task<BundleBacktestRequest?> GetBundleBacktestRequestByIdForUserAsync(User user, Guid id) => throw new NotImplementedException("Not available in compute worker");
|
||||
public void UpdateBundleBacktestRequest(BundleBacktestRequest bundleRequest) => throw new NotImplementedException("Not available in compute worker");
|
||||
public Task UpdateBundleBacktestRequestAsync(BundleBacktestRequest bundleRequest) => throw new NotImplementedException("Not available in compute worker");
|
||||
public void DeleteBundleBacktestRequestByIdForUser(User user, Guid id) => throw new NotImplementedException("Not available in compute worker");
|
||||
public Task DeleteBundleBacktestRequestByIdForUserAsync(User user, Guid id) => throw new NotImplementedException("Not available in compute worker");
|
||||
public IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByStatus(BundleBacktestRequestStatus status) => throw new NotImplementedException("Not available in compute worker");
|
||||
public Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus status) => throw new NotImplementedException("Not available in compute worker");
|
||||
}
|
||||
|
||||
@@ -15,18 +15,18 @@ namespace Managing.Application.Backtests;
|
||||
/// <summary>
|
||||
/// Service for creating and managing backtest jobs in the queue
|
||||
/// </summary>
|
||||
public class BacktestJobService
|
||||
public class JobService
|
||||
{
|
||||
private readonly IBacktestJobRepository _jobRepository;
|
||||
private readonly IJobRepository _jobRepository;
|
||||
private readonly IBacktestRepository _backtestRepository;
|
||||
private readonly IKaigenService _kaigenService;
|
||||
private readonly ILogger<BacktestJobService> _logger;
|
||||
private readonly ILogger<JobService> _logger;
|
||||
|
||||
public BacktestJobService(
|
||||
IBacktestJobRepository jobRepository,
|
||||
public JobService(
|
||||
IJobRepository jobRepository,
|
||||
IBacktestRepository backtestRepository,
|
||||
IKaigenService kaigenService,
|
||||
ILogger<BacktestJobService> logger)
|
||||
ILogger<JobService> logger)
|
||||
{
|
||||
_jobRepository = jobRepository;
|
||||
_backtestRepository = backtestRepository;
|
||||
@@ -37,7 +37,7 @@ public class BacktestJobService
|
||||
/// <summary>
|
||||
/// Creates a single backtest job
|
||||
/// </summary>
|
||||
public async Task<BacktestJob> CreateJobAsync(
|
||||
public async Task<Job> CreateJobAsync(
|
||||
TradingBotConfig config,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
@@ -63,10 +63,10 @@ public class BacktestJobService
|
||||
|
||||
try
|
||||
{
|
||||
var job = new BacktestJob
|
||||
var job = new Job
|
||||
{
|
||||
UserId = user.Id,
|
||||
Status = BacktestJobStatus.Pending,
|
||||
Status = JobStatus.Pending,
|
||||
JobType = JobType.Backtest,
|
||||
Priority = priority,
|
||||
ConfigJson = JsonSerializer.Serialize(config),
|
||||
@@ -109,11 +109,11 @@ public class BacktestJobService
|
||||
/// <summary>
|
||||
/// Creates multiple backtest jobs from bundle variants
|
||||
/// </summary>
|
||||
public async Task<List<BacktestJob>> CreateBundleJobsAsync(
|
||||
public async Task<List<Job>> CreateBundleJobsAsync(
|
||||
BundleBacktestRequest bundleRequest,
|
||||
List<RunBacktestRequest> backtestRequests)
|
||||
{
|
||||
var jobs = new List<BacktestJob>();
|
||||
var jobs = new List<Job>();
|
||||
var creditRequestId = (string?)null;
|
||||
|
||||
try
|
||||
@@ -203,10 +203,10 @@ public class BacktestJobService
|
||||
UseForDynamicStopLoss = backtestRequest.Config.UseForDynamicStopLoss
|
||||
};
|
||||
|
||||
var job = new BacktestJob
|
||||
var job = new Job
|
||||
{
|
||||
UserId = bundleRequest.User.Id,
|
||||
Status = BacktestJobStatus.Pending,
|
||||
Status = JobStatus.Pending,
|
||||
JobType = JobType.Backtest,
|
||||
Priority = 0, // All bundle jobs have same priority
|
||||
ConfigJson = JsonSerializer.Serialize(backtestConfig),
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Managing.Application.Backtests
|
||||
private readonly IMessengerService _messengerService;
|
||||
private readonly IKaigenService _kaigenService;
|
||||
private readonly IHubContext<BacktestHub> _hubContext;
|
||||
private readonly BacktestJobService _jobService;
|
||||
private readonly JobService _jobService;
|
||||
|
||||
public Backtester(
|
||||
IExchangeService exchangeService,
|
||||
@@ -41,7 +41,7 @@ namespace Managing.Application.Backtests
|
||||
IKaigenService kaigenService,
|
||||
IHubContext<BacktestHub> hubContext,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
BacktestJobService jobService)
|
||||
JobService jobService)
|
||||
{
|
||||
_exchangeService = exchangeService;
|
||||
_backtestRepository = backtestRepository;
|
||||
|
||||
149
src/Managing.Application/Backtests/GeneticExecutor.cs
Normal file
149
src/Managing.Application/Backtests/GeneticExecutor.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Domain.Backtests;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Managing.Application.Backtests;
|
||||
|
||||
/// <summary>
|
||||
/// Service for executing genetic algorithm requests without Orleans dependencies.
|
||||
/// Extracted from GeneticBacktestGrain to be reusable in compute workers.
|
||||
/// </summary>
|
||||
public class GeneticExecutor
|
||||
{
|
||||
private readonly ILogger<GeneticExecutor> _logger;
|
||||
private readonly IGeneticService _geneticService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IWebhookService _webhookService;
|
||||
|
||||
public GeneticExecutor(
|
||||
ILogger<GeneticExecutor> logger,
|
||||
IGeneticService geneticService,
|
||||
IAccountService accountService,
|
||||
IWebhookService webhookService)
|
||||
{
|
||||
_logger = logger;
|
||||
_geneticService = geneticService;
|
||||
_accountService = accountService;
|
||||
_webhookService = webhookService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a genetic algorithm request.
|
||||
/// </summary>
|
||||
/// <param name="geneticRequestId">The genetic request ID to process</param>
|
||||
/// <param name="progressCallback">Optional callback for progress updates (0-100)</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>The genetic algorithm result</returns>
|
||||
public async Task<GeneticAlgorithmResult> ExecuteAsync(
|
||||
string geneticRequestId,
|
||||
Func<int, Task> progressCallback = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Load the request by status lists and filter by ID (Pending first, then Failed for retries)
|
||||
var pending = await _geneticService.GetGeneticRequestsAsync(GeneticRequestStatus.Pending);
|
||||
var failed = await _geneticService.GetGeneticRequestsAsync(GeneticRequestStatus.Failed);
|
||||
var request = pending.Concat(failed).FirstOrDefault(r => r.RequestId == geneticRequestId);
|
||||
|
||||
if (request == null)
|
||||
{
|
||||
_logger.LogWarning("[GeneticExecutor] Request {RequestId} not found among pending/failed.",
|
||||
geneticRequestId);
|
||||
throw new InvalidOperationException($"Genetic request {geneticRequestId} not found");
|
||||
}
|
||||
|
||||
_logger.LogInformation("[GeneticExecutor] Processing genetic request {RequestId} for user {UserId}",
|
||||
request.RequestId, request.User.Id);
|
||||
|
||||
// Mark running
|
||||
request.Status = GeneticRequestStatus.Running;
|
||||
await _geneticService.UpdateGeneticRequestAsync(request);
|
||||
|
||||
// Load user accounts if not already loaded
|
||||
if (request.User.Accounts == null || !request.User.Accounts.Any())
|
||||
{
|
||||
request.User.Accounts = (await _accountService.GetAccountsByUserAsync(request.User)).ToList();
|
||||
}
|
||||
|
||||
// Create progress wrapper if callback provided
|
||||
Func<int, Task> wrappedProgressCallback = null;
|
||||
if (progressCallback != null)
|
||||
{
|
||||
wrappedProgressCallback = async (percentage) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await progressCallback(percentage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error in progress callback for genetic request {RequestId}", geneticRequestId);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Run GA
|
||||
var result = await _geneticService.RunGeneticAlgorithm(request, cancellationToken);
|
||||
|
||||
// Update final state
|
||||
request.Status = GeneticRequestStatus.Completed;
|
||||
request.CompletedAt = DateTime.UtcNow;
|
||||
request.BestFitness = result.BestFitness;
|
||||
request.BestIndividual = result.BestIndividual;
|
||||
request.ProgressInfo = result.ProgressInfo;
|
||||
await _geneticService.UpdateGeneticRequestAsync(request);
|
||||
|
||||
_logger.LogInformation("[GeneticExecutor] Completed genetic request {RequestId}. Best Fitness: {BestFitness}",
|
||||
request.RequestId, result.BestFitness);
|
||||
|
||||
// Send webhook notification if user has telegram channel
|
||||
if (!string.IsNullOrEmpty(request.User?.TelegramChannel))
|
||||
{
|
||||
var message = $"✅ Genetic algorithm optimization completed for {request.Ticker} on {request.Timeframe}. " +
|
||||
$"Request ID: {request.RequestId}. " +
|
||||
$"Best Fitness: {result.BestFitness:F4}. " +
|
||||
$"Generations: {request.Generations}, Population: {request.PopulationSize}.";
|
||||
|
||||
await _webhookService.SendMessage(message, request.User.TelegramChannel);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[GeneticExecutor] Error processing genetic request {RequestId}", geneticRequestId);
|
||||
|
||||
// Try to mark as failed
|
||||
try
|
||||
{
|
||||
var running = await _geneticService.GetGeneticRequestsAsync(GeneticRequestStatus.Running);
|
||||
var req = running.FirstOrDefault(r => r.RequestId == geneticRequestId);
|
||||
if (req != null)
|
||||
{
|
||||
req.Status = GeneticRequestStatus.Failed;
|
||||
req.ErrorMessage = ex.Message;
|
||||
req.CompletedAt = DateTime.UtcNow;
|
||||
await _geneticService.UpdateGeneticRequestAsync(req);
|
||||
|
||||
// Send webhook notification for failed genetic request
|
||||
if (!string.IsNullOrEmpty(req.User?.TelegramChannel))
|
||||
{
|
||||
var message = $"❌ Genetic algorithm optimization failed for {req.Ticker} on {req.Timeframe}. " +
|
||||
$"Request ID: {req.RequestId}. " +
|
||||
$"Error: {ex.Message}";
|
||||
|
||||
await _webhookService.SendMessage(message, req.User.TelegramChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception updateEx)
|
||||
{
|
||||
_logger.LogError(updateEx, "[GeneticExecutor] Failed to update request status to Failed for {RequestId}", geneticRequestId);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@ public class GeneticService : IGeneticService
|
||||
CrossoverProbability = 0.75f, // Fixed crossover rate as in frontend
|
||||
TaskExecutor = new ParallelTaskExecutor
|
||||
{
|
||||
MinThreads = 4,
|
||||
MinThreads = 2,
|
||||
MaxThreads = Environment.ProcessorCount
|
||||
}
|
||||
};
|
||||
|
||||
@@ -77,10 +77,10 @@ public class BacktestComputeWorker : BackgroundService
|
||||
try
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var jobRepository = scope.ServiceProvider.GetRequiredService<IBacktestJobRepository>();
|
||||
var jobRepository = scope.ServiceProvider.GetRequiredService<IJobRepository>();
|
||||
|
||||
// Try to claim a job
|
||||
var job = await jobRepository.ClaimNextJobAsync(_options.WorkerId);
|
||||
// Try to claim a backtest job (exclude genetic jobs)
|
||||
var job = await jobRepository.ClaimNextJobAsync(_options.WorkerId, JobType.Backtest);
|
||||
|
||||
if (job == null)
|
||||
{
|
||||
@@ -114,11 +114,11 @@ public class BacktestComputeWorker : BackgroundService
|
||||
}
|
||||
|
||||
private async Task ProcessJobAsync(
|
||||
BacktestJob job,
|
||||
Job job,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var jobRepository = scope.ServiceProvider.GetRequiredService<IBacktestJobRepository>();
|
||||
var jobRepository = scope.ServiceProvider.GetRequiredService<IJobRepository>();
|
||||
var executor = scope.ServiceProvider.GetRequiredService<BacktestExecutor>();
|
||||
var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
|
||||
var exchangeService = scope.ServiceProvider.GetRequiredService<IExchangeService>();
|
||||
@@ -184,7 +184,7 @@ public class BacktestComputeWorker : BackgroundService
|
||||
progressCallback: progressCallback);
|
||||
|
||||
// Update job with result
|
||||
job.Status = BacktestJobStatus.Completed;
|
||||
job.Status = JobStatus.Completed;
|
||||
job.ProgressPercentage = 100;
|
||||
job.ResultJson = JsonSerializer.Serialize(result);
|
||||
job.CompletedAt = DateTime.UtcNow;
|
||||
@@ -207,24 +207,7 @@ public class BacktestComputeWorker : BackgroundService
|
||||
_logger.LogError(ex, "Error processing backtest job {JobId}", job.Id);
|
||||
SentrySdk.CaptureException(ex);
|
||||
|
||||
// Update job status to failed
|
||||
try
|
||||
{
|
||||
job.Status = BacktestJobStatus.Failed;
|
||||
job.ErrorMessage = ex.Message;
|
||||
job.CompletedAt = DateTime.UtcNow;
|
||||
await jobRepository.UpdateAsync(job);
|
||||
|
||||
// Update bundle request if this is part of a bundle
|
||||
if (job.BundleRequestId.HasValue)
|
||||
{
|
||||
await UpdateBundleRequestProgress(job.BundleRequestId.Value, scope.ServiceProvider);
|
||||
}
|
||||
}
|
||||
catch (Exception updateEx)
|
||||
{
|
||||
_logger.LogError(updateEx, "Error updating job {JobId} status to failed", job.Id);
|
||||
}
|
||||
await HandleJobFailure(job, ex, jobRepository, scope.ServiceProvider);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,14 +216,15 @@ public class BacktestComputeWorker : BackgroundService
|
||||
try
|
||||
{
|
||||
var backtestRepository = serviceProvider.GetRequiredService<IBacktestRepository>();
|
||||
var jobRepository = serviceProvider.GetRequiredService<IBacktestJobRepository>();
|
||||
var jobRepository = serviceProvider.GetRequiredService<IJobRepository>();
|
||||
var userService = serviceProvider.GetRequiredService<IUserService>();
|
||||
var webhookService = serviceProvider.GetRequiredService<IWebhookService>();
|
||||
|
||||
// Get all jobs for this bundle
|
||||
var jobs = await jobRepository.GetByBundleRequestIdAsync(bundleRequestId);
|
||||
var completedJobs = jobs.Count(j => j.Status == BacktestJobStatus.Completed);
|
||||
var failedJobs = jobs.Count(j => j.Status == BacktestJobStatus.Failed);
|
||||
var runningJobs = jobs.Count(j => j.Status == BacktestJobStatus.Running);
|
||||
var completedJobs = jobs.Count(j => j.Status == JobStatus.Completed);
|
||||
var failedJobs = jobs.Count(j => j.Status == JobStatus.Failed);
|
||||
var runningJobs = jobs.Count(j => j.Status == JobStatus.Running);
|
||||
var totalJobs = jobs.Count();
|
||||
|
||||
if (totalJobs == 0)
|
||||
@@ -265,6 +249,8 @@ public class BacktestComputeWorker : BackgroundService
|
||||
return;
|
||||
}
|
||||
|
||||
var previousStatus = bundleRequest.Status;
|
||||
|
||||
// Update bundle request progress
|
||||
bundleRequest.CompletedBacktests = completedJobs;
|
||||
bundleRequest.FailedBacktests = failedJobs;
|
||||
@@ -298,7 +284,7 @@ public class BacktestComputeWorker : BackgroundService
|
||||
|
||||
// Update results list from completed jobs
|
||||
var completedJobResults = jobs
|
||||
.Where(j => j.Status == BacktestJobStatus.Completed && !string.IsNullOrEmpty(j.ResultJson))
|
||||
.Where(j => j.Status == JobStatus.Completed && !string.IsNullOrEmpty(j.ResultJson))
|
||||
.Select(j =>
|
||||
{
|
||||
try
|
||||
@@ -318,6 +304,28 @@ public class BacktestComputeWorker : BackgroundService
|
||||
|
||||
await backtestRepository.UpdateBundleBacktestRequestAsync(bundleRequest);
|
||||
|
||||
// Send webhook notification if bundle request just completed
|
||||
if (previousStatus != BundleBacktestRequestStatus.Completed &&
|
||||
bundleRequest.Status == BundleBacktestRequestStatus.Completed &&
|
||||
!string.IsNullOrEmpty(user.TelegramChannel))
|
||||
{
|
||||
var message = $"✅ Bundle backtest '{bundleRequest.Name}' (ID: {bundleRequest.RequestId}) completed successfully. " +
|
||||
$"Completed: {completedJobs}/{totalJobs} backtests" +
|
||||
(failedJobs > 0 ? $", Failed: {failedJobs}" : "") +
|
||||
$". Results: {completedJobResults.Count} backtest(s) generated.";
|
||||
|
||||
await webhookService.SendMessage(message, user.TelegramChannel);
|
||||
}
|
||||
else if (previousStatus != BundleBacktestRequestStatus.Failed &&
|
||||
bundleRequest.Status == BundleBacktestRequestStatus.Failed &&
|
||||
!string.IsNullOrEmpty(user.TelegramChannel))
|
||||
{
|
||||
var message = $"❌ Bundle backtest '{bundleRequest.Name}' (ID: {bundleRequest.RequestId}) failed. " +
|
||||
$"All {totalJobs} backtests failed. Error: {bundleRequest.ErrorMessage}";
|
||||
|
||||
await webhookService.SendMessage(message, user.TelegramChannel);
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Updated bundle request {BundleRequestId} progress: {Completed}/{Total} completed, {Failed} failed, {Running} running",
|
||||
bundleRequestId, completedJobs, totalJobs, failedJobs, runningJobs);
|
||||
@@ -337,13 +345,58 @@ public class BacktestComputeWorker : BackgroundService
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken); // Check every minute
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var jobRepository = scope.ServiceProvider.GetRequiredService<IBacktestJobRepository>();
|
||||
var jobRepository = scope.ServiceProvider.GetRequiredService<IJobRepository>();
|
||||
|
||||
var resetCount = await jobRepository.ResetStaleJobsAsync(_options.StaleJobTimeoutMinutes);
|
||||
// Get stale jobs for this worker
|
||||
var runningJobs = await jobRepository.GetRunningJobsByWorkerIdAsync(_options.WorkerId);
|
||||
var staleJobs = runningJobs
|
||||
.Where(j => j.JobType == JobType.Backtest &&
|
||||
(j.LastHeartbeat == null ||
|
||||
j.LastHeartbeat < DateTime.UtcNow.AddMinutes(-_options.StaleJobTimeoutMinutes)))
|
||||
.ToList();
|
||||
|
||||
if (resetCount > 0)
|
||||
foreach (var job in staleJobs)
|
||||
{
|
||||
_logger.LogInformation("Reset {Count} stale backtest jobs back to Pending status", resetCount);
|
||||
// If it's stale but retryable, reset to pending with retry count
|
||||
if (job.RetryCount < job.MaxRetries)
|
||||
{
|
||||
job.Status = JobStatus.Pending;
|
||||
job.RetryCount++;
|
||||
var backoffMinutes = Math.Min(Math.Pow(2, job.RetryCount), _options.MaxRetryDelayMinutes);
|
||||
job.RetryAfter = DateTime.UtcNow.AddMinutes(backoffMinutes);
|
||||
job.ErrorMessage = $"Worker timeout - retry {job.RetryCount}/{job.MaxRetries}";
|
||||
job.FailureCategory = FailureCategory.SystemError;
|
||||
_logger.LogWarning(
|
||||
"Stale job {JobId} will be retried (attempt {RetryCount}/{MaxRetries}) after {RetryAfter}",
|
||||
job.Id, job.RetryCount, job.MaxRetries, job.RetryAfter);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Exceeded retries - mark as failed
|
||||
job.Status = JobStatus.Failed;
|
||||
job.ErrorMessage = "Worker timeout - exceeded max retries";
|
||||
job.FailureCategory = FailureCategory.SystemError;
|
||||
job.IsRetryable = false;
|
||||
job.CompletedAt = DateTime.UtcNow;
|
||||
|
||||
// Notify permanent failure
|
||||
await NotifyPermanentFailure(job, new TimeoutException("Worker timeout"), scope.ServiceProvider);
|
||||
|
||||
// Update bundle request if this is part of a bundle
|
||||
if (job.BundleRequestId.HasValue)
|
||||
{
|
||||
await UpdateBundleRequestProgress(job.BundleRequestId.Value, scope.ServiceProvider);
|
||||
}
|
||||
}
|
||||
|
||||
job.AssignedWorkerId = null;
|
||||
job.LastHeartbeat = null;
|
||||
await jobRepository.UpdateAsync(job);
|
||||
}
|
||||
|
||||
if (staleJobs.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Processed {Count} stale backtest jobs", staleJobs.Count);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -362,7 +415,7 @@ public class BacktestComputeWorker : BackgroundService
|
||||
await Task.Delay(TimeSpan.FromSeconds(_options.HeartbeatIntervalSeconds), cancellationToken);
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var jobRepository = scope.ServiceProvider.GetRequiredService<IBacktestJobRepository>();
|
||||
var jobRepository = scope.ServiceProvider.GetRequiredService<IJobRepository>();
|
||||
|
||||
// Update heartbeat for all jobs assigned to this worker
|
||||
var runningJobs = await jobRepository.GetRunningJobsByWorkerIdAsync(_options.WorkerId);
|
||||
@@ -380,6 +433,118 @@ public class BacktestComputeWorker : BackgroundService
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleJobFailure(
|
||||
Job job,
|
||||
Exception ex,
|
||||
IJobRepository jobRepository,
|
||||
IServiceProvider serviceProvider)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Categorize the failure
|
||||
var failureCategory = CategorizeFailure(ex);
|
||||
var isRetryable = IsRetryableFailure(ex, failureCategory);
|
||||
|
||||
// Check if we should retry
|
||||
if (isRetryable && job.RetryCount < job.MaxRetries)
|
||||
{
|
||||
// Calculate exponential backoff: 2^retryCount minutes, capped at MaxRetryDelayMinutes
|
||||
var backoffMinutes = Math.Min(Math.Pow(2, job.RetryCount), _options.MaxRetryDelayMinutes);
|
||||
job.RetryAfter = DateTime.UtcNow.AddMinutes(backoffMinutes);
|
||||
job.RetryCount++;
|
||||
job.Status = JobStatus.Pending; // Reset to pending for retry
|
||||
job.AssignedWorkerId = null; // Allow any worker to pick it up
|
||||
job.ErrorMessage = $"Retry {job.RetryCount}/{job.MaxRetries}: {ex.Message}";
|
||||
job.FailureCategory = failureCategory;
|
||||
job.IsRetryable = true;
|
||||
|
||||
_logger.LogWarning(
|
||||
"Job {JobId} will be retried (attempt {RetryCount}/{MaxRetries}) after {RetryAfter}. Error: {Error}",
|
||||
job.Id, job.RetryCount, job.MaxRetries, job.RetryAfter, ex.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Permanent failure - mark as failed
|
||||
job.Status = JobStatus.Failed;
|
||||
job.ErrorMessage = ex.Message;
|
||||
job.FailureCategory = failureCategory;
|
||||
job.IsRetryable = false;
|
||||
job.CompletedAt = DateTime.UtcNow;
|
||||
|
||||
_logger.LogError(
|
||||
"Job {JobId} failed permanently after {RetryCount} retries. Error: {Error}",
|
||||
job.Id, job.RetryCount, ex.Message);
|
||||
|
||||
// Send notification for permanent failure
|
||||
await NotifyPermanentFailure(job, ex, serviceProvider);
|
||||
|
||||
// Update bundle request if this is part of a bundle
|
||||
if (job.BundleRequestId.HasValue)
|
||||
{
|
||||
await UpdateBundleRequestProgress(job.BundleRequestId.Value, serviceProvider);
|
||||
}
|
||||
}
|
||||
|
||||
job.LastHeartbeat = DateTime.UtcNow;
|
||||
await jobRepository.UpdateAsync(job);
|
||||
}
|
||||
catch (Exception updateEx)
|
||||
{
|
||||
_logger.LogError(updateEx, "Failed to update job {JobId} status after failure", job.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private FailureCategory CategorizeFailure(Exception ex)
|
||||
{
|
||||
return ex switch
|
||||
{
|
||||
TimeoutException => FailureCategory.Transient,
|
||||
TaskCanceledException => FailureCategory.Transient,
|
||||
HttpRequestException => FailureCategory.Transient,
|
||||
InvalidOperationException when ex.Message.Contains("candles") || ex.Message.Contains("No candles") => FailureCategory.DataError,
|
||||
InvalidOperationException when ex.Message.Contains("User") || ex.Message.Contains("not found") => FailureCategory.UserError,
|
||||
OutOfMemoryException => FailureCategory.SystemError,
|
||||
_ => FailureCategory.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
private bool IsRetryableFailure(Exception ex, FailureCategory category)
|
||||
{
|
||||
// Don't retry user errors or data errors (missing candles, invalid config)
|
||||
if (category == FailureCategory.UserError || category == FailureCategory.DataError)
|
||||
return false;
|
||||
|
||||
// Retry transient and system errors
|
||||
return category == FailureCategory.Transient || category == FailureCategory.SystemError;
|
||||
}
|
||||
|
||||
private async Task NotifyPermanentFailure(
|
||||
Job job,
|
||||
Exception ex,
|
||||
IServiceProvider serviceProvider)
|
||||
{
|
||||
try
|
||||
{
|
||||
var webhookService = serviceProvider.GetRequiredService<IWebhookService>();
|
||||
const string alertsChannel = "2676086723";
|
||||
|
||||
var jobTypeName = job.JobType == JobType.Genetic ? "Genetic" : "Backtest";
|
||||
var message = $"🚨 **{jobTypeName} Job Failed Permanently**\n" +
|
||||
$"Job ID: `{job.Id}`\n" +
|
||||
$"User ID: {job.UserId}\n" +
|
||||
$"Retry Attempts: {job.RetryCount}/{job.MaxRetries}\n" +
|
||||
$"Failure Category: {job.FailureCategory}\n" +
|
||||
$"Error: {ex.Message}\n" +
|
||||
$"Time: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC";
|
||||
|
||||
await webhookService.SendMessage(message, alertsChannel);
|
||||
}
|
||||
catch (Exception notifyEx)
|
||||
{
|
||||
_logger.LogError(notifyEx, "Failed to send permanent failure notification for job {JobId}", job.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
_semaphore?.Dispose();
|
||||
@@ -418,5 +583,15 @@ public class BacktestComputeWorkerOptions
|
||||
/// Timeout in minutes for considering a job stale
|
||||
/// </summary>
|
||||
public int StaleJobTimeoutMinutes { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Default maximum retry attempts for failed jobs
|
||||
/// </summary>
|
||||
public int DefaultMaxRetries { get; set; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum retry delay in minutes (cap for exponential backoff)
|
||||
/// </summary>
|
||||
public int MaxRetryDelayMinutes { get; set; } = 60;
|
||||
}
|
||||
|
||||
|
||||
429
src/Managing.Application/Workers/GeneticComputeWorker.cs
Normal file
429
src/Managing.Application/Workers/GeneticComputeWorker.cs
Normal file
@@ -0,0 +1,429 @@
|
||||
using System.Text.Json;
|
||||
using Managing.Application.Abstractions.Repositories;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Backtests;
|
||||
using Managing.Domain.Backtests;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Workers;
|
||||
|
||||
/// <summary>
|
||||
/// Background worker that processes genetic algorithm jobs from the queue.
|
||||
/// Polls for pending genetic jobs, claims them using advisory locks, and processes them.
|
||||
/// </summary>
|
||||
public class GeneticComputeWorker : BackgroundService
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ILogger<GeneticComputeWorker> _logger;
|
||||
private readonly GeneticComputeWorkerOptions _options;
|
||||
private readonly SemaphoreSlim _semaphore;
|
||||
|
||||
public GeneticComputeWorker(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<GeneticComputeWorker> logger,
|
||||
IOptions<GeneticComputeWorkerOptions> options)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
_options = options.Value;
|
||||
_semaphore = new SemaphoreSlim(_options.MaxConcurrentGenetics, _options.MaxConcurrentGenetics);
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"GeneticComputeWorker starting. WorkerId: {WorkerId}, MaxConcurrent: {MaxConcurrent}, PollInterval: {PollInterval}s",
|
||||
_options.WorkerId, _options.MaxConcurrentGenetics, _options.JobPollIntervalSeconds);
|
||||
|
||||
// Background task for stale job recovery
|
||||
var staleJobRecoveryTask = Task.Run(() => StaleJobRecoveryLoop(stoppingToken), stoppingToken);
|
||||
|
||||
// Background task for heartbeat updates
|
||||
var heartbeatTask = Task.Run(() => HeartbeatLoop(stoppingToken), stoppingToken);
|
||||
|
||||
// Main job processing loop
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessJobsAsync(stoppingToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in GeneticComputeWorker main loop");
|
||||
SentrySdk.CaptureException(ex);
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(_options.JobPollIntervalSeconds), stoppingToken);
|
||||
}
|
||||
|
||||
_logger.LogInformation("GeneticComputeWorker stopping");
|
||||
}
|
||||
|
||||
private async Task ProcessJobsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Check if we have capacity
|
||||
if (!await _semaphore.WaitAsync(0, cancellationToken))
|
||||
{
|
||||
// At capacity, skip this iteration
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var jobRepository = scope.ServiceProvider.GetRequiredService<IJobRepository>();
|
||||
|
||||
// Try to claim a genetic job
|
||||
var job = await jobRepository.ClaimNextJobAsync(_options.WorkerId, JobType.Genetic);
|
||||
|
||||
if (job == null)
|
||||
{
|
||||
// No jobs available, release semaphore
|
||||
_semaphore.Release();
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Claimed genetic job {JobId} for worker {WorkerId}", job.Id, _options.WorkerId);
|
||||
|
||||
// Process the job asynchronously (don't await, let it run in background)
|
||||
// Create a new scope for the job processing to ensure proper lifetime management
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessJobAsync(job, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error claiming or processing genetic job");
|
||||
_semaphore.Release();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessJobAsync(
|
||||
Job job,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var jobRepository = scope.ServiceProvider.GetRequiredService<IJobRepository>();
|
||||
var geneticExecutor = scope.ServiceProvider.GetRequiredService<GeneticExecutor>();
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Processing genetic job {JobId} (GeneticRequestId: {GeneticRequestId}, UserId: {UserId})",
|
||||
job.Id, job.GeneticRequestId, job.UserId);
|
||||
|
||||
if (string.IsNullOrEmpty(job.GeneticRequestId))
|
||||
{
|
||||
throw new InvalidOperationException("GeneticRequestId is required for genetic jobs");
|
||||
}
|
||||
|
||||
// Progress callback to update job progress
|
||||
Func<int, Task> progressCallback = async (percentage) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
job.ProgressPercentage = percentage;
|
||||
job.LastHeartbeat = DateTime.UtcNow;
|
||||
await jobRepository.UpdateAsync(job);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error updating job progress for job {JobId}", job.Id);
|
||||
}
|
||||
};
|
||||
|
||||
// Execute the genetic algorithm
|
||||
var result = await geneticExecutor.ExecuteAsync(
|
||||
job.GeneticRequestId,
|
||||
progressCallback,
|
||||
cancellationToken);
|
||||
|
||||
// Update job with result
|
||||
job.Status = JobStatus.Completed;
|
||||
job.ProgressPercentage = 100;
|
||||
job.ResultJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
BestFitness = result.BestFitness,
|
||||
BestIndividual = result.BestIndividual,
|
||||
ProgressInfo = result.ProgressInfo
|
||||
});
|
||||
job.CompletedAt = DateTime.UtcNow;
|
||||
job.LastHeartbeat = DateTime.UtcNow;
|
||||
|
||||
await jobRepository.UpdateAsync(job);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Completed genetic job {JobId}. Best Fitness: {BestFitness}",
|
||||
job.Id, result.BestFitness);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing genetic job {JobId}", job.Id);
|
||||
SentrySdk.CaptureException(ex);
|
||||
|
||||
await HandleJobFailure(job, ex, jobRepository, scope.ServiceProvider);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StaleJobRecoveryLoop(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken); // Check every minute
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var jobRepository = scope.ServiceProvider.GetRequiredService<IJobRepository>();
|
||||
|
||||
// Reset stale genetic jobs only
|
||||
var runningJobs = await jobRepository.GetRunningJobsByWorkerIdAsync(_options.WorkerId);
|
||||
var staleJobs = runningJobs
|
||||
.Where(j => j.JobType == JobType.Genetic &&
|
||||
(j.LastHeartbeat == null ||
|
||||
j.LastHeartbeat < DateTime.UtcNow.AddMinutes(-_options.StaleJobTimeoutMinutes)))
|
||||
.ToList();
|
||||
|
||||
foreach (var job in staleJobs)
|
||||
{
|
||||
// If it's stale but retryable, reset to pending with retry count
|
||||
if (job.RetryCount < job.MaxRetries)
|
||||
{
|
||||
job.Status = JobStatus.Pending;
|
||||
job.RetryCount++;
|
||||
var backoffMinutes = Math.Min(Math.Pow(2, job.RetryCount), _options.MaxRetryDelayMinutes);
|
||||
job.RetryAfter = DateTime.UtcNow.AddMinutes(backoffMinutes);
|
||||
job.ErrorMessage = $"Worker timeout - retry {job.RetryCount}/{job.MaxRetries}";
|
||||
job.FailureCategory = FailureCategory.SystemError;
|
||||
_logger.LogWarning(
|
||||
"Stale job {JobId} will be retried (attempt {RetryCount}/{MaxRetries}) after {RetryAfter}",
|
||||
job.Id, job.RetryCount, job.MaxRetries, job.RetryAfter);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Exceeded retries - mark as failed
|
||||
job.Status = JobStatus.Failed;
|
||||
job.ErrorMessage = "Worker timeout - exceeded max retries";
|
||||
job.FailureCategory = FailureCategory.SystemError;
|
||||
job.IsRetryable = false;
|
||||
job.CompletedAt = DateTime.UtcNow;
|
||||
|
||||
// Notify permanent failure
|
||||
await NotifyPermanentFailure(job, new TimeoutException("Worker timeout"), scope.ServiceProvider);
|
||||
}
|
||||
|
||||
job.AssignedWorkerId = null;
|
||||
job.LastHeartbeat = null;
|
||||
await jobRepository.UpdateAsync(job);
|
||||
}
|
||||
|
||||
if (staleJobs.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Processed {Count} stale genetic jobs", staleJobs.Count);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in stale job recovery loop");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HeartbeatLoop(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(_options.HeartbeatIntervalSeconds), cancellationToken);
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var jobRepository = scope.ServiceProvider.GetRequiredService<IJobRepository>();
|
||||
|
||||
// Update heartbeat for all genetic jobs assigned to this worker
|
||||
var runningJobs = await jobRepository.GetRunningJobsByWorkerIdAsync(_options.WorkerId);
|
||||
var geneticJobs = runningJobs.Where(j => j.JobType == JobType.Genetic);
|
||||
|
||||
foreach (var job in geneticJobs)
|
||||
{
|
||||
job.LastHeartbeat = DateTime.UtcNow;
|
||||
await jobRepository.UpdateAsync(job);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in heartbeat loop");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleJobFailure(
|
||||
Job job,
|
||||
Exception ex,
|
||||
IJobRepository jobRepository,
|
||||
IServiceProvider serviceProvider)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Categorize the failure
|
||||
var failureCategory = CategorizeFailure(ex);
|
||||
var isRetryable = IsRetryableFailure(ex, failureCategory);
|
||||
|
||||
// Check if we should retry
|
||||
if (isRetryable && job.RetryCount < job.MaxRetries)
|
||||
{
|
||||
// Calculate exponential backoff: 2^retryCount minutes, capped at MaxRetryDelayMinutes
|
||||
var backoffMinutes = Math.Min(Math.Pow(2, job.RetryCount), _options.MaxRetryDelayMinutes);
|
||||
job.RetryAfter = DateTime.UtcNow.AddMinutes(backoffMinutes);
|
||||
job.RetryCount++;
|
||||
job.Status = JobStatus.Pending; // Reset to pending for retry
|
||||
job.AssignedWorkerId = null; // Allow any worker to pick it up
|
||||
job.ErrorMessage = $"Retry {job.RetryCount}/{job.MaxRetries}: {ex.Message}";
|
||||
job.FailureCategory = failureCategory;
|
||||
job.IsRetryable = true;
|
||||
|
||||
_logger.LogWarning(
|
||||
"Job {JobId} will be retried (attempt {RetryCount}/{MaxRetries}) after {RetryAfter}. Error: {Error}",
|
||||
job.Id, job.RetryCount, job.MaxRetries, job.RetryAfter, ex.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Permanent failure - mark as failed
|
||||
job.Status = JobStatus.Failed;
|
||||
job.ErrorMessage = ex.Message;
|
||||
job.FailureCategory = failureCategory;
|
||||
job.IsRetryable = false;
|
||||
job.CompletedAt = DateTime.UtcNow;
|
||||
|
||||
_logger.LogError(
|
||||
"Job {JobId} failed permanently after {RetryCount} retries. Error: {Error}",
|
||||
job.Id, job.RetryCount, ex.Message);
|
||||
|
||||
// Send notification for permanent failure
|
||||
await NotifyPermanentFailure(job, ex, serviceProvider);
|
||||
}
|
||||
|
||||
job.LastHeartbeat = DateTime.UtcNow;
|
||||
await jobRepository.UpdateAsync(job);
|
||||
}
|
||||
catch (Exception updateEx)
|
||||
{
|
||||
_logger.LogError(updateEx, "Failed to update job {JobId} status after failure", job.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private FailureCategory CategorizeFailure(Exception ex)
|
||||
{
|
||||
return ex switch
|
||||
{
|
||||
TimeoutException => FailureCategory.Transient,
|
||||
TaskCanceledException => FailureCategory.Transient,
|
||||
HttpRequestException => FailureCategory.Transient,
|
||||
InvalidOperationException when ex.Message.Contains("candles") || ex.Message.Contains("data") => FailureCategory.DataError,
|
||||
InvalidOperationException when ex.Message.Contains("User") || ex.Message.Contains("not found") => FailureCategory.UserError,
|
||||
OutOfMemoryException => FailureCategory.SystemError,
|
||||
_ => FailureCategory.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
private bool IsRetryableFailure(Exception ex, FailureCategory category)
|
||||
{
|
||||
// Don't retry user errors or data errors (missing candles, invalid config)
|
||||
if (category == FailureCategory.UserError || category == FailureCategory.DataError)
|
||||
return false;
|
||||
|
||||
// Retry transient and system errors
|
||||
return category == FailureCategory.Transient || category == FailureCategory.SystemError;
|
||||
}
|
||||
|
||||
private async Task NotifyPermanentFailure(
|
||||
Job job,
|
||||
Exception ex,
|
||||
IServiceProvider serviceProvider)
|
||||
{
|
||||
try
|
||||
{
|
||||
var webhookService = serviceProvider.GetRequiredService<IWebhookService>();
|
||||
const string alertsChannel = "2676086723";
|
||||
|
||||
var jobTypeName = job.JobType == JobType.Genetic ? "Genetic" : "Backtest";
|
||||
var message = $"🚨 **{jobTypeName} Job Failed Permanently**\n" +
|
||||
$"Job ID: `{job.Id}`\n" +
|
||||
$"User ID: {job.UserId}\n" +
|
||||
$"Retry Attempts: {job.RetryCount}/{job.MaxRetries}\n" +
|
||||
$"Failure Category: {job.FailureCategory}\n" +
|
||||
$"Error: {ex.Message}\n" +
|
||||
$"Time: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC";
|
||||
|
||||
await webhookService.SendMessage(message, alertsChannel);
|
||||
}
|
||||
catch (Exception notifyEx)
|
||||
{
|
||||
_logger.LogError(notifyEx, "Failed to send permanent failure notification for job {JobId}", job.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
_semaphore?.Dispose();
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for GeneticComputeWorker
|
||||
/// </summary>
|
||||
public class GeneticComputeWorkerOptions
|
||||
{
|
||||
public const string SectionName = "GeneticComputeWorker";
|
||||
|
||||
/// <summary>
|
||||
/// Unique identifier for this worker instance
|
||||
/// </summary>
|
||||
public string WorkerId { get; set; } = Environment.MachineName + "-genetic";
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of concurrent genetic algorithm jobs to process
|
||||
/// </summary>
|
||||
public int MaxConcurrentGenetics { get; set; } = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Interval in seconds between job polling attempts
|
||||
/// </summary>
|
||||
public int JobPollIntervalSeconds { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Interval in seconds between heartbeat updates
|
||||
/// </summary>
|
||||
public int HeartbeatIntervalSeconds { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Timeout in minutes for considering a job stale
|
||||
/// </summary>
|
||||
public int StaleJobTimeoutMinutes { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Default maximum retry attempts for failed jobs
|
||||
/// </summary>
|
||||
public int DefaultMaxRetries { get; set; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum retry delay in minutes (cap for exponential backoff)
|
||||
/// </summary>
|
||||
public int MaxRetryDelayMinutes { get; set; } = 60;
|
||||
}
|
||||
|
||||
@@ -400,7 +400,7 @@ public static class ApiBootstrap
|
||||
|
||||
// Processors
|
||||
services.AddTransient<IBacktester, Backtester>();
|
||||
services.AddTransient<BacktestJobService>();
|
||||
services.AddTransient<JobService>();
|
||||
services.AddTransient<IExchangeProcessor, EvmProcessor>();
|
||||
|
||||
services.AddTransient<ITradaoService, TradaoService>();
|
||||
@@ -443,7 +443,7 @@ public static class ApiBootstrap
|
||||
|
||||
services.AddTransient<IAccountRepository, PostgreSqlAccountRepository>();
|
||||
services.AddTransient<IBacktestRepository, PostgreSqlBacktestRepository>();
|
||||
services.AddTransient<IBacktestJobRepository, PostgreSqlJobRepository>();
|
||||
services.AddTransient<IJobRepository, PostgreSqlJobRepository>();
|
||||
services.AddTransient<IGeneticRepository, PostgreSqlGeneticRepository>();
|
||||
services.AddTransient<ITradingRepository, PostgreSqlTradingRepository>();
|
||||
services.AddTransient<ISettingsRepository, PostgreSqlSettingsRepository>();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Managing.Application;
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Repositories;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
@@ -57,7 +58,6 @@ public static class ComputeBootstrap
|
||||
// Services not needed for compute worker (depend on IBacktester/Orleans)
|
||||
// services.AddScoped<IStatisticService, StatisticService>(); // Requires IBacktester
|
||||
// services.AddScoped<ISettingsService, SettingsService>(); // Requires IBacktester
|
||||
// services.AddScoped<IGeneticService, GeneticService>(); // Requires IBacktester
|
||||
// services.AddScoped<IAgentService, AgentService>(); // May require Orleans
|
||||
// services.AddScoped<IBotService, BotService>(); // May require Orleans
|
||||
// services.AddScoped<IWorkerService, WorkerService>(); // May require Orleans
|
||||
@@ -66,6 +66,13 @@ public static class ComputeBootstrap
|
||||
// Processors
|
||||
// Note: IBacktester not needed for compute worker - BacktestExecutor is used directly
|
||||
services.AddTransient<BacktestExecutor>();
|
||||
services.AddTransient<GeneticExecutor>();
|
||||
|
||||
// Adapter to make BacktestExecutor implement IBacktester (needed for GeneticService)
|
||||
services.AddTransient<IBacktester, BacktestExecutorAdapter>();
|
||||
|
||||
// Genetic service (needed for GeneticExecutor)
|
||||
services.AddScoped<IGeneticService, GeneticService>();
|
||||
services.AddTransient<IExchangeProcessor, EvmProcessor>();
|
||||
|
||||
services.AddTransient<ITradaoService, TradaoService>();
|
||||
@@ -120,7 +127,7 @@ public static class ComputeBootstrap
|
||||
// PostgreSql Repositories
|
||||
services.AddTransient<IAccountRepository, PostgreSqlAccountRepository>();
|
||||
services.AddTransient<IBacktestRepository, PostgreSqlBacktestRepository>();
|
||||
services.AddTransient<IBacktestJobRepository, PostgreSqlJobRepository>();
|
||||
services.AddTransient<IJobRepository, PostgreSqlJobRepository>();
|
||||
services.AddTransient<IGeneticRepository, PostgreSqlGeneticRepository>();
|
||||
services.AddTransient<ITradingRepository, PostgreSqlTradingRepository>();
|
||||
services.AddTransient<ISettingsRepository, PostgreSqlSettingsRepository>();
|
||||
|
||||
@@ -564,6 +564,11 @@ public static class Enums
|
||||
/// <summary>
|
||||
/// Genetic algorithm backtest job
|
||||
/// </summary>
|
||||
GeneticBacktest
|
||||
GeneticBacktest,
|
||||
|
||||
/// <summary>
|
||||
/// Genetic algorithm request processing job
|
||||
/// </summary>
|
||||
Genetic
|
||||
}
|
||||
}
|
||||
@@ -5,16 +5,16 @@ using static Managing.Common.Enums;
|
||||
namespace Managing.Domain.Backtests;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single backtest job in the queue system.
|
||||
/// Can be a standalone backtest or part of a bundle backtest request.
|
||||
/// Represents a single job in the queue system.
|
||||
/// Can be a standalone backtest, genetic algorithm, or part of a bundle backtest request.
|
||||
/// </summary>
|
||||
public class BacktestJob
|
||||
public class Job
|
||||
{
|
||||
public BacktestJob()
|
||||
public Job()
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
CreatedAt = DateTime.UtcNow;
|
||||
Status = BacktestJobStatus.Pending;
|
||||
Status = JobStatus.Pending;
|
||||
ProgressPercentage = 0;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ public class BacktestJob
|
||||
/// Current status of the job
|
||||
/// </summary>
|
||||
[Required]
|
||||
public BacktestJobStatus Status { get; set; }
|
||||
public JobStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Priority of the job (higher = more important)
|
||||
@@ -122,12 +122,37 @@ public class BacktestJob
|
||||
/// Optional genetic request ID if this is a genetic backtest job
|
||||
/// </summary>
|
||||
public string? GeneticRequestId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of times this job has been retried
|
||||
/// </summary>
|
||||
public int RetryCount { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of retry attempts allowed
|
||||
/// </summary>
|
||||
public int MaxRetries { get; set; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// When the job should be retried next (for exponential backoff)
|
||||
/// </summary>
|
||||
public DateTime? RetryAfter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the failure is retryable (transient vs permanent)
|
||||
/// </summary>
|
||||
public bool IsRetryable { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Failure category for better error handling
|
||||
/// </summary>
|
||||
public FailureCategory? FailureCategory { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Status of a backtest job
|
||||
/// Status of a job
|
||||
/// </summary>
|
||||
public enum BacktestJobStatus
|
||||
public enum JobStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Job is pending and waiting to be claimed by a worker
|
||||
@@ -155,3 +180,34 @@ public enum BacktestJobStatus
|
||||
Cancelled
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Category of failure for better error handling and retry logic
|
||||
/// </summary>
|
||||
public enum FailureCategory
|
||||
{
|
||||
/// <summary>
|
||||
/// Transient failures: network issues, timeouts, temporary service unavailability
|
||||
/// </summary>
|
||||
Transient,
|
||||
|
||||
/// <summary>
|
||||
/// Data errors: missing candles, invalid data format
|
||||
/// </summary>
|
||||
DataError,
|
||||
|
||||
/// <summary>
|
||||
/// System errors: out of memory, database errors, infrastructure issues
|
||||
/// </summary>
|
||||
SystemError,
|
||||
|
||||
/// <summary>
|
||||
/// User errors: invalid input, configuration errors
|
||||
/// </summary>
|
||||
UserError,
|
||||
|
||||
/// <summary>
|
||||
/// Unknown or unclassified errors
|
||||
/// </summary>
|
||||
Unknown
|
||||
}
|
||||
|
||||
|
||||
1720
src/Managing.Infrastructure.Database/Migrations/20251108195534_RenameBacktestJobsToJobs.Designer.cs
generated
Normal file
1720
src/Managing.Infrastructure.Database/Migrations/20251108195534_RenameBacktestJobsToJobs.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Managing.Infrastructure.Databases.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RenameBacktestJobsToJobs : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_BacktestJobs_Users_UserId",
|
||||
table: "BacktestJobs");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_BacktestJobs",
|
||||
table: "BacktestJobs");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "BacktestJobs",
|
||||
newName: "Jobs");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "FailureCategory",
|
||||
table: "Jobs",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsRetryable",
|
||||
table: "Jobs",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "MaxRetries",
|
||||
table: "Jobs",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "RetryAfter",
|
||||
table: "Jobs",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "RetryCount",
|
||||
table: "Jobs",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Jobs_Users_UserId",
|
||||
table: "Jobs");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_Jobs",
|
||||
table: "Jobs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "FailureCategory",
|
||||
table: "Jobs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsRetryable",
|
||||
table: "Jobs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxRetries",
|
||||
table: "Jobs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RetryAfter",
|
||||
table: "Jobs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RetryCount",
|
||||
table: "Jobs");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "Jobs",
|
||||
newName: "BacktestJobs");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_BacktestJobs",
|
||||
table: "BacktestJobs",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_BacktestJobs_Users_UserId",
|
||||
table: "BacktestJobs",
|
||||
column: "UserId",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
}
|
||||
}
|
||||
}
|
||||
1720
src/Managing.Infrastructure.Database/Migrations/20251108202235_RenameJobsTableToLowercase.Designer.cs
generated
Normal file
1720
src/Managing.Infrastructure.Database/Migrations/20251108202235_RenameJobsTableToLowercase.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,77 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Managing.Infrastructure.Databases.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RenameJobsTableToLowercase : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -734,10 +734,16 @@ namespace Managing.Infrastructure.Databases.Migrations
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int?>("FailureCategory")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("GeneticRequestId")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<bool>("IsRetryable")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("JobType")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
@@ -746,6 +752,9 @@ namespace Managing.Infrastructure.Databases.Migrations
|
||||
b.Property<DateTime?>("LastHeartbeat")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("MaxRetries")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
@@ -763,6 +772,12 @@ namespace Managing.Infrastructure.Databases.Migrations
|
||||
b.Property<string>("ResultJson")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<DateTime?>("RetryAfter")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("RetryCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("StartDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
@@ -792,7 +807,7 @@ namespace Managing.Infrastructure.Databases.Migrations
|
||||
b.HasIndex("Status", "JobType", "Priority", "CreatedAt")
|
||||
.HasDatabaseName("idx_status_jobtype_priority_created");
|
||||
|
||||
b.ToTable("BacktestJobs");
|
||||
b.ToTable("jobs", "public");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b =>
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
|
||||
|
||||
[Table("BacktestJobs")]
|
||||
[Table("Jobs")]
|
||||
public class JobEntity
|
||||
{
|
||||
[Key]
|
||||
@@ -16,7 +16,7 @@ public class JobEntity
|
||||
public int UserId { get; set; }
|
||||
|
||||
[Required]
|
||||
public int Status { get; set; } // BacktestJobStatus enum as int
|
||||
public int Status { get; set; } // JobStatus enum as int
|
||||
|
||||
[Required]
|
||||
public int JobType { get; set; } // JobType enum as int
|
||||
@@ -61,6 +61,16 @@ public class JobEntity
|
||||
[MaxLength(255)]
|
||||
public string? GeneticRequestId { get; set; }
|
||||
|
||||
public int RetryCount { get; set; } = 0;
|
||||
|
||||
public int MaxRetries { get; set; } = 3;
|
||||
|
||||
public DateTime? RetryAfter { get; set; }
|
||||
|
||||
public bool IsRetryable { get; set; } = true;
|
||||
|
||||
public int? FailureCategory { get; set; } // FailureCategory enum as int
|
||||
|
||||
// Navigation property
|
||||
public UserEntity? User { get; set; }
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Infrastructure.Databases.PostgreSql;
|
||||
|
||||
public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
public class PostgreSqlJobRepository : IJobRepository
|
||||
{
|
||||
private readonly ManagingDbContext _context;
|
||||
private readonly ILogger<PostgreSqlJobRepository> _logger;
|
||||
@@ -20,7 +20,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<BacktestJob> CreateAsync(BacktestJob job)
|
||||
public async Task<Job> CreateAsync(Job job)
|
||||
{
|
||||
var entity = MapToEntity(job);
|
||||
_context.Jobs.Add(entity);
|
||||
@@ -28,10 +28,9 @@ public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
return MapToDomain(entity);
|
||||
}
|
||||
|
||||
public async Task<BacktestJob?> ClaimNextJobAsync(string workerId, JobType? jobType = null)
|
||||
public async Task<Job?> ClaimNextJobAsync(string workerId, JobType? jobType = null)
|
||||
{
|
||||
// Use execution strategy to support retry with transactions
|
||||
// FOR UPDATE SKIP LOCKED ensures only one worker can claim a specific job
|
||||
var strategy = _context.Database.CreateExecutionStrategy();
|
||||
|
||||
return await strategy.ExecuteAsync(async () =>
|
||||
@@ -42,10 +41,10 @@ public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
{
|
||||
// Build SQL query with optional job type filter
|
||||
var sql = @"
|
||||
SELECT * FROM ""BacktestJobs""
|
||||
SELECT * FROM ""Jobs""
|
||||
WHERE ""Status"" = {0}";
|
||||
|
||||
var parameters = new List<object> { (int)BacktestJobStatus.Pending };
|
||||
var parameters = new List<object> { (int)JobStatus.Pending };
|
||||
|
||||
if (jobType.HasValue)
|
||||
{
|
||||
@@ -70,7 +69,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
}
|
||||
|
||||
// Update the job status atomically
|
||||
job.Status = (int)BacktestJobStatus.Running;
|
||||
job.Status = (int)JobStatus.Running;
|
||||
job.AssignedWorkerId = workerId;
|
||||
job.StartedAt = DateTime.UtcNow;
|
||||
job.LastHeartbeat = DateTime.UtcNow;
|
||||
@@ -89,7 +88,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
});
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(BacktestJob job)
|
||||
public async Task UpdateAsync(Job job)
|
||||
{
|
||||
// Use AsTracking() to enable change tracking since DbContext uses NoTracking by default
|
||||
var entity = await _context.Jobs
|
||||
@@ -115,11 +114,16 @@ public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
entity.RequestId = job.RequestId;
|
||||
entity.GeneticRequestId = job.GeneticRequestId;
|
||||
entity.Priority = job.Priority;
|
||||
entity.RetryCount = job.RetryCount;
|
||||
entity.MaxRetries = job.MaxRetries;
|
||||
entity.RetryAfter = job.RetryAfter;
|
||||
entity.IsRetryable = job.IsRetryable;
|
||||
entity.FailureCategory = job.FailureCategory.HasValue ? (int)job.FailureCategory.Value : null;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<BacktestJob>> GetByBundleRequestIdAsync(Guid bundleRequestId)
|
||||
public async Task<IEnumerable<Job>> GetByBundleRequestIdAsync(Guid bundleRequestId)
|
||||
{
|
||||
var entities = await _context.Jobs
|
||||
.Where(j => j.BundleRequestId == bundleRequestId)
|
||||
@@ -128,7 +132,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
return entities.Select(MapToDomain);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<BacktestJob>> GetByUserIdAsync(int userId)
|
||||
public async Task<IEnumerable<Job>> GetByUserIdAsync(int userId)
|
||||
{
|
||||
var entities = await _context.Jobs
|
||||
.Where(j => j.UserId == userId)
|
||||
@@ -140,16 +144,16 @@ public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
/// <summary>
|
||||
/// Gets all running jobs assigned to a specific worker
|
||||
/// </summary>
|
||||
public async Task<IEnumerable<BacktestJob>> GetRunningJobsByWorkerIdAsync(string workerId)
|
||||
public async Task<IEnumerable<Job>> GetRunningJobsByWorkerIdAsync(string workerId)
|
||||
{
|
||||
var entities = await _context.Jobs
|
||||
.Where(j => j.AssignedWorkerId == workerId && j.Status == (int)BacktestJobStatus.Running)
|
||||
.Where(j => j.AssignedWorkerId == workerId && j.Status == (int)JobStatus.Running)
|
||||
.ToListAsync();
|
||||
|
||||
return entities.Select(MapToDomain);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<BacktestJob>> GetByGeneticRequestIdAsync(string geneticRequestId)
|
||||
public async Task<IEnumerable<Job>> GetByGeneticRequestIdAsync(string geneticRequestId)
|
||||
{
|
||||
var entities = await _context.Jobs
|
||||
.Where(j => j.GeneticRequestId == geneticRequestId)
|
||||
@@ -158,12 +162,12 @@ public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
return entities.Select(MapToDomain);
|
||||
}
|
||||
|
||||
public async Task<(IEnumerable<BacktestJob> Jobs, int TotalCount)> GetPaginatedAsync(
|
||||
public async Task<(IEnumerable<Job> Jobs, int TotalCount)> GetPaginatedAsync(
|
||||
int page,
|
||||
int pageSize,
|
||||
string sortBy = "CreatedAt",
|
||||
string sortOrder = "desc",
|
||||
BacktestJobStatus? status = null,
|
||||
JobStatus? status = null,
|
||||
JobType? jobType = null,
|
||||
int? userId = null,
|
||||
string? workerId = null,
|
||||
@@ -235,7 +239,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
return (jobs, totalCount);
|
||||
}
|
||||
|
||||
public async Task<BacktestJob?> GetByIdAsync(Guid jobId)
|
||||
public async Task<Job?> GetByIdAsync(Guid jobId)
|
||||
{
|
||||
var entity = await _context.Jobs
|
||||
.FirstOrDefaultAsync(j => j.Id == jobId);
|
||||
@@ -243,12 +247,12 @@ public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
return entity != null ? MapToDomain(entity) : null;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<BacktestJob>> GetStaleJobsAsync(int timeoutMinutes = 5)
|
||||
public async Task<IEnumerable<Job>> GetStaleJobsAsync(int timeoutMinutes = 5)
|
||||
{
|
||||
var timeoutThreshold = DateTime.UtcNow.AddMinutes(-timeoutMinutes);
|
||||
|
||||
var entities = await _context.Jobs
|
||||
.Where(j => j.Status == (int)BacktestJobStatus.Running &&
|
||||
.Where(j => j.Status == (int)JobStatus.Running &&
|
||||
(j.LastHeartbeat == null || j.LastHeartbeat < timeoutThreshold))
|
||||
.ToListAsync();
|
||||
|
||||
@@ -262,13 +266,13 @@ public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
// Use AsTracking() to enable change tracking since DbContext uses NoTracking by default
|
||||
var staleJobs = await _context.Jobs
|
||||
.AsTracking()
|
||||
.Where(j => j.Status == (int)BacktestJobStatus.Running &&
|
||||
.Where(j => j.Status == (int)JobStatus.Running &&
|
||||
(j.LastHeartbeat == null || j.LastHeartbeat < timeoutThreshold))
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var job in staleJobs)
|
||||
{
|
||||
job.Status = (int)BacktestJobStatus.Pending;
|
||||
job.Status = (int)JobStatus.Pending;
|
||||
job.AssignedWorkerId = null;
|
||||
job.LastHeartbeat = null;
|
||||
}
|
||||
@@ -299,7 +303,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
// Query 1: Status summary
|
||||
var statusSummarySql = @"
|
||||
SELECT ""Status"", COUNT(*) as Count
|
||||
FROM ""BacktestJobs""
|
||||
FROM ""Jobs""
|
||||
GROUP BY ""Status""
|
||||
ORDER BY ""Status""";
|
||||
|
||||
@@ -322,7 +326,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
// Query 2: Job type summary
|
||||
var jobTypeSummarySql = @"
|
||||
SELECT ""JobType"", COUNT(*) as Count
|
||||
FROM ""BacktestJobs""
|
||||
FROM ""Jobs""
|
||||
GROUP BY ""JobType""
|
||||
ORDER BY ""JobType""";
|
||||
|
||||
@@ -345,7 +349,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
// Query 3: Status + Job type summary
|
||||
var statusTypeSummarySql = @"
|
||||
SELECT ""Status"", ""JobType"", COUNT(*) as Count
|
||||
FROM ""BacktestJobs""
|
||||
FROM ""Jobs""
|
||||
GROUP BY ""Status"", ""JobType""
|
||||
ORDER BY ""Status"", ""JobType""";
|
||||
|
||||
@@ -369,7 +373,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
// Query 4: Total count
|
||||
var totalCountSql = @"
|
||||
SELECT COUNT(*) as Count
|
||||
FROM ""BacktestJobs""";
|
||||
FROM ""Jobs""";
|
||||
|
||||
using (var command = connection.CreateCommand())
|
||||
{
|
||||
@@ -382,7 +386,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
{
|
||||
StatusCounts = statusCounts.Select(s => new JobStatusCount
|
||||
{
|
||||
Status = (BacktestJobStatus)s.Status,
|
||||
Status = (JobStatus)s.Status,
|
||||
Count = s.Count
|
||||
}).ToList(),
|
||||
JobTypeCounts = jobTypeCounts.Select(j => new JobTypeCount
|
||||
@@ -392,7 +396,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
}).ToList(),
|
||||
StatusTypeCounts = statusTypeCounts.Select(st => new JobStatusTypeCount
|
||||
{
|
||||
Status = (BacktestJobStatus)st.Status,
|
||||
Status = (JobStatus)st.Status,
|
||||
JobType = (JobType)st.JobType,
|
||||
Count = st.Count
|
||||
}).ToList(),
|
||||
@@ -430,7 +434,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
public int Count { get; set; }
|
||||
}
|
||||
|
||||
private static JobEntity MapToEntity(BacktestJob job)
|
||||
private static JobEntity MapToEntity(Job job)
|
||||
{
|
||||
return new JobEntity
|
||||
{
|
||||
@@ -452,18 +456,23 @@ public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
ResultJson = job.ResultJson,
|
||||
ErrorMessage = job.ErrorMessage,
|
||||
RequestId = job.RequestId,
|
||||
GeneticRequestId = job.GeneticRequestId
|
||||
GeneticRequestId = job.GeneticRequestId,
|
||||
RetryCount = job.RetryCount,
|
||||
MaxRetries = job.MaxRetries,
|
||||
RetryAfter = job.RetryAfter,
|
||||
IsRetryable = job.IsRetryable,
|
||||
FailureCategory = job.FailureCategory.HasValue ? (int)job.FailureCategory.Value : null
|
||||
};
|
||||
}
|
||||
|
||||
private static BacktestJob MapToDomain(JobEntity entity)
|
||||
private static Job MapToDomain(JobEntity entity)
|
||||
{
|
||||
return new BacktestJob
|
||||
return new Job
|
||||
{
|
||||
Id = entity.Id,
|
||||
BundleRequestId = entity.BundleRequestId,
|
||||
UserId = entity.UserId,
|
||||
Status = (BacktestJobStatus)entity.Status,
|
||||
Status = (JobStatus)entity.Status,
|
||||
JobType = (JobType)entity.JobType,
|
||||
Priority = entity.Priority,
|
||||
ConfigJson = entity.ConfigJson,
|
||||
@@ -478,7 +487,12 @@ public class PostgreSqlJobRepository : IBacktestJobRepository
|
||||
ResultJson = entity.ResultJson,
|
||||
ErrorMessage = entity.ErrorMessage,
|
||||
RequestId = entity.RequestId,
|
||||
GeneticRequestId = entity.GeneticRequestId
|
||||
GeneticRequestId = entity.GeneticRequestId,
|
||||
RetryCount = entity.RetryCount,
|
||||
MaxRetries = entity.MaxRetries,
|
||||
RetryAfter = entity.RetryAfter,
|
||||
IsRetryable = entity.IsRetryable,
|
||||
FailureCategory = entity.FailureCategory.HasValue ? (FailureCategory)entity.FailureCategory.Value : null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ const JobsSettings: React.FC = () => {
|
||||
const [workerIdFilter, setWorkerIdFilter] = useState<string>('')
|
||||
const [bundleRequestIdFilter, setBundleRequestIdFilter] = useState<string>('')
|
||||
const [filtersOpen, setFiltersOpen] = useState<boolean>(false)
|
||||
const [showTable, setShowTable] = useState<boolean>(false)
|
||||
|
||||
const jobClient = new JobClient({}, apiUrl)
|
||||
|
||||
@@ -56,9 +57,10 @@ const JobsSettings: React.FC = () => {
|
||||
bundleRequestIdFilter || null
|
||||
)
|
||||
},
|
||||
enabled: showTable, // Only fetch when table is shown
|
||||
staleTime: 10000, // 10 seconds
|
||||
gcTime: 5 * 60 * 1000,
|
||||
refetchInterval: 5000, // Auto-refresh every 5 seconds
|
||||
refetchInterval: showTable ? 5000 : false, // Auto-refresh only when table is shown
|
||||
})
|
||||
|
||||
const jobs = jobsData?.jobs || []
|
||||
@@ -316,7 +318,50 @@ const JobsSettings: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{filtersOpen && (
|
||||
{/* Load Table Button */}
|
||||
{!showTable && (
|
||||
<div className="card bg-base-100 shadow-md mb-4">
|
||||
<div className="card-body">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="card-title text-lg">Jobs List</h3>
|
||||
<p className="text-sm text-base-content/70">Click the button below to load and view the jobs table</p>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setShowTable(true)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-5 h-5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5M9 11.25v1.5M12 9v3.75m3-3.75v3.75m-9 .75h12.75a2.25 2.25 0 002.25-2.25V6.75a2.25 2.25 0 00-2.25-2.25H6.75A2.25 2.25 0 004.5 6.75v7.5a2.25 2.25 0 002.25 2.25z" />
|
||||
</svg>
|
||||
Load Jobs Table
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showTable && (
|
||||
<>
|
||||
{/* Hide Table Button */}
|
||||
<div className="card bg-base-100 shadow-md mb-4">
|
||||
<div className="card-body py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="card-title text-lg">Jobs List</h3>
|
||||
<button
|
||||
className="btn btn-ghost btn-sm"
|
||||
onClick={() => setShowTable(false)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-5 h-5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Hide Table
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filtersOpen && (
|
||||
<div className="card bg-base-200 mb-4">
|
||||
<div className="card-body">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||
@@ -416,92 +461,96 @@ const JobsSettings: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="alert alert-error mb-4">
|
||||
<span>Error loading jobs: {(error as any)?.message || 'Unknown error'}</span>
|
||||
</div>
|
||||
{error && (
|
||||
<div className="alert alert-error mb-4">
|
||||
<span>Error loading jobs: {(error as any)?.message || 'Unknown error'}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<JobsTable
|
||||
jobs={jobs}
|
||||
isLoading={isLoading}
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
totalCount={totalCount}
|
||||
pageSize={pageSize}
|
||||
sortBy={sortBy}
|
||||
sortOrder={sortOrder}
|
||||
onPageChange={handlePageChange}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<JobsTable
|
||||
jobs={jobs}
|
||||
isLoading={isLoading}
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
totalCount={totalCount}
|
||||
pageSize={pageSize}
|
||||
sortBy={sortBy}
|
||||
sortOrder={sortOrder}
|
||||
onPageChange={handlePageChange}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
|
||||
{/* Bottom Menu Bar */}
|
||||
<BottomMenuBar>
|
||||
<li>
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setFiltersOpen(!filtersOpen)
|
||||
}}
|
||||
className={filtersOpen ? 'active' : ''}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
refetch()
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
clearFilters()
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</BottomMenuBar>
|
||||
{showTable && (
|
||||
<BottomMenuBar>
|
||||
<li>
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setFiltersOpen(!filtersOpen)
|
||||
}}
|
||||
className={filtersOpen ? 'active' : ''}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
refetch()
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
clearFilters()
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</BottomMenuBar>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ const BundleRequestModal: React.FC<BundleRequestModalProps> = ({
|
||||
const successMessage = asTemplate
|
||||
? 'Template saved successfully!'
|
||||
: 'Bundle backtest request created successfully!';
|
||||
new Toast(successMessage, true);
|
||||
new Toast(successMessage, false);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
const errorMessage = asTemplate
|
||||
|
||||
@@ -12,6 +12,8 @@ var host = Host.CreateDefaultBuilder(args)
|
||||
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<Program>();
|
||||
})
|
||||
@@ -50,6 +52,7 @@ var host = Host.CreateDefaultBuilder(args)
|
||||
{
|
||||
options.EnableDetailedErrors();
|
||||
options.EnableSensitiveDataLogging();
|
||||
options.LogTo(Console.WriteLine, LogLevel.Information); // Enable SQL logging to debug table name issues
|
||||
}
|
||||
|
||||
options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
|
||||
@@ -63,21 +66,47 @@ var host = Host.CreateDefaultBuilder(args)
|
||||
services.Configure<BacktestComputeWorkerOptions>(
|
||||
configuration.GetSection(BacktestComputeWorkerOptions.SectionName));
|
||||
|
||||
// Override WorkerId from environment variable if provided
|
||||
// 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;
|
||||
$"{Environment.MachineName}-{taskSlot}";
|
||||
services.Configure<BacktestComputeWorkerOptions>(options =>
|
||||
{
|
||||
options.WorkerId = workerId;
|
||||
});
|
||||
|
||||
// Register the compute worker if enabled
|
||||
var isWorkerEnabled = configuration.GetValue<bool>("WorkerBacktestCompute", false);
|
||||
if (isWorkerEnabled)
|
||||
// Configure GeneticComputeWorker options
|
||||
services.Configure<GeneticComputeWorkerOptions>(
|
||||
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<GeneticComputeWorkerOptions>(options =>
|
||||
{
|
||||
options.WorkerId = geneticWorkerId;
|
||||
});
|
||||
|
||||
// Register the backtest compute worker if enabled
|
||||
var isBacktestWorkerEnabled = configuration.GetValue<bool>("WorkerBacktestCompute", false);
|
||||
if (isBacktestWorkerEnabled)
|
||||
{
|
||||
services.AddHostedService<BacktestComputeWorker>();
|
||||
}
|
||||
|
||||
// Register the genetic compute worker if enabled
|
||||
var isGeneticWorkerEnabled = configuration.GetValue<bool>("WorkerGeneticCompute", false);
|
||||
if (isGeneticWorkerEnabled)
|
||||
{
|
||||
services.AddHostedService<GeneticComputeWorker>();
|
||||
}
|
||||
})
|
||||
.ConfigureLogging((hostingContext, logging) =>
|
||||
{
|
||||
@@ -89,18 +118,41 @@ var host = Host.CreateDefaultBuilder(args)
|
||||
|
||||
// Log worker status
|
||||
var logger = host.Services.GetRequiredService<ILogger<Program>>();
|
||||
var isWorkerEnabled = host.Services.GetRequiredService<IConfiguration>().GetValue<bool>("WorkerBacktestCompute", false);
|
||||
var config = host.Services.GetRequiredService<IConfiguration>();
|
||||
|
||||
if (isWorkerEnabled)
|
||||
var isBacktestWorkerEnabled = config.GetValue<bool>("WorkerBacktestCompute", false);
|
||||
var isGeneticWorkerEnabled = config.GetValue<bool>("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("Worker ID: {WorkerId}", Environment.GetEnvironmentVariable("WORKER_ID") ??
|
||||
host.Services.GetRequiredService<IConfiguration>()["BacktestComputeWorker:WorkerId"] ??
|
||||
Environment.MachineName);
|
||||
logger.LogInformation("Backtest Worker ID: {WorkerId} (Task Slot: {TaskSlot})", backtestWorkerId, taskSlot);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogWarning("BacktestComputeWorker is disabled via configuration. No jobs will be processed.");
|
||||
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
|
||||
|
||||
@@ -7,12 +7,18 @@
|
||||
},
|
||||
"WorkerBacktestCompute": true,
|
||||
"BacktestComputeWorker": {
|
||||
"WorkerId": "local-worker-1",
|
||||
"MaxConcurrentBacktests": 6,
|
||||
"JobPollIntervalSeconds": 5,
|
||||
"HeartbeatIntervalSeconds": 30,
|
||||
"StaleJobTimeoutMinutes": 5
|
||||
},
|
||||
"WorkerGeneticCompute": true,
|
||||
"GeneticComputeWorker": {
|
||||
"MaxConcurrentGenetics": 2,
|
||||
"JobPollIntervalSeconds": 5,
|
||||
"HeartbeatIntervalSeconds": 30,
|
||||
"StaleJobTimeoutMinutes": 10
|
||||
},
|
||||
"PostgreSql": {
|
||||
"ConnectionString": "Host=localhost;Port=5432;Database=managing;Username=postgres;Password=postgres"
|
||||
},
|
||||
|
||||
45
src/Managing.Workers.Api/appsettings.ProductionLocal.json
Normal file
45
src/Managing.Workers.Api/appsettings.ProductionLocal.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"WorkerBacktestCompute": true,
|
||||
"BacktestComputeWorker": {
|
||||
"MaxConcurrentBacktests": 6,
|
||||
"JobPollIntervalSeconds": 5,
|
||||
"HeartbeatIntervalSeconds": 30,
|
||||
"StaleJobTimeoutMinutes": 5
|
||||
},
|
||||
"WorkerGeneticCompute": true,
|
||||
"GeneticComputeWorker": {
|
||||
"MaxConcurrentGenetics": 2,
|
||||
"JobPollIntervalSeconds": 5,
|
||||
"HeartbeatIntervalSeconds": 30,
|
||||
"StaleJobTimeoutMinutes": 10
|
||||
},
|
||||
"PostgreSql": {
|
||||
"ConnectionString": "Host=kaigen-db.kaigen.managing.live;Port=5432;Database=managing;Username=postgres;Password=2ab5423dcca4aa2d"
|
||||
},
|
||||
"InfluxDb": {
|
||||
"Url": "https://influx-db.kaigen.managing.live",
|
||||
"Organization": "managing-org",
|
||||
"Token": "ROvQoZ1Dg5jiKDFxB0saEGqHC3rsLkUNlPL6_AFbOcpNjMieIv8v58yA4v5tFU9sX9LLvXEToPvUrxqQEMaWDw=="
|
||||
},
|
||||
"Sentry": {
|
||||
"Dsn": "https://fe12add48c56419bbdfa86227c188e7a@glitch.kai.managing.live/1"
|
||||
},
|
||||
"N8n": {
|
||||
"WebhookUrl": "https://n8n.kai.managing.live/webhook/fa9308b6-983b-42ec-b085-71599d655951",
|
||||
"IndicatorRequestWebhookUrl": "https://n8n.kai.managing.live/webhook/3aa07b66-1e64-46a7-8618-af300914cb11",
|
||||
"Username": "managing-api",
|
||||
"Password": "T259836*PdiV2@%!eR%Qf4"
|
||||
},
|
||||
"Kaigen": {
|
||||
"BaseUrl": "https://kaigen-back-kaigen-stage.up.railway.app",
|
||||
"DebitEndpoint": "/api/credits/debit",
|
||||
"RefundEndpoint": "/api/credits/refund"
|
||||
}
|
||||
}
|
||||
|
||||
45
src/Managing.Workers.Api/appsettings.SandboxLocal.json
Normal file
45
src/Managing.Workers.Api/appsettings.SandboxLocal.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"WorkerBacktestCompute": true,
|
||||
"BacktestComputeWorker": {
|
||||
"MaxConcurrentBacktests": 6,
|
||||
"JobPollIntervalSeconds": 5,
|
||||
"HeartbeatIntervalSeconds": 30,
|
||||
"StaleJobTimeoutMinutes": 5
|
||||
},
|
||||
"WorkerGeneticCompute": true,
|
||||
"GeneticComputeWorker": {
|
||||
"MaxConcurrentGenetics": 2,
|
||||
"JobPollIntervalSeconds": 5,
|
||||
"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": "zODh8Hn8sN5VwpVJH0HAwDpCJPE4oB5IUg8L4Q0T67KM1Rta6PoM0nATUzf1ddkyWx_VledooZXfFIddahbL9Q=="
|
||||
},
|
||||
"Sentry": {
|
||||
"Dsn": "https://fe12add48c56419bbdfa86227c188e7a@glitch.kai.managing.live/1"
|
||||
},
|
||||
"N8n": {
|
||||
"WebhookUrl": "https://n8n.kai.managing.live/webhook/fa9308b6-983b-42ec-b085-71599d655951",
|
||||
"IndicatorRequestWebhookUrl": "https://n8n.kai.managing.live/webhook/3aa07b66-1e64-46a7-8618-af300914cb11",
|
||||
"Username": "managing-api",
|
||||
"Password": "T259836*PdiV2@%!eR%Qf4"
|
||||
},
|
||||
"Kaigen": {
|
||||
"BaseUrl": "https://kaigen-back-kaigen-stage.up.railway.app",
|
||||
"DebitEndpoint": "/api/credits/debit",
|
||||
"RefundEndpoint": "/api/credits/refund"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,18 @@
|
||||
},
|
||||
"WorkerBacktestCompute": true,
|
||||
"BacktestComputeWorker": {
|
||||
"WorkerId": "worker-1",
|
||||
"MaxConcurrentBacktests": 6,
|
||||
"JobPollIntervalSeconds": 5,
|
||||
"HeartbeatIntervalSeconds": 30,
|
||||
"StaleJobTimeoutMinutes": 5
|
||||
},
|
||||
"WorkerGeneticCompute": true,
|
||||
"GeneticComputeWorker": {
|
||||
"MaxConcurrentGenetics": 2,
|
||||
"JobPollIntervalSeconds": 5,
|
||||
"HeartbeatIntervalSeconds": 30,
|
||||
"StaleJobTimeoutMinutes": 10
|
||||
},
|
||||
"PostgreSql": {
|
||||
"ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user