1+
2+ /// <summary>
3+ /// The centralized background service that monitors the clock and executes
4+ /// all registered ICronScheduledTask instances based on their Schedule property.
5+ /// </summary>
6+ public class CronTaskRunnerService : BackgroundService
7+ {
8+ private readonly IServiceProvider _serviceProvider ;
9+ private readonly ILogger < CronTaskRunnerService > _logger ;
10+
11+ // The runner checks the schedule every 60 seconds (or less if needed).
12+ private static readonly TimeSpan CheckInterval = TimeSpan . FromSeconds ( 60 ) ;
13+
14+ public CronTaskRunnerService (
15+ ILogger < CronTaskRunnerService > logger ,
16+ IServiceProvider serviceProvider )
17+ {
18+ _logger = logger ;
19+ _serviceProvider = serviceProvider ;
20+ }
21+
22+ /// <summary>
23+ /// Logic to check if a task's schedule matches the current minute.
24+ /// Uses a custom format: [DayOfWeek|Daily]@[HH:MM]
25+ /// </summary>
26+ private static bool IsScheduleDue ( string schedule , DateTime now )
27+ {
28+ // Example: "Sunday@00:00"
29+ if ( string . IsNullOrWhiteSpace ( schedule ) || ! schedule . Contains ( '@' ) ) return false ;
30+
31+ var parts = schedule . Split ( '@' ) ;
32+ var dayPart = parts [ 0 ] . Trim ( ) ;
33+ var timePart = parts [ 1 ] . Trim ( ) ;
34+
35+ // 1. Check Time (HH:MM)
36+ // Check if the current hour and minute match the scheduled time
37+ if ( ! TimeSpan . TryParseExact ( timePart , "hh\\ :mm" , null , out TimeSpan scheduledTime ) )
38+ {
39+ return false ;
40+ }
41+
42+ // Only fire if the current UTC hour and minute match the scheduled time
43+ if ( now . Hour != scheduledTime . Hours || now . Minute != scheduledTime . Minutes )
44+ {
45+ return false ;
46+ }
47+
48+ // 2. Check Day (Daily or Specific DayOfWeek)
49+ if ( dayPart . Equals ( "Daily" , StringComparison . OrdinalIgnoreCase ) )
50+ {
51+ return true ; // Scheduled to run every day at this time
52+ }
53+
54+ // Check if the current DayOfWeek matches the scheduled day
55+ if ( Enum . TryParse < DayOfWeek > ( dayPart , true , out DayOfWeek scheduledDay ) )
56+ {
57+ return now . DayOfWeek == scheduledDay ;
58+ }
59+
60+ return false ;
61+ }
62+
63+ protected override async Task ExecuteAsync ( CancellationToken stoppingToken )
64+ {
65+ _logger . LogInformation ( "Cron Task Runner Service started. Checking schedule every minute." ) ;
66+
67+ while ( ! stoppingToken . IsCancellationRequested )
68+ {
69+ // Get the current time in UTC, rounded down to the nearest minute.
70+ var now = DateTime . UtcNow . AddSeconds ( - DateTime . UtcNow . Second ) ;
71+ _logger . LogDebug ( $ "Checking schedules for time: { now : yyyy-MM-dd HH:mm} UTC") ;
72+
73+ try
74+ {
75+ // Must create a scope for each execution cycle
76+ using ( var scope = _serviceProvider . CreateScope ( ) )
77+ {
78+ // Resolve ALL services registered under the ICronScheduledTask contract.
79+ IEnumerable < ICronScheduledTask > scheduledTasks =
80+ scope . ServiceProvider . GetServices < ICronScheduledTask > ( ) ;
81+
82+ var tasksToRun = scheduledTasks
83+ . Where ( task => IsScheduleDue ( task . Schedule , now ) )
84+ . ToList ( ) ;
85+
86+ if ( tasksToRun . Any ( ) )
87+ {
88+ _logger . LogInformation ( $ "Found { tasksToRun . Count } tasks due now. Executing concurrently.") ;
89+
90+ var executionTasks = tasksToRun
91+ . Select ( task => task . ExecuteAsync ( ) . ContinueWith ( t =>
92+ {
93+ if ( t . IsFaulted )
94+ {
95+ _logger . LogError ( t . Exception , $ "Task '{ task . Name } ' failed to execute.") ;
96+ }
97+ } , TaskContinuationOptions . ExecuteSynchronously ) ) // Ensure logging is safe
98+ . ToList ( ) ;
99+
100+ await Task . WhenAll ( executionTasks ) ;
101+ _logger . LogInformation ( "Concurrent execution completed for this cycle." ) ;
102+ }
103+ }
104+ }
105+ catch ( Exception ex )
106+ {
107+ _logger . LogError ( ex , "An unhandled error occurred during the Cron execution cycle." ) ;
108+ }
109+
110+ // Wait for the next interval check.
111+ await Task . Delay ( CheckInterval , stoppingToken ) ;
112+ }
113+
114+ _logger . LogInformation ( "Cron Task Runner Service stopping." ) ;
115+ }
116+ }
0 commit comments