Add jobs
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using Managing.Api.Models.Requests;
|
||||
using Managing.Api.Models.Responses;
|
||||
using Managing.Application.Abstractions.Repositories;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Abstractions.Shared;
|
||||
using Managing.Application.Hubs;
|
||||
@@ -34,6 +35,7 @@ public class BacktestController : BaseController
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IMoneyManagementService _moneyManagementService;
|
||||
private readonly IGeneticService _geneticService;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BacktestController"/> class.
|
||||
@@ -51,13 +53,15 @@ public class BacktestController : BaseController
|
||||
IAccountService accountService,
|
||||
IMoneyManagementService moneyManagementService,
|
||||
IGeneticService geneticService,
|
||||
IUserService userService) : base(userService)
|
||||
IUserService userService,
|
||||
IServiceScopeFactory serviceScopeFactory) : base(userService)
|
||||
{
|
||||
_hubContext = hubContext;
|
||||
_backtester = backtester;
|
||||
_accountService = accountService;
|
||||
_moneyManagementService = moneyManagementService;
|
||||
_geneticService = geneticService;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -788,6 +792,51 @@ public class BacktestController : BaseController
|
||||
return Ok(new { Unsubscribed = true, RequestId = requestId });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status of a bundle backtest request, aggregating all job statuses.
|
||||
/// </summary>
|
||||
/// <param name="bundleRequestId">The bundle request ID</param>
|
||||
/// <returns>The bundle status with aggregated job statistics</returns>
|
||||
[HttpGet]
|
||||
[Route("Bundle/{bundleRequestId}/Status")]
|
||||
public async Task<ActionResult<BundleBacktestStatusResponse>> GetBundleStatus(string bundleRequestId)
|
||||
{
|
||||
if (!Guid.TryParse(bundleRequestId, out var bundleGuid))
|
||||
{
|
||||
return BadRequest("Invalid bundle request ID format. Must be a valid GUID.");
|
||||
}
|
||||
|
||||
var user = await GetUser();
|
||||
var bundleRequest = _backtester.GetBundleBacktestRequestByIdForUser(user, bundleGuid);
|
||||
|
||||
if (bundleRequest == null)
|
||||
{
|
||||
return NotFound($"Bundle backtest request with ID {bundleRequestId} not found.");
|
||||
}
|
||||
|
||||
// Get all jobs for this bundle
|
||||
using var serviceScope = _serviceScopeFactory.CreateScope();
|
||||
var jobRepository = serviceScope.ServiceProvider.GetRequiredService<IBacktestJobRepository>();
|
||||
var jobs = await jobRepository.GetByBundleRequestIdAsync(bundleGuid);
|
||||
|
||||
var response = new BundleBacktestStatusResponse
|
||||
{
|
||||
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),
|
||||
ProgressPercentage = bundleRequest.ProgressPercentage,
|
||||
CreatedAt = bundleRequest.CreatedAt,
|
||||
CompletedAt = bundleRequest.CompletedAt,
|
||||
ErrorMessage = bundleRequest.ErrorMessage
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a genetic algorithm optimization with the specified configuration.
|
||||
/// This endpoint saves the genetic request to the database and returns the request ID.
|
||||
|
||||
288
src/Managing.Api/Controllers/JobController.cs
Normal file
288
src/Managing.Api/Controllers/JobController.cs
Normal file
@@ -0,0 +1,288 @@
|
||||
#nullable enable
|
||||
using System.Text.Json;
|
||||
using Managing.Api.Models.Responses;
|
||||
using Managing.Application.Abstractions.Repositories;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Shared;
|
||||
using Managing.Domain.Backtests;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for managing job operations.
|
||||
/// Provides endpoints for querying job status and progress.
|
||||
/// Requires admin authorization for access.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("[controller]")]
|
||||
[Produces("application/json")]
|
||||
public class JobController : BaseController
|
||||
{
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly IAdminConfigurationService _adminService;
|
||||
private readonly ILogger<JobController> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JobController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userService">The service for user management.</param>
|
||||
/// <param name="serviceScopeFactory">The service scope factory for creating scoped services.</param>
|
||||
/// <param name="adminService">The admin configuration service for authorization checks.</param>
|
||||
/// <param name="logger">The logger instance.</param>
|
||||
public JobController(
|
||||
IUserService userService,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
IAdminConfigurationService adminService,
|
||||
ILogger<JobController> logger) : base(userService)
|
||||
{
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_adminService = adminService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the current user is an admin
|
||||
/// </summary>
|
||||
private async Task<bool> IsUserAdmin()
|
||||
{
|
||||
try
|
||||
{
|
||||
var user = await GetUser();
|
||||
return await _adminService.IsUserAdminAsync(user.Name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error checking if user is admin");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status of a job by its ID.
|
||||
/// Admin only endpoint.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
if (!await IsUserAdmin())
|
||||
{
|
||||
_logger.LogWarning("Non-admin user attempted to access job status endpoint");
|
||||
return StatusCode(403, new { error = "Only admin users can access job status" });
|
||||
}
|
||||
|
||||
if (!Guid.TryParse(jobId, out var jobGuid))
|
||||
{
|
||||
return BadRequest("Invalid job ID format. Must be a valid GUID.");
|
||||
}
|
||||
|
||||
using var serviceScope = _serviceScopeFactory.CreateScope();
|
||||
var jobRepository = serviceScope.ServiceProvider.GetRequiredService<IBacktestJobRepository>();
|
||||
var job = await jobRepository.GetByIdAsync(jobGuid);
|
||||
|
||||
if (job == null)
|
||||
{
|
||||
return NotFound($"Job with ID {jobId} not found.");
|
||||
}
|
||||
|
||||
var response = new BacktestJobStatusResponse
|
||||
{
|
||||
JobId = job.Id,
|
||||
Status = job.Status.ToString(),
|
||||
ProgressPercentage = job.ProgressPercentage,
|
||||
CreatedAt = job.CreatedAt,
|
||||
StartedAt = job.StartedAt,
|
||||
CompletedAt = job.CompletedAt,
|
||||
ErrorMessage = job.ErrorMessage,
|
||||
Result = job.Status == BacktestJobStatus.Completed && !string.IsNullOrEmpty(job.ResultJson)
|
||||
? JsonSerializer.Deserialize<LightBacktest>(job.ResultJson)
|
||||
: null
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a paginated list of jobs with optional filters and sorting.
|
||||
/// Admin only endpoint.
|
||||
/// </summary>
|
||||
/// <param name="page">Page number (defaults to 1)</param>
|
||||
/// <param name="pageSize">Number of items per page (defaults to 50, max 100)</param>
|
||||
/// <param name="sortBy">Field to sort by (CreatedAt, StartedAt, CompletedAt, Priority, Status, JobType) - defaults to CreatedAt</param>
|
||||
/// <param name="sortOrder">Sort order - "asc" or "desc" (defaults to "desc")</param>
|
||||
/// <param name="status">Optional status filter (Pending, Running, Completed, Failed, Cancelled)</param>
|
||||
/// <param name="jobType">Optional job type filter (Backtest, GeneticBacktest)</param>
|
||||
/// <param name="userId">Optional user ID filter</param>
|
||||
/// <param name="workerId">Optional worker ID filter</param>
|
||||
/// <param name="bundleRequestId">Optional bundle request ID filter</param>
|
||||
/// <returns>A paginated list of jobs</returns>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PaginatedJobsResponse>> GetJobs(
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 50,
|
||||
[FromQuery] string sortBy = "CreatedAt",
|
||||
[FromQuery] string sortOrder = "desc",
|
||||
[FromQuery] string? status = null,
|
||||
[FromQuery] string? jobType = null,
|
||||
[FromQuery] int? userId = null,
|
||||
[FromQuery] string? workerId = null,
|
||||
[FromQuery] string? bundleRequestId = null)
|
||||
{
|
||||
if (!await IsUserAdmin())
|
||||
{
|
||||
_logger.LogWarning("Non-admin user attempted to list jobs");
|
||||
return StatusCode(403, new { error = "Only admin users can list jobs" });
|
||||
}
|
||||
|
||||
// Validate pagination parameters
|
||||
if (page < 1)
|
||||
{
|
||||
return BadRequest("Page must be greater than 0");
|
||||
}
|
||||
|
||||
if (pageSize < 1 || pageSize > 100)
|
||||
{
|
||||
return BadRequest("Page size must be between 1 and 100");
|
||||
}
|
||||
|
||||
if (sortOrder != "asc" && sortOrder != "desc")
|
||||
{
|
||||
return BadRequest("Sort order must be 'asc' or 'desc'");
|
||||
}
|
||||
|
||||
// Parse status filter
|
||||
BacktestJobStatus? statusFilter = null;
|
||||
if (!string.IsNullOrEmpty(status))
|
||||
{
|
||||
if (Enum.TryParse<BacktestJobStatus>(status, true, out var parsedStatus))
|
||||
{
|
||||
statusFilter = parsedStatus;
|
||||
}
|
||||
else
|
||||
{
|
||||
return BadRequest($"Invalid status value. Valid values are: {string.Join(", ", Enum.GetNames<BacktestJobStatus>())}");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse job type filter
|
||||
JobType? jobTypeFilter = null;
|
||||
if (!string.IsNullOrEmpty(jobType))
|
||||
{
|
||||
if (Enum.TryParse<JobType>(jobType, true, out var parsedJobType))
|
||||
{
|
||||
jobTypeFilter = parsedJobType;
|
||||
}
|
||||
else
|
||||
{
|
||||
return BadRequest($"Invalid job type value. Valid values are: {string.Join(", ", Enum.GetNames<JobType>())}");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse bundle request ID
|
||||
Guid? bundleRequestIdFilter = null;
|
||||
if (!string.IsNullOrEmpty(bundleRequestId))
|
||||
{
|
||||
if (!Guid.TryParse(bundleRequestId, out var bundleGuid))
|
||||
{
|
||||
return BadRequest("Invalid bundle request ID format. Must be a valid GUID.");
|
||||
}
|
||||
bundleRequestIdFilter = bundleGuid;
|
||||
}
|
||||
|
||||
using var serviceScope = _serviceScopeFactory.CreateScope();
|
||||
var jobRepository = serviceScope.ServiceProvider.GetRequiredService<IBacktestJobRepository>();
|
||||
|
||||
var (jobs, totalCount) = await jobRepository.GetPaginatedAsync(
|
||||
page,
|
||||
pageSize,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
statusFilter,
|
||||
jobTypeFilter,
|
||||
userId,
|
||||
workerId,
|
||||
bundleRequestIdFilter);
|
||||
|
||||
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
|
||||
|
||||
var response = new PaginatedJobsResponse
|
||||
{
|
||||
Jobs = jobs.Select(j => new JobListItemResponse
|
||||
{
|
||||
JobId = j.Id,
|
||||
Status = j.Status.ToString(),
|
||||
JobType = j.JobType.ToString(),
|
||||
ProgressPercentage = j.ProgressPercentage,
|
||||
Priority = j.Priority,
|
||||
UserId = j.UserId,
|
||||
BundleRequestId = j.BundleRequestId,
|
||||
GeneticRequestId = j.GeneticRequestId,
|
||||
AssignedWorkerId = j.AssignedWorkerId,
|
||||
CreatedAt = j.CreatedAt,
|
||||
StartedAt = j.StartedAt,
|
||||
CompletedAt = j.CompletedAt,
|
||||
LastHeartbeat = j.LastHeartbeat,
|
||||
ErrorMessage = j.ErrorMessage,
|
||||
StartDate = j.StartDate,
|
||||
EndDate = j.EndDate
|
||||
}).ToList(),
|
||||
TotalCount = totalCount,
|
||||
CurrentPage = page,
|
||||
PageSize = pageSize,
|
||||
TotalPages = totalPages,
|
||||
HasNextPage = page < totalPages,
|
||||
HasPreviousPage = page > 1
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a summary of jobs grouped by status and job type with counts.
|
||||
/// Admin only endpoint.
|
||||
/// </summary>
|
||||
/// <returns>Summary statistics of jobs</returns>
|
||||
[HttpGet("summary")]
|
||||
public async Task<ActionResult<JobSummaryResponse>> GetJobSummary()
|
||||
{
|
||||
if (!await IsUserAdmin())
|
||||
{
|
||||
_logger.LogWarning("Non-admin user attempted to get job summary");
|
||||
return StatusCode(403, new { error = "Only admin users can access job summary" });
|
||||
}
|
||||
|
||||
using var serviceScope = _serviceScopeFactory.CreateScope();
|
||||
var jobRepository = serviceScope.ServiceProvider.GetRequiredService<IBacktestJobRepository>();
|
||||
|
||||
var summary = await jobRepository.GetSummaryAsync();
|
||||
|
||||
var response = new JobSummaryResponse
|
||||
{
|
||||
StatusSummary = summary.StatusCounts.Select(s => new JobStatusSummary
|
||||
{
|
||||
Status = s.Status.ToString(),
|
||||
Count = s.Count
|
||||
}).ToList(),
|
||||
JobTypeSummary = summary.JobTypeCounts.Select(j => new JobTypeSummary
|
||||
{
|
||||
JobType = j.JobType.ToString(),
|
||||
Count = j.Count
|
||||
}).ToList(),
|
||||
StatusTypeSummary = summary.StatusTypeCounts.Select(st => new JobStatusTypeSummary
|
||||
{
|
||||
Status = st.Status.ToString(),
|
||||
JobType = st.JobType.ToString(),
|
||||
Count = st.Count
|
||||
}).ToList(),
|
||||
TotalJobs = summary.TotalJobs
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user