289 lines
10 KiB
C#
289 lines
10 KiB
C#
#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<JobStatusResponse>> 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<IJobRepository>();
|
|
var job = await jobRepository.GetByIdAsync(jobGuid);
|
|
|
|
if (job == null)
|
|
{
|
|
return NotFound($"Job with ID {jobId} not found.");
|
|
}
|
|
|
|
var response = new JobStatusResponse
|
|
{
|
|
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 == JobStatus.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
|
|
JobStatus? statusFilter = null;
|
|
if (!string.IsNullOrEmpty(status))
|
|
{
|
|
if (Enum.TryParse<JobStatus>(status, true, out var parsedStatus))
|
|
{
|
|
statusFilter = parsedStatus;
|
|
}
|
|
else
|
|
{
|
|
return BadRequest($"Invalid status value. Valid values are: {string.Join(", ", Enum.GetNames<JobStatus>())}");
|
|
}
|
|
}
|
|
|
|
// 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<IJobRepository>();
|
|
|
|
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<IJobRepository>();
|
|
|
|
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);
|
|
}
|
|
}
|
|
|