Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions src/RandomAPI/APIServices/ServiceInterfaces/ICronScheduledTask.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@

/// <summary>
/// Contract for a scheduled background task that includes its execution logic and its specific schedule.
/// </summary>
public interface ICronScheduledTask
{
/// <summary>
/// A unique name for logging and identification.
/// </summary>
string Name { get; }

/// <summary>
/// The schedule definition for the task.
/// Format: [DayOfWeek|Daily]@[HH:MM]
/// Examples: "Daily@08:30", "Monday@08:30", "Sunday@00:00"
/// </summary>
string Schedule { get; }

/// <summary>
/// Executes the scheduled job logic.
/// </summary>
Task ExecuteAsync();
}


/// <summary>
/// A daily scheduled task for routine maintenance (e.g., database cleanup, log rotation).
/// Runs every day at 04:00 AM UTC.
/// </summary>
public class DailyMaintenanceTask : ICronScheduledTask
{
private readonly ILogger<DailyMaintenanceTask> _logger;

public DailyMaintenanceTask(ILogger<DailyMaintenanceTask> logger)
{
_logger = logger;
}

// --- ICronScheduledTask Implementation ---
public string Name => "Daily Maintenance & Cleanup";
public string Schedule => "Daily@04:00"; // Run every day at 4:00 AM UTC
// ----------------------------------------

public async Task ExecuteAsync()
{
_logger.LogInformation("Executing Daily Maintenance Task: Running routine cleanup.");

// --- Actual Scheduled Logic ---
await Task.Delay(100); // Simulate asynchronous database cleanup or file deletion
// Note: For a real cleanup task, you would inject and use a repository here.
// ------------------------------

_logger.LogInformation("Daily Maintenance Task completed successfully.");
}
}

/// <summary>
/// Scheduled task for generating a report every Monday morning.
/// </summary>
public class BiWeeklyReportTaskMonday : ICronScheduledTask
{
private readonly ILogger<BiWeeklyReportTaskMonday> _logger;

public BiWeeklyReportTaskMonday(ILogger<BiWeeklyReportTaskMonday> logger)
{
_logger = logger;
}

public string Name => "Bi-Weekly Report (Monday)";
public string Schedule => "Monday@08:30"; // Run every Monday at 8:30 AM UTC

public async Task ExecuteAsync()
{
_logger.LogInformation("Executing Monday Report Task: Compiling weekly progress report.");
await Task.Delay(100); // Simulate report generation
_logger.LogInformation("Monday Report Task completed successfully.");
}
}

/// <summary>
/// Scheduled task for generating a report every Friday morning.
/// </summary>
public class BiWeeklyReportTaskFriday : ICronScheduledTask
{
private readonly ILogger<BiWeeklyReportTaskFriday> _logger;

public BiWeeklyReportTaskFriday(ILogger<BiWeeklyReportTaskFriday> logger)
{
_logger = logger;
}

public string Name => "Bi-Weekly Report (Friday)";
public string Schedule => "Friday@08:30"; // Run every Friday at 8:30 AM UTC

public async Task ExecuteAsync()
{
_logger.LogInformation("Executing Friday Report Task: Compiling end-of-week summary report.");
await Task.Delay(100); // Simulate report generation
_logger.LogInformation("Friday Report Task completed successfully.");
}
}
116 changes: 116 additions & 0 deletions src/RandomAPI/APIServices/Services/CronTaskRunnerService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@

/// <summary>
/// The centralized background service that monitors the clock and executes
/// all registered ICronScheduledTask instances based on their Schedule property.
/// </summary>
public class CronTaskRunnerService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<CronTaskRunnerService> _logger;

// The runner checks the schedule every 60 seconds (or less if needed).
private static readonly TimeSpan CheckInterval = TimeSpan.FromSeconds(60);

public CronTaskRunnerService(
ILogger<CronTaskRunnerService> logger,
IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}

/// <summary>
/// Logic to check if a task's schedule matches the current minute.
/// Uses a custom format: [DayOfWeek|Daily]@[HH:MM]
/// </summary>
private static bool IsScheduleDue(string schedule, DateTime now)
{
// Example: "Sunday@00:00"
if (string.IsNullOrWhiteSpace(schedule) || !schedule.Contains('@')) return false;

var parts = schedule.Split('@');
var dayPart = parts[0].Trim();
var timePart = parts[1].Trim();

// 1. Check Time (HH:MM)
// Check if the current hour and minute match the scheduled time
if (!TimeSpan.TryParseExact(timePart, "hh\\:mm", null, out TimeSpan scheduledTime))
{
return false;
}

// Only fire if the current UTC hour and minute match the scheduled time
if (now.Hour != scheduledTime.Hours || now.Minute != scheduledTime.Minutes)
{
return false;
}

// 2. Check Day (Daily or Specific DayOfWeek)
if (dayPart.Equals("Daily", StringComparison.OrdinalIgnoreCase))
{
return true; // Scheduled to run every day at this time
}

// Check if the current DayOfWeek matches the scheduled day
if (Enum.TryParse<DayOfWeek>(dayPart, true, out DayOfWeek scheduledDay))
{
return now.DayOfWeek == scheduledDay;
}

return false;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Cron Task Runner Service started. Checking schedule every minute.");

while (!stoppingToken.IsCancellationRequested)
{
// Get the current time in UTC, rounded down to the nearest minute.
var now = DateTime.UtcNow.AddSeconds(-DateTime.UtcNow.Second);
_logger.LogDebug($"Checking schedules for time: {now:yyyy-MM-dd HH:mm} UTC");

try
{
// Must create a scope for each execution cycle
using (var scope = _serviceProvider.CreateScope())
{
// Resolve ALL services registered under the ICronScheduledTask contract.
IEnumerable<ICronScheduledTask> scheduledTasks =
scope.ServiceProvider.GetServices<ICronScheduledTask>();

var tasksToRun = scheduledTasks
.Where(task => IsScheduleDue(task.Schedule, now))
.ToList();

if (tasksToRun.Any())
{
_logger.LogInformation($"Found {tasksToRun.Count} tasks due now. Executing concurrently.");

var executionTasks = tasksToRun
.Select(task => task.ExecuteAsync().ContinueWith(t =>
{
if (t.IsFaulted)
{
_logger.LogError(t.Exception, $"Task '{task.Name}' failed to execute.");
}
}, TaskContinuationOptions.ExecuteSynchronously)) // Ensure logging is safe
.ToList();

await Task.WhenAll(executionTasks);
_logger.LogInformation("Concurrent execution completed for this cycle.");
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "An unhandled error occurred during the Cron execution cycle.");
}

// Wait for the next interval check.
await Task.Delay(CheckInterval, stoppingToken);
}

_logger.LogInformation("Cron Task Runner Service stopping.");
}
}
3 changes: 1 addition & 2 deletions src/RandomAPI/APIServices/Services/WebhookActionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,13 @@ public async Task<IActionResult> HandleRegisterActionAsync([FromBody] string url

public async Task<IActionResult> HandleUnregisterActionAsync([FromBody] string url)
{
string safeUrlForLog = url;
if (string.IsNullOrWhiteSpace(url))
{

return new BadRequestObjectResult("URL cannot be empty.");
}

safeUrlForLog = SanitizeURL(ref url);
string safeUrlForLog = SanitizeURL(ref url);
var removed = await base.RemoveListenerAsync(url);
if (!removed)
{
Expand Down
Loading
Loading