|
14 | 14 | import asyncio |
15 | 15 | import threading |
16 | 16 | import time |
17 | | -from datetime import datetime |
| 17 | +from datetime import datetime, timedelta |
18 | 18 | from enum import Enum |
19 | 19 | from typing import Any, Dict, Optional |
20 | 20 |
|
|
28 | 28 | except ImportError: |
29 | 29 | HAS_CRONITER = False |
30 | 30 |
|
| 31 | +from camel.logger import get_logger |
31 | 32 | from camel.triggers.base_trigger import ( |
32 | 33 | BaseTrigger, |
33 | 34 | TriggerEvent, |
34 | 35 | TriggerState, |
35 | 36 | TriggerType, |
36 | 37 | ) |
37 | 38 |
|
| 39 | +logger = get_logger(__name__) |
| 40 | + |
38 | 41 |
|
39 | 42 | class ScheduleType(Enum): |
40 | 43 | """Enumeration of supported schedule types.""" |
@@ -254,18 +257,23 @@ def _calculate_next_run(self) -> None: |
254 | 257 | If cron parsing fails, defaults to next minute as fallback. |
255 | 258 | """ |
256 | 259 | try: |
| 260 | + now = datetime.now() |
| 261 | + |
257 | 262 | if HAS_CRONITER: |
258 | | - cron = croniter(self.cron_expression, datetime.now()) |
| 263 | + cron = croniter(self.cron_expression, now) |
259 | 264 | self.next_run_at = cron.get_next(datetime) |
260 | 265 | else: |
261 | 266 | # Basic fallback for common patterns |
262 | 267 | self._calculate_next_run_basic() |
263 | 268 | except Exception: |
264 | | - # Fallback: calculate next run in 1 minute if cron parsing fails |
265 | | - self.next_run_at = datetime.now().replace(second=0, microsecond=0) |
266 | | - self.next_run_at = self.next_run_at.replace( |
267 | | - minute=self.next_run_at.minute + 1 |
| 269 | + logger.warning( |
| 270 | + "Failed to parse cron expression '%s'. Falling back to " |
| 271 | + "next minute calculation.", |
| 272 | + self.cron_expression, |
268 | 273 | ) |
| 274 | + # Fallback: calculate next run in 1 minute if cron parsing fails |
| 275 | + base_time = now.replace(second=0, microsecond=0) |
| 276 | + self.next_run_at = base_time + timedelta(minutes=1) |
269 | 277 |
|
270 | 278 | def _calculate_next_run_basic(self) -> None: |
271 | 279 | """Basic cron calculation without croniter dependency. |
@@ -294,22 +302,20 @@ def _calculate_next_run_basic(self) -> None: |
294 | 302 | interval = int(minute[2:]) |
295 | 303 | next_minute = ((now.minute // interval) + 1) * interval |
296 | 304 | if next_minute >= 60: |
297 | | - self.next_run_at = now.replace( |
298 | | - hour=now.hour + 1, |
299 | | - minute=next_minute - 60, |
300 | | - second=0, |
301 | | - microsecond=0, |
| 305 | + # Use timedelta to properly handle hour and day rollovers |
| 306 | + # e.g. 23:58 with interval 5 -> 0:00, 0:05 ... next day |
| 307 | + base_time = now.replace(minute=0, second=0, microsecond=0) |
| 308 | + self.next_run_at = base_time + timedelta( |
| 309 | + hours=1, minutes=next_minute - 60 |
302 | 310 | ) |
303 | 311 | else: |
304 | 312 | self.next_run_at = now.replace( |
305 | 313 | minute=next_minute, second=0, microsecond=0 |
306 | 314 | ) |
307 | 315 | else: |
308 | | - # Default: next minute |
309 | | - self.next_run_at = now.replace(second=0, microsecond=0) |
310 | | - self.next_run_at = self.next_run_at.replace( |
311 | | - minute=self.next_run_at.minute + 1 |
312 | | - ) |
| 316 | + # Default: next minute - use timedelta to handle rollovers |
| 317 | + base_time = now.replace(second=0, microsecond=0) |
| 318 | + self.next_run_at = base_time + timedelta(minutes=1) |
313 | 319 |
|
314 | 320 | async def activate(self) -> bool: |
315 | 321 | """Activate the schedule trigger and start the scheduler thread. |
|
0 commit comments