//----------------------------------------------------------------------- // // Copyright (c) aliasvault. All rights reserved. // Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information. // //----------------------------------------------------------------------- namespace AliasVault.TaskRunner.Workers; using AliasServerDb; using AliasVault.Shared.Models.Enums; using AliasVault.Shared.Server.Services; using AliasVault.TaskRunner.Tasks; using Microsoft.EntityFrameworkCore; /// /// A worker for the TaskRunner. /// /// ILogger instance. /// List of maintenance tasks. /// Server settings service. /// Database context factory. public class TaskRunnerWorker( ILogger logger, IEnumerable tasks, ServerSettingsService settingsService, IAliasServerDbContextFactory dbContextFactory) : BackgroundService { /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { logger.LogInformation("AliasVault.TaskRunner started at: {Time}", DateTimeOffset.Now); while (!stoppingToken.IsCancellationRequested) { await using var dbContext = await dbContextFactory.CreateDbContextAsync(stoppingToken); var settings = await settingsService.GetAllSettingsAsync(); var now = DateTime.UtcNow; var today = now.Date; // Check for on-demand run request var onDemandJob = await dbContext.TaskRunnerJobs .Where(j => j.IsOnDemand && j.Status == TaskRunnerJobStatus.Pending) .OrderByDescending(j => j.StartTime) .FirstOrDefaultAsync(stoppingToken); if (onDemandJob != null) { await ExecuteMaintenanceTasks(onDemandJob, dbContext, stoppingToken); } else { // Regular scheduled run logic var scheduledTime = settings.MaintenanceTime; var currentTime = TimeOnly.FromDateTime(now); var shouldRunToday = settings.TaskRunnerDays.Contains((int)now.DayOfWeek + 1); var hasPassedScheduledTime = currentTime >= scheduledTime; if (shouldRunToday && hasPassedScheduledTime) { var existingJob = await dbContext.TaskRunnerJobs .Where(j => j.Name == nameof(TaskRunnerJobType.Maintenance) && !j.IsOnDemand && j.RunDate.Date == today) .OrderByDescending(j => j.StartTime) .FirstOrDefaultAsync(stoppingToken); if (existingJob == null) { var job = new TaskRunnerJob { Name = nameof(TaskRunnerJobType.Maintenance), RunDate = today, StartTime = TimeOnly.FromDateTime(now), Status = TaskRunnerJobStatus.Running, IsOnDemand = false, }; dbContext.TaskRunnerJobs.Add(job); await dbContext.SaveChangesAsync(stoppingToken); await ExecuteMaintenanceTasks(job, dbContext, stoppingToken); } } } // Check every minute for schedule changes or on-demand requests await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); } } /// /// Executes the maintenance tasks. /// /// The job to execute. /// The database context. /// The cancellation token. private async Task ExecuteMaintenanceTasks(TaskRunnerJob job, AliasServerDbContext dbContext, CancellationToken stoppingToken) { logger.LogInformation("Starting maintenance tasks at {Time} (On-demand: {IsOnDemand})", DateTime.UtcNow, job.IsOnDemand); try { foreach (var task in tasks) { // Check cancellation before each task stoppingToken.ThrowIfCancellationRequested(); try { job.Status = TaskRunnerJobStatus.Running; await dbContext.SaveChangesAsync(stoppingToken); await task.ExecuteAsync(stoppingToken); } catch (OperationCanceledException) { // Handle cancellation gracefully job.Status = TaskRunnerJobStatus.Canceled; job.ErrorMessage = "Task execution was canceled."; await dbContext.SaveChangesAsync(stoppingToken); throw; } catch (Exception ex) { logger.LogError(ex, "Error executing task {TaskName}", task.Name); job.Status = TaskRunnerJobStatus.Error; job.ErrorMessage = $"Task {task.Name} failed: {ex.Message}"; await dbContext.SaveChangesAsync(stoppingToken); break; } } if (job.Status != TaskRunnerJobStatus.Error && job.Status != TaskRunnerJobStatus.Canceled) { job.Status = TaskRunnerJobStatus.Finished; } } finally { job.EndTime = TimeOnly.FromDateTime(DateTime.UtcNow); await dbContext.SaveChangesAsync(stoppingToken); } logger.LogInformation("Tasks completed with status: {Status}", job.Status); } }