146 lines
5.7 KiB
C#
146 lines
5.7 KiB
C#
using System.Text.Json;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Tiku.Application.Jobs;
|
|
using Tiku.Domain.Operations;
|
|
|
|
namespace Tiku.Infrastructure.Jobs;
|
|
|
|
internal sealed partial class BackgroundJobService
|
|
{
|
|
public async Task<IReadOnlyCollection<BackgroundJobItem>> ListAsync(
|
|
Guid tenantId,
|
|
string? jobType = null,
|
|
int limit = 50,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var query = jobsOperationsPersistence.BackgroundJobs.AsNoTracking()
|
|
.Where(job => job.TenantId == tenantId);
|
|
if (!string.IsNullOrWhiteSpace(jobType))
|
|
{
|
|
var normalized = NormalizeJobType(jobType);
|
|
query = query.Where(job => job.JobType == normalized);
|
|
}
|
|
|
|
var jobs = await query
|
|
.OrderByDescending(job => job.CreatedAt)
|
|
.Take(Math.Clamp(limit, 1, 200))
|
|
.ToArrayAsync(cancellationToken);
|
|
return jobs.Select(ToItem).ToArray();
|
|
}
|
|
|
|
public async Task<BackgroundJobItem?> GetAsync(
|
|
Guid jobId,
|
|
Guid? tenantId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var query = jobsOperationsPersistence.BackgroundJobs.AsNoTracking().Where(item => item.Id == jobId);
|
|
if (tenantId.HasValue) query = query.Where(item => item.TenantId == tenantId.Value);
|
|
var job = await query.SingleOrDefaultAsync(cancellationToken);
|
|
return job is null ? null : ToItem(job);
|
|
}
|
|
|
|
public async Task<IReadOnlyCollection<BackgroundJobItem>> ListPlatformAsync(
|
|
Guid? tenantId = null,
|
|
string? jobType = null,
|
|
BackgroundJobStatus? status = null,
|
|
int limit = 100,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var query = jobsOperationsPersistence.BackgroundJobs.AsNoTracking().AsQueryable();
|
|
if (tenantId.HasValue) query = query.Where(item => item.TenantId == tenantId.Value);
|
|
if (!string.IsNullOrWhiteSpace(jobType))
|
|
{
|
|
var normalized = NormalizeJobType(jobType);
|
|
query = query.Where(item => item.JobType == normalized);
|
|
}
|
|
|
|
if (status.HasValue) query = query.Where(item => item.Status == status.Value);
|
|
return (await query.OrderByDescending(item => item.CreatedAt)
|
|
.Take(Math.Clamp(limit, 1, 500))
|
|
.ToArrayAsync(cancellationToken))
|
|
.Select(ToItem)
|
|
.ToArray();
|
|
}
|
|
|
|
public async Task<BackgroundJobItem> RequestCancellationAsync(
|
|
Guid jobId,
|
|
Guid? tenantId,
|
|
Guid actorUserId,
|
|
string reason,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
unitOfWork.ChangeTracker.Clear();
|
|
if (string.IsNullOrWhiteSpace(reason))
|
|
throw new BackgroundJobException("background_job_cancel_reason_required",
|
|
"Cancellation reason is required.");
|
|
var job = await FindMutableAsync(jobId, tenantId, cancellationToken);
|
|
if (job.Status is BackgroundJobStatus.Succeeded or BackgroundJobStatus.Failed or BackgroundJobStatus.Cancelled)
|
|
throw new BackgroundJobException("background_job_not_cancellable",
|
|
"Only pending or processing jobs can be cancelled.");
|
|
|
|
var now = DateTimeOffset.UtcNow;
|
|
job.CancellationRequestedAt = now;
|
|
job.CancellationRequestedBy = actorUserId;
|
|
job.CancellationReason = reason.Trim();
|
|
if (job.Status == BackgroundJobStatus.Pending)
|
|
{
|
|
job.Status = BackgroundJobStatus.Cancelled;
|
|
job.CompletedAt = now;
|
|
}
|
|
|
|
AddMutationAudit(job, actorUserId, "background_job.cancel_requested");
|
|
await unitOfWork.SaveChangesAsync(cancellationToken);
|
|
return ToItem(job);
|
|
}
|
|
|
|
public async Task<BackgroundJobItem> RetryAsync(
|
|
Guid jobId,
|
|
Guid? tenantId,
|
|
Guid actorUserId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
unitOfWork.ChangeTracker.Clear();
|
|
var job = await FindMutableAsync(jobId, tenantId, cancellationToken);
|
|
if (job.Status is not (BackgroundJobStatus.Failed or BackgroundJobStatus.Cancelled))
|
|
throw new BackgroundJobException("background_job_not_retryable",
|
|
"Only failed or cancelled jobs can be retried.");
|
|
|
|
job.Status = BackgroundJobStatus.Pending;
|
|
job.RunAfter = DateTimeOffset.UtcNow;
|
|
job.StartedAt = null;
|
|
job.CompletedAt = null;
|
|
job.LockedBy = null;
|
|
job.LockExpiresAt = null;
|
|
job.LastError = null;
|
|
job.CancellationRequestedAt = null;
|
|
job.CancellationRequestedBy = null;
|
|
job.CancellationReason = null;
|
|
AddMutationAudit(job, actorUserId, "background_job.retry_requested");
|
|
await unitOfWork.SaveChangesAsync(cancellationToken);
|
|
return ToItem(job);
|
|
}
|
|
|
|
private async Task<BackgroundJob> FindMutableAsync(
|
|
Guid jobId,
|
|
Guid? tenantId,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var query = jobsOperationsPersistence.BackgroundJobs.Where(item => item.Id == jobId);
|
|
if (tenantId.HasValue) query = query.Where(item => item.TenantId == tenantId.Value);
|
|
return await query.SingleOrDefaultAsync(cancellationToken) ??
|
|
throw new BackgroundJobException("background_job_not_found", "Background job was not found.");
|
|
}
|
|
|
|
private void AddMutationAudit(BackgroundJob job, Guid actorUserId, string action)
|
|
{
|
|
jobsOperationsPersistence.AuditLogs.Add(new AuditLog
|
|
{
|
|
TenantId = job.TenantId,
|
|
ActorUserId = actorUserId,
|
|
Action = action,
|
|
TargetType = "background_job",
|
|
TargetId = job.Id.ToString(),
|
|
Details = JsonSerializer.SerializeToElement(new { job.JobType, job.Status })
|
|
});
|
|
}
|
|
} |