From 3d9b286206e52cd4b3ff8217e46ab438f807a24f Mon Sep 17 00:00:00 2001 From: Flaminel Date: Mon, 19 May 2025 12:35:58 +0300 Subject: [PATCH] #19 --- code/Executable/Controllers/JobsController.cs | 98 +++++++++++-------- code/Executable/Models/ScheduleRequest.cs | 14 +++ code/Infrastructure/Models/JobSchedule.cs | 36 +++++++ code/Infrastructure/Models/JobType.cs | 37 +++++++ .../Interfaces/IJobManagementService.cs | 14 +-- .../Services/JobManagementService.cs | 26 +++-- .../Utilities/CronExpressionConverter.cs | 85 ++++++++++++++++ 7 files changed, 254 insertions(+), 56 deletions(-) create mode 100644 code/Executable/Models/ScheduleRequest.cs create mode 100644 code/Infrastructure/Models/JobSchedule.cs create mode 100644 code/Infrastructure/Models/JobType.cs create mode 100644 code/Infrastructure/Utilities/CronExpressionConverter.cs diff --git a/code/Executable/Controllers/JobsController.cs b/code/Executable/Controllers/JobsController.cs index 35cf910e..bbc9c3a4 100644 --- a/code/Executable/Controllers/JobsController.cs +++ b/code/Executable/Controllers/JobsController.cs @@ -1,4 +1,7 @@ +using Executable.Models; +using Infrastructure.Models; using Infrastructure.Services.Interfaces; +using Infrastructure.Utilities; using Microsoft.AspNetCore.Mvc; namespace Executable.Controllers; @@ -31,122 +34,131 @@ public class JobsController : ControllerBase } } - [HttpGet("{jobName}")] - public async Task GetJob(string jobName) + [HttpGet("{jobType}")] + public async Task GetJob(JobType jobType) { try { - var jobInfo = await _jobManagementService.GetJob(jobName); + var jobInfo = await _jobManagementService.GetJob(jobType); + if (jobInfo.Status == "Not Found") { - return NotFound($"Job '{jobName}' not found"); + return NotFound($"Job '{jobType}' not found"); } return Ok(jobInfo); } catch (Exception ex) { - _logger.LogError(ex, "Error getting job {jobName}", jobName); - return StatusCode(500, $"An error occurred while retrieving job '{jobName}'"); + _logger.LogError(ex, "Error getting job {jobType}", jobType); + return StatusCode(500, $"An error occurred while retrieving job '{jobType}'"); } } - [HttpPost("{jobName}/start")] - public async Task StartJob(string jobName, [FromQuery] string cronExpression = null) + [HttpPost("{jobType}/start")] + public async Task StartJob(JobType jobType, [FromBody] ScheduleRequest scheduleRequest = null) { try { - var result = await _jobManagementService.StartJob(jobName, cronExpression); + // Get the schedule from the request body if provided + JobSchedule jobSchedule = scheduleRequest?.Schedule; + + var result = await _jobManagementService.StartJob(jobType, jobSchedule); + if (!result) { - return BadRequest($"Failed to start job '{jobName}'"); + return BadRequest($"Failed to start job '{jobType}'"); } - return Ok(new { Message = $"Job '{jobName}' started successfully" }); + return Ok(new { Message = $"Job '{jobType}' started successfully" }); } catch (Exception ex) { - _logger.LogError(ex, "Error starting job {jobName}", jobName); - return StatusCode(500, $"An error occurred while starting job '{jobName}'"); + _logger.LogError(ex, "Error starting job {jobType}", jobType); + return StatusCode(500, $"An error occurred while starting job '{jobType}'"); } } - [HttpPost("{jobName}/stop")] - public async Task StopJob(string jobName) + [HttpPost("{jobType}/stop")] + public async Task StopJob(JobType jobType) { try { - var result = await _jobManagementService.StopJob(jobName); + var result = await _jobManagementService.StopJob(jobType); + if (!result) { - return BadRequest($"Failed to stop job '{jobName}'"); + return BadRequest($"Failed to stop job '{jobType}'"); } - return Ok(new { Message = $"Job '{jobName}' stopped successfully" }); + return Ok(new { Message = $"Job '{jobType}' stopped successfully" }); } catch (Exception ex) { - _logger.LogError(ex, "Error stopping job {jobName}", jobName); - return StatusCode(500, $"An error occurred while stopping job '{jobName}'"); + _logger.LogError(ex, "Error stopping job {jobType}", jobType); + return StatusCode(500, $"An error occurred while stopping job '{jobType}'"); } } - [HttpPost("{jobName}/pause")] - public async Task PauseJob(string jobName) + [HttpPost("{jobType}/pause")] + public async Task PauseJob(JobType jobType) { try { - var result = await _jobManagementService.PauseJob(jobName); + var result = await _jobManagementService.PauseJob(jobType); + if (!result) { - return BadRequest($"Failed to pause job '{jobName}'"); + return BadRequest($"Failed to pause job '{jobType}'"); } - return Ok(new { Message = $"Job '{jobName}' paused successfully" }); + return Ok(new { Message = $"Job '{jobType}' paused successfully" }); } catch (Exception ex) { - _logger.LogError(ex, "Error pausing job {jobName}", jobName); - return StatusCode(500, $"An error occurred while pausing job '{jobName}'"); + _logger.LogError(ex, "Error pausing job {jobType}", jobType); + return StatusCode(500, $"An error occurred while pausing job '{jobType}'"); } } - [HttpPost("{jobName}/resume")] - public async Task ResumeJob(string jobName) + [HttpPost("{jobType}/resume")] + public async Task ResumeJob(JobType jobType) { try { - var result = await _jobManagementService.ResumeJob(jobName); + var result = await _jobManagementService.ResumeJob(jobType); + if (!result) { - return BadRequest($"Failed to resume job '{jobName}'"); + return BadRequest($"Failed to resume job '{jobType}'"); } - return Ok(new { Message = $"Job '{jobName}' resumed successfully" }); + return Ok(new { Message = $"Job '{jobType}' resumed successfully" }); } catch (Exception ex) { - _logger.LogError(ex, "Error resuming job {jobName}", jobName); - return StatusCode(500, $"An error occurred while resuming job '{jobName}'"); + _logger.LogError(ex, "Error resuming job {jobType}", jobType); + return StatusCode(500, $"An error occurred while resuming job '{jobType}'"); } } - [HttpPut("{jobName}/schedule")] - public async Task UpdateJobSchedule(string jobName, [FromQuery] string cronExpression) + [HttpPut("{jobType}/schedule")] + public async Task UpdateJobSchedule(JobType jobType, [FromBody] ScheduleRequest scheduleRequest) { - if (string.IsNullOrEmpty(cronExpression)) + if (scheduleRequest?.Schedule == null) { - return BadRequest("Cron expression is required"); + return BadRequest("Schedule is required"); } try { - var result = await _jobManagementService.UpdateJobSchedule(jobName, cronExpression); + var result = await _jobManagementService.UpdateJobSchedule(jobType, scheduleRequest.Schedule); + if (!result) { - return BadRequest($"Failed to update schedule for job '{jobName}'"); + return BadRequest($"Failed to update schedule for job '{jobType}'"); } - return Ok(new { Message = $"Job '{jobName}' schedule updated successfully" }); + return Ok(new { Message = $"Job '{jobType}' schedule updated successfully" }); } catch (Exception ex) { - _logger.LogError(ex, "Error updating job {jobName} schedule", jobName); - return StatusCode(500, $"An error occurred while updating schedule for job '{jobName}'"); + _logger.LogError(ex, "Error updating job {jobType} schedule", jobType); + return StatusCode(500, $"An error occurred while updating schedule for job '{jobType}'"); } } } diff --git a/code/Executable/Models/ScheduleRequest.cs b/code/Executable/Models/ScheduleRequest.cs new file mode 100644 index 00000000..2304a1b5 --- /dev/null +++ b/code/Executable/Models/ScheduleRequest.cs @@ -0,0 +1,14 @@ +namespace Executable.Models; + +using Infrastructure.Models; + +/// +/// Represents a request to schedule a job +/// +public class ScheduleRequest +{ + /// + /// The schedule information for the job + /// + public JobSchedule Schedule { get; set; } +} diff --git a/code/Infrastructure/Models/JobSchedule.cs b/code/Infrastructure/Models/JobSchedule.cs new file mode 100644 index 00000000..3a6e9b46 --- /dev/null +++ b/code/Infrastructure/Models/JobSchedule.cs @@ -0,0 +1,36 @@ +namespace Infrastructure.Models; + +/// +/// Represents the unit of time for job scheduling intervals +/// +public enum ScheduleUnit +{ + Seconds, + Minutes, + Hours +} + +/// +/// Represents a user-friendly job schedule format +/// +public class JobSchedule +{ + /// + /// The numeric interval value + /// + public int Every { get; set; } + + /// + /// The unit of time for the interval (seconds, minutes, or hours) + /// + public ScheduleUnit Type { get; set; } + + /// + /// Converts the JobSchedule to a Quartz cron expression string + /// + /// A valid cron expression string + public string ToCronExpression() + { + return CronExpressionConverter.ConvertToCronExpression(this); + } +} diff --git a/code/Infrastructure/Models/JobType.cs b/code/Infrastructure/Models/JobType.cs new file mode 100644 index 00000000..80138443 --- /dev/null +++ b/code/Infrastructure/Models/JobType.cs @@ -0,0 +1,37 @@ +namespace Infrastructure.Models; + +/// +/// Represents the supported job types in the application +/// +public enum JobType +{ + QueueCleaner, + ContentBlocker, + DownloadCleaner +} + +/// +/// Extension methods for JobType enum +/// +public static class JobTypeExtensions +{ + /// + /// Converts a JobType enum to its string representation + /// + /// The job type to convert + /// String representation of the job type + public static string ToJobName(this JobType jobType) => jobType.ToString(); + + /// + /// Parses a string to JobType enum + /// + /// The job name to parse + /// JobType if successful, null if parsing failed + public static JobType? TryParseJobType(string jobName) + { + if (string.IsNullOrEmpty(jobName)) + return null; + + return Enum.TryParse(jobName, true, out var result) ? result : null; + } +} diff --git a/code/Infrastructure/Services/Interfaces/IJobManagementService.cs b/code/Infrastructure/Services/Interfaces/IJobManagementService.cs index 349b9375..a8e1cc83 100644 --- a/code/Infrastructure/Services/Interfaces/IJobManagementService.cs +++ b/code/Infrastructure/Services/Interfaces/IJobManagementService.cs @@ -1,14 +1,14 @@ -using Infrastructure.Models; +using Infrastructure.Models; namespace Infrastructure.Services.Interfaces; public interface IJobManagementService { - Task StartJob(string jobName, string? cronExpression = null); - Task StopJob(string jobName); - Task PauseJob(string jobName); - Task ResumeJob(string jobName); + Task StartJob(JobType jobType, JobSchedule? schedule = null); + Task StopJob(JobType jobType); + Task PauseJob(JobType jobType); + Task ResumeJob(JobType jobType); Task> GetAllJobs(); - Task GetJob(string jobName); - Task UpdateJobSchedule(string jobName, string cronExpression); + Task GetJob(JobType jobType); + Task UpdateJobSchedule(JobType jobType, JobSchedule schedule); } \ No newline at end of file diff --git a/code/Infrastructure/Services/JobManagementService.cs b/code/Infrastructure/Services/JobManagementService.cs index d0f1b5ff..70df20f8 100644 --- a/code/Infrastructure/Services/JobManagementService.cs +++ b/code/Infrastructure/Services/JobManagementService.cs @@ -1,5 +1,6 @@ using Common.Configuration; using Infrastructure.Models; +using Infrastructure.Utilities; using Microsoft.Extensions.Logging; using Quartz; using System.Collections.Concurrent; @@ -20,8 +21,11 @@ public class JobManagementService : IJobManagementService _schedulerFactory = schedulerFactory; } - public async Task StartJob(string jobName, string? cronExpression = null) + public async Task StartJob(JobType jobType, JobSchedule? schedule = null) { + string jobName = jobType.ToJobName(); + string? cronExpression = schedule?.ToCronExpression(); + try { var scheduler = await _schedulerFactory.GetScheduler(); @@ -92,8 +96,9 @@ public class JobManagementService : IJobManagementService } } - public async Task StopJob(string jobName) + public async Task StopJob(JobType jobType) { + string jobName = jobType.ToJobName(); try { var scheduler = await _schedulerFactory.GetScheduler(); @@ -122,8 +127,9 @@ public class JobManagementService : IJobManagementService } } - public async Task PauseJob(string jobName) + public async Task PauseJob(JobType jobType) { + string jobName = jobType.ToJobName(); try { var scheduler = await _schedulerFactory.GetScheduler(); @@ -146,8 +152,9 @@ public class JobManagementService : IJobManagementService } } - public async Task ResumeJob(string jobName) + public async Task ResumeJob(JobType jobType) { + string jobName = jobType.ToJobName(); try { var scheduler = await _schedulerFactory.GetScheduler(); @@ -230,8 +237,9 @@ public class JobManagementService : IJobManagementService } } - public async Task GetJob(string jobName) + public async Task GetJob(JobType jobType) { + string jobName = jobType.ToJobName(); try { var scheduler = await _schedulerFactory.GetScheduler(); @@ -287,8 +295,14 @@ public class JobManagementService : IJobManagementService } } - public async Task UpdateJobSchedule(string jobName, string cronExpression) + public async Task UpdateJobSchedule(JobType jobType, JobSchedule schedule) { + if (schedule == null) + throw new ArgumentNullException(nameof(schedule)); + + string jobName = jobType.ToJobName(); + string cronExpression = schedule.ToCronExpression(); + try { var scheduler = await _schedulerFactory.GetScheduler(); diff --git a/code/Infrastructure/Utilities/CronExpressionConverter.cs b/code/Infrastructure/Utilities/CronExpressionConverter.cs new file mode 100644 index 00000000..8c92db74 --- /dev/null +++ b/code/Infrastructure/Utilities/CronExpressionConverter.cs @@ -0,0 +1,85 @@ +using Infrastructure.Models; + +namespace Infrastructure.Utilities; + +/// +/// Utility for converting user-friendly schedule formats to Quartz cron expressions +/// +public static class CronExpressionConverter +{ + /// + /// Converts a JobSchedule to a Quartz cron expression + /// + /// The job schedule to convert + /// A valid Quartz cron expression + /// Thrown when the schedule has invalid values + public static string ConvertToCronExpression(JobSchedule schedule) + { + if (schedule == null) + throw new ArgumentNullException(nameof(schedule)); + + if (schedule.Every <= 0) + throw new ArgumentException("Every must be greater than zero", nameof(schedule.Every)); + + // Cron format: Seconds Minutes Hours Day-of-month Month Day-of-week Year + return schedule.Type switch + { + ScheduleUnit.Seconds when schedule.Every < 60 => + $"*/{schedule.Every} * * ? * * *", // Every n seconds + + ScheduleUnit.Minutes when schedule.Every < 60 => + $"0 */{schedule.Every} * ? * * *", // Every n minutes + + ScheduleUnit.Hours when schedule.Every < 24 => + $"0 0 */{schedule.Every} ? * * *", // Every n hours + + _ => throw new ArgumentException($"Invalid schedule: {schedule.Every} {schedule.Type}") + }; + } + + /// + /// This method is only kept for reference. We no longer parse schedules from strings. + /// + /// The schedule string to parse + /// A JobSchedule object if successful, null otherwise + [Obsolete("Schedule should be provided as a proper object, not a string.")] + private static JobSchedule? TryParseSchedule(string scheduleString) + { + if (string.IsNullOrEmpty(scheduleString)) + return null; + + try + { + // Expecting format like "every: 30, type: minutes" + var parts = scheduleString.Split(',', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length != 2) + return null; + + var intervalPart = parts[0].Trim(); + var typePart = parts[1].Trim(); + + // Extract interval value + var intervalValue = intervalPart.Split(':', StringSplitOptions.RemoveEmptyEntries); + if (intervalValue.Length != 2 || !intervalValue[0].Trim().Equals("every", StringComparison.OrdinalIgnoreCase)) + return null; + + if (!int.TryParse(intervalValue[1].Trim(), out var interval) || interval <= 0) + return null; + + // Extract unit type + var typeParts = typePart.Split(':', StringSplitOptions.RemoveEmptyEntries); + if (typeParts.Length != 2 || !typeParts[0].Trim().Equals("type", StringComparison.OrdinalIgnoreCase)) + return null; + + var unitString = typeParts[1].Trim(); + if (!Enum.TryParse(unitString, true, out var unit)) + return null; + + return new JobSchedule { Every = interval, Type = unit }; + } + catch + { + return null; + } + } +}