33import sys
44from datetime import datetime , timedelta
55from logging import basicConfig , getLevelName , getLogger
6- from typing import Dict , List , Optional
6+ from typing import Any , Dict , List , Optional , Set
77
88import pytz
99from pycron import is_now
@@ -98,12 +98,10 @@ def get_task_delay(task: ScheduledTask) -> Optional[int]:
9898 task_time = to_tz_aware (task .time )
9999 if task_time <= now :
100100 return 0
101- one_min_ahead = (now + timedelta (minutes = 1 )).replace (second = 1 , microsecond = 0 )
102- if task_time <= one_min_ahead :
103- delay = task_time - now
104- if delay .microseconds :
105- return int (delay .total_seconds ()) + 1
106- return int (delay .total_seconds ())
101+ delay = task_time - now
102+ if delay .microseconds :
103+ return int (delay .total_seconds ()) + 1
104+ return int (delay .total_seconds ())
107105 return None
108106
109107
@@ -145,7 +143,10 @@ async def delayed_send(
145143 await scheduler .on_ready (source , task )
146144
147145
148- async def run_scheduler_loop (scheduler : TaskiqScheduler ) -> None :
146+ async def run_scheduler_loop ( # noqa: C901
147+ scheduler : TaskiqScheduler ,
148+ interval : Optional [timedelta ] = None ,
149+ ) -> None :
149150 """
150151 Runs scheduler loop.
151152
@@ -155,9 +156,18 @@ async def run_scheduler_loop(scheduler: TaskiqScheduler) -> None:
155156 :param scheduler: current scheduler.
156157 """
157158 loop = asyncio .get_event_loop ()
158- running_schedules = set ()
159+ running_schedules : Dict [str , asyncio .Task [Any ]] = {}
160+ ran_cron_jobs : Set [str ] = set ()
161+ current_minute = datetime .now (tz = pytz .UTC ).minute
159162 while True :
160- # We use this method to correctly sleep for one minute.
163+ now = datetime .now (tz = pytz .UTC )
164+ if now .minute != current_minute :
165+ current_minute = now .minute
166+ ran_cron_jobs .clear ()
167+ if interval is not None :
168+ next_run = now + interval
169+ else :
170+ next_run = (now + timedelta (minutes = 1 )).replace (second = 1 , microsecond = 0 )
161171 scheduled_tasks = await get_all_schedules (scheduler )
162172 for source , task_list in scheduled_tasks .items ():
163173 logger .debug ("Got %d schedules from source %s." , len (task_list ), source )
@@ -172,16 +182,37 @@ async def run_scheduler_loop(scheduler: TaskiqScheduler) -> None:
172182 task .schedule_id ,
173183 )
174184 continue
175- if task_delay is not None :
176- send_task = loop .create_task (
177- delayed_send (scheduler , source , task , task_delay ),
178- )
179- running_schedules .add (send_task )
180- send_task .add_done_callback (running_schedules .discard )
181- next_minute = datetime .now ().replace (second = 0 , microsecond = 0 ) + timedelta (
182- minutes = 1 ,
183- )
184- delay = next_minute - datetime .now ()
185+ # If task delay is None, we don't need to run it.
186+ if task_delay is None :
187+ continue
188+ # If task is delayed for more than next_run,
189+ # we don't need to run it, because we will
190+ # run it in the next iteration.
191+ if now + timedelta (seconds = task_delay ) >= next_run :
192+ continue
193+ # If task is already running, we don't need to run it again.
194+ if task .schedule_id in running_schedules and task_delay < 1 :
195+ continue
196+ # If task is cron job, we need to check if
197+ # we already ran it this minute.
198+ if task .cron is not None :
199+ if task .schedule_id in ran_cron_jobs :
200+ continue
201+ ran_cron_jobs .add (task .schedule_id )
202+ send_task = loop .create_task (
203+ delayed_send (scheduler , source , task , task_delay ),
204+ # We need to set the name of the task
205+ # to be able to discard its reference
206+ # after it is done.
207+ name = f"schedule_{ task .schedule_id } " ,
208+ )
209+ running_schedules [task .schedule_id ] = send_task
210+ send_task .add_done_callback (
211+ lambda task_future : running_schedules .pop (
212+ task_future .get_name ().removeprefix ("schedule_" ),
213+ ),
214+ )
215+ delay = next_run - datetime .now (tz = pytz .UTC )
185216 logger .debug (
186217 "Sleeping for %.2f seconds before getting schedules." ,
187218 delay .total_seconds (),
@@ -226,6 +257,10 @@ async def run_scheduler(args: SchedulerArgs) -> None:
226257 for source in scheduler .sources :
227258 await source .startup ()
228259
260+ interval = None
261+ if args .update_interval :
262+ interval = timedelta (seconds = args .update_interval )
263+
229264 logger .info ("Starting scheduler." )
230265 await scheduler .startup ()
231266 logger .info ("Startup completed." )
@@ -239,7 +274,7 @@ async def run_scheduler(args: SchedulerArgs) -> None:
239274 await asyncio .sleep (delay .total_seconds ())
240275 logger .info ("First run skipped. The scheduler is now running." )
241276 try :
242- await run_scheduler_loop (scheduler )
277+ await run_scheduler_loop (scheduler , interval )
243278 except asyncio .CancelledError :
244279 logger .warning ("Shutting down scheduler." )
245280 await scheduler .shutdown ()
0 commit comments