feat: Jobs infrastructure, endpoints, UI and worker prioritization

This commit is contained in:
Richard
2026-07-06 19:58:23 +02:00
parent 33a52ed173
commit fb703b6c81
9 changed files with 321 additions and 26 deletions
@@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Predictalytics.Infrastructure.Data;
using Predictalytics.Application.Interfaces;
using Predictalytics.Domain.Interfaces;
namespace Predictalytics.Worker.Services;
@@ -40,23 +41,40 @@ public class TraderAnalyticsWorker : BackgroundService
private async Task RunAnalyticsAsync(CancellationToken ct)
{
List<int> traderIds;
List<int> traderIds = new List<int>();
Domain.Entities.BackgroundJob? activeJob = null;
using (var scope = _services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// Find traders who have never been analyzed, or whose last analysis was before their latest trade.
// Prioritize never-analyzed traders.
traderIds = await db.Traders
.Where(t => t.LastAnalyzedAt == null || t.Trades.Any(tr => tr.ExecutedAt > t.LastAnalyzedAt))
.OrderBy(t => t.LastAnalyzedAt == null ? 0 : 1)
.ThenBy(t => t.LastAnalyzedAt)
.Select(t => t.Id)
.Take(500) // Limit batch size to prevent long-running loops without save
.ToListAsync(ct);
var jobRepo = scope.ServiceProvider.GetRequiredService<IJobRepository>();
activeJob = await jobRepo.GetNextPendingJobAsync(Predictalytics.Domain.Enums.JobType.TraderAnalysis, ct);
if (activeJob != null && activeJob.TraderId.HasValue)
{
traderIds.Add(activeJob.TraderId.Value);
activeJob.Status = Predictalytics.Domain.Enums.JobStatus.InProgress;
activeJob.StartedAt = DateTime.UtcNow;
await jobRepo.UpdateAsync(activeJob, ct);
}
else
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// Find traders who have never been analyzed, or whose last analysis was before their latest trade.
// Prioritize never-analyzed traders.
traderIds = await db.Traders
.Where(t => t.LastAnalyzedAt == null || t.Trades.Any(tr => tr.ExecutedAt > t.LastAnalyzedAt))
.OrderBy(t => t.LastAnalyzedAt == null ? 0 : 1)
.ThenBy(t => t.LastAnalyzedAt)
.Select(t => t.Id)
.Take(500) // Limit batch size to prevent long-running loops without save
.ToListAsync(ct);
}
}
_logger.LogInformation("Found {Count} active or unanalyzed traders to update", traderIds.Count);
if (traderIds.Count > 0)
{
_logger.LogInformation("Found {Count} active or unanalyzed traders to update", traderIds.Count);
}
foreach (var id in traderIds)
{
@@ -65,13 +83,36 @@ public class TraderAnalyticsWorker : BackgroundService
using var traderScope = _services.CreateScope();
var pnlEngine = traderScope.ServiceProvider.GetRequiredService<IPositionPnLEngine>();
await pnlEngine.RecalculateTraderPositionsAsync(id, ct);
if (activeJob != null && activeJob.TraderId == id)
{
using var jobScope = _services.CreateScope();
var updateJobRepo = jobScope.ServiceProvider.GetRequiredService<IJobRepository>();
activeJob.Status = Predictalytics.Domain.Enums.JobStatus.Completed;
activeJob.CompletedAt = DateTime.UtcNow;
await updateJobRepo.UpdateAsync(activeJob, ct);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error recalculating positions/PnL for trader {TraderId}", id);
if (activeJob != null && activeJob.TraderId == id)
{
try {
using var jobScope = _services.CreateScope();
var updateJobRepo = jobScope.ServiceProvider.GetRequiredService<IJobRepository>();
activeJob.Status = Predictalytics.Domain.Enums.JobStatus.Failed;
activeJob.CompletedAt = DateTime.UtcNow;
activeJob.ErrorMessage = ex.Message;
await updateJobRepo.UpdateAsync(activeJob, CancellationToken.None);
} catch { /* Ignore secondary errors */ }
}
}
}
_logger.LogInformation("Trader analytics update complete.");
if (traderIds.Count > 0)
{
_logger.LogInformation("Trader analytics update complete.");
}
}
}