Replace task scheduler utcnow calls (#1456)

Co-authored-by: ghreprimand <203024559+ghreprimand@users.noreply.github.com>
This commit is contained in:
ghreprimand
2026-06-03 00:14:30 -05:00
committed by GitHub
parent 4f03f5ccdd
commit 41d2767b30
2 changed files with 72 additions and 55 deletions

View File

@@ -6,12 +6,17 @@ import logging
import re
import time
import uuid
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import Any, Awaitable, Callable, Dict, Tuple
logger = logging.getLogger(__name__)
def _utcnow() -> datetime:
"""Return naive UTC for task DB fields without using deprecated APIs."""
return datetime.now(timezone.utc).replace(tzinfo=None)
# ── Shared TTL cache (singleflight) ────────────────────────────────────────
# Multiple scheduled tasks firing in the same minute often need the same
# external data (Miniflux unreads, MCP tool snapshots, etc.). This cache
@@ -73,7 +78,6 @@ def compute_next_run(schedule: str, scheduled_time: str,
the legacy behavior (`scheduled_time` interpreted as naive-UTC wall clock)
is preserved so existing tasks don't shift.
"""
from datetime import timezone
try:
from zoneinfo import ZoneInfo
except ImportError:
@@ -89,12 +93,12 @@ def compute_next_run(schedule: str, scheduled_time: str,
# "now" used for comparisons. When tz is set we work entirely in local tz
# and convert to UTC at the end. Otherwise we use naive UTC (legacy).
if tz is not None:
now_utc = after or datetime.utcnow()
now_utc = after or _utcnow()
if now_utc.tzinfo is None:
now_utc = now_utc.replace(tzinfo=timezone.utc)
now = now_utc.astimezone(tz)
else:
now = after or datetime.utcnow()
now = after or _utcnow()
def _to_utc_naive(dt: datetime) -> datetime:
"""Convert a tz-aware datetime to naive UTC for DB storage."""
@@ -284,7 +288,7 @@ class TaskScheduler:
run.status = "aborted"
run.error = message
run.result = run.result or message
run.finished_at = datetime.utcnow()
run.finished_at = _utcnow()
db.commit()
return True
finally:
@@ -305,7 +309,7 @@ class TaskScheduler:
"task_id": task_id,
"owner": owner,
"body": (body[:500] + "") if body and len(body) > 500 else body,
"timestamp": datetime.utcnow().isoformat() + "Z",
"timestamp": _utcnow().isoformat() + "Z",
})
# Cap at 50 to avoid unbounded growth
if len(self._pending_notifications) > 50:
@@ -351,7 +355,7 @@ class TaskScheduler:
TaskRun.status.in_(("running", "queued"))
).all()
if stale:
now = datetime.utcnow()
now = _utcnow()
for r in stale:
old_status = r.status or "running"
r.status = "aborted"
@@ -372,7 +376,7 @@ class TaskScheduler:
from core.database import SessionLocal as _SL, ScheduledTask as _ST
db = _SL()
try:
now = datetime.utcnow()
now = _utcnow()
overdue = db.query(_ST).filter(
_ST.status == "active",
_ST.next_run.isnot(None),
@@ -574,7 +578,7 @@ class TaskScheduler:
_ST.next_run.isnot(None),
).order_by(_ST.next_run.asc()).first()
if next_run and next_run[0]:
delta = (next_run[0] - datetime.utcnow()).total_seconds()
delta = (next_run[0] - _utcnow()).total_seconds()
sleep_for = max(1.0, min(60.0, delta))
finally:
_db.close()
@@ -586,7 +590,7 @@ class TaskScheduler:
from core.database import SessionLocal, ScheduledTask
db = SessionLocal()
try:
now = datetime.utcnow()
now = _utcnow()
async with self._executing_lock:
# Snapshot under the lock so we don't race with mid-iteration adds.
executing_snapshot = set(self._executing)
@@ -622,7 +626,7 @@ class TaskScheduler:
run = TaskRun(
id=run_id,
task_id=task_id,
started_at=datetime.utcnow(),
started_at=_utcnow(),
status="queued",
result="Queued — waiting for a free slot…",
)
@@ -665,7 +669,7 @@ class TaskScheduler:
stale = db.query(TaskRun).filter(TaskRun.id == run_id).first()
if stale and stale.status == "queued":
stale.status = "skipped"
stale.finished_at = datetime.utcnow()
stale.finished_at = _utcnow()
stale.error = f"Task no longer active (status={task.status if task else 'deleted'})"
db.commit()
return
@@ -676,7 +680,7 @@ class TaskScheduler:
run = db.query(TaskRun).filter(TaskRun.id == run_id).first()
if run:
run.status = "running"
run.started_at = datetime.utcnow()
run.started_at = _utcnow()
run.result = "Starting…"
db.commit()
else:
@@ -685,7 +689,7 @@ class TaskScheduler:
run = TaskRun(
id=run_id,
task_id=task.id,
started_at=datetime.utcnow(),
started_at=_utcnow(),
status="running",
result="Starting…",
)
@@ -727,7 +731,7 @@ class TaskScheduler:
delay_seconds = int(getattr(defer, "delay_seconds", 20 * 60) or (20 * 60))
if count > 2:
delay_seconds = max(delay_seconds, 40 * 60)
when = datetime.utcnow() + timedelta(seconds=delay_seconds)
when = _utcnow() + timedelta(seconds=delay_seconds)
logger.info(
"Task '%s' deferred for %ss after %s quiet-window hit(s): %s",
task.name, delay_seconds, count, defer,
@@ -745,13 +749,13 @@ class TaskScheduler:
run_obj.status = "aborted"
run_obj.error = "Stopped by user"
run_obj.result = run_obj.result or "Stopped by user"
run_obj.finished_at = datetime.utcnow()
task.last_run = datetime.utcnow()
run_obj.finished_at = _utcnow()
task.last_run = _utcnow()
if (task.trigger_type or "schedule") == "schedule":
task.next_run = compute_next_run(
task.schedule, task.scheduled_time,
task.scheduled_day, task.scheduled_date,
after=datetime.utcnow(),
after=_utcnow(),
cron_expression=task.cron_expression,
tz_name=_resolve_task_timezone(db, task),
)
@@ -768,13 +772,13 @@ class TaskScheduler:
logger.info(f"Task '{task.name}' no-op: {noop}")
run.status = "skipped"
run.result = str(noop)
run.finished_at = datetime.utcnow()
task.last_run = datetime.utcnow()
run.finished_at = _utcnow()
task.last_run = _utcnow()
if (task.trigger_type or "schedule") == "schedule":
task.next_run = compute_next_run(
task.schedule, task.scheduled_time,
task.scheduled_day, task.scheduled_date,
after=datetime.utcnow(),
after=_utcnow(),
cron_expression=task.cron_expression,
tz_name=_resolve_task_timezone(db, task),
)
@@ -783,10 +787,10 @@ class TaskScheduler:
db.commit()
return
run.finished_at = datetime.utcnow()
run.finished_at = _utcnow()
# Update task
task.last_run = datetime.utcnow()
task.last_run = _utcnow()
task.run_count = (task.run_count or 0) + 1
self._task_defer_counts.pop(task_id, None)
@@ -795,7 +799,7 @@ class TaskScheduler:
task.next_run = compute_next_run(
task.schedule, task.scheduled_time,
task.scheduled_day, task.scheduled_date,
after=datetime.utcnow(),
after=_utcnow(),
cron_expression=task.cron_expression,
tz_name=_resolve_task_timezone(db, task),
)
@@ -869,17 +873,17 @@ class TaskScheduler:
if run_obj and run_obj.status in ("running", "success"):
run_obj.status = "error"
run_obj.error = err_text[:2000]
run_obj.finished_at = datetime.utcnow()
run_obj.finished_at = _utcnow()
# Advance next_run even on failure so a broken task doesn't
# busy-loop the scheduler every tick with a stale past date.
task_obj = db.query(ScheduledTask).filter(ScheduledTask.id == task_id).first()
if task_obj and (task_obj.trigger_type or "schedule") == "schedule":
task_obj.last_run = datetime.utcnow()
task_obj.last_run = _utcnow()
try:
task_obj.next_run = compute_next_run(
task_obj.schedule, task_obj.scheduled_time,
task_obj.scheduled_day, task_obj.scheduled_date,
after=datetime.utcnow(),
after=_utcnow(),
cron_expression=task_obj.cron_expression,
tz_name=_resolve_task_timezone(db, task_obj),
)
@@ -904,13 +908,13 @@ class TaskScheduler:
if _r and _r.status in ("running", "queued"):
_r.status = "aborted"
_r.error = f"commit_failed: {type(commit_err).__name__}: {commit_err}"[:2000]
_r.finished_at = datetime.utcnow()
_r.finished_at = _utcnow()
_t = _recover_db.query(ScheduledTask).filter(ScheduledTask.id == task_id).first()
if _t and (_t.trigger_type or "schedule") == "schedule":
# Push next_run forward 5min as a safe stall so the
# scheduler doesn't immediately re-dispatch.
_t.next_run = datetime.utcnow() + _td(minutes=5)
_t.last_run = datetime.utcnow()
_t.next_run = _utcnow() + _td(minutes=5)
_t.last_run = _utcnow()
_recover_db.commit()
except Exception as recover_err:
logger.error("Task %s recovery commit ALSO failed: %s", task_id, recover_err)
@@ -1087,14 +1091,14 @@ class TaskScheduler:
if tz_name:
from zoneinfo import ZoneInfo
from datetime import timezone, timedelta
now = datetime.utcnow().replace(tzinfo=timezone.utc).astimezone(ZoneInfo(tz_name))
now = _utcnow().replace(tzinfo=timezone.utc).astimezone(ZoneInfo(tz_name))
else:
from datetime import timedelta
now = datetime.utcnow()
now = _utcnow()
time_str = now.strftime("%A, %B %d %Y, %H:%M")
except Exception:
from datetime import timedelta
now = datetime.utcnow()
now = _utcnow()
time_str = now.strftime("%H:%M UTC")
raw = {}
@@ -1297,8 +1301,8 @@ class TaskScheduler:
endpoint_url=endpoint_url,
model=model,
owner=task.owner,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
created_at=_utcnow(),
updated_at=_utcnow(),
)
db.add(sess)
task.session_id = session_id
@@ -1327,12 +1331,12 @@ class TaskScheduler:
if tz_name:
from zoneinfo import ZoneInfo
from datetime import timezone
now_local = datetime.utcnow().replace(tzinfo=timezone.utc).astimezone(ZoneInfo(tz_name))
now_local = _utcnow().replace(tzinfo=timezone.utc).astimezone(ZoneInfo(tz_name))
time_str = now_local.strftime("%A, %B %d %Y, %H:%M %Z")
else:
time_str = datetime.utcnow().strftime("%A, %B %d %Y, %H:%M UTC")
time_str = _utcnow().strftime("%A, %B %d %Y, %H:%M UTC")
except Exception:
time_str = datetime.utcnow().strftime("%A, %B %d %Y, %H:%M UTC")
time_str = _utcnow().strftime("%A, %B %d %Y, %H:%M UTC")
system_prompt = f"Current time: {time_str}\n\n{system_prompt}"
# Compute tool filter from CrewMember.enabled_tools if set
@@ -1439,8 +1443,8 @@ class TaskScheduler:
endpoint_url=endpoint_url or "",
model=model_name or "",
owner=task.owner,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
created_at=_utcnow(),
updated_at=_utcnow(),
)
db.add(sess)
task.session_id = session_id
@@ -1463,7 +1467,7 @@ class TaskScheduler:
session_id=session_id,
role="user",
content=user_content,
timestamp=datetime.utcnow(),
timestamp=_utcnow(),
meta_data=msg_meta,
)
assistant_msg = ChatMessage(
@@ -1471,7 +1475,7 @@ class TaskScheduler:
session_id=session_id,
role="assistant",
content=result or "",
timestamp=datetime.utcnow(),
timestamp=_utcnow(),
meta_data=msg_meta,
)
db.add(user_msg)
@@ -1719,8 +1723,8 @@ class TaskScheduler:
endpoint_url=endpoint_url,
model=model,
owner=task.owner,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
created_at=_utcnow(),
updated_at=_utcnow(),
)
db.add(sess)
task.session_id = session_id
@@ -2001,7 +2005,7 @@ class TaskScheduler:
task.cron_expression = defs["cron_expression"]
task.next_run = compute_next_run(
defs["schedule"], defs["scheduled_time"], None, None,
after=datetime.utcnow(), cron_expression=defs["cron_expression"],
after=_utcnow(), cron_expression=defs["cron_expression"],
tz_name=_resolve_task_timezone(db, task),
)
normalized = True
@@ -2036,7 +2040,7 @@ class TaskScheduler:
task.next_run = compute_next_run(
task.schedule, task.scheduled_time,
task.scheduled_day, task.scheduled_date,
after=datetime.utcnow(), cron_expression=task.cron_expression,
after=_utcnow(), cron_expression=task.cron_expression,
tz_name=_resolve_task_timezone(db, task),
)
# Built-in housekeeping/action jobs should not create browser
@@ -2051,7 +2055,7 @@ class TaskScheduler:
if trigger_type == "schedule":
next_run = compute_next_run(
defs["schedule"], defs["scheduled_time"], None, None,
after=datetime.utcnow(), cron_expression=defs["cron_expression"],
after=_utcnow(), cron_expression=defs["cron_expression"],
)
ships_paused = bool(defs.get("ship_paused"))
task = ScheduledTask(
@@ -2188,8 +2192,8 @@ class TaskScheduler:
is_important=True,
mode="agent",
folder="Assistant",
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
created_at=_utcnow(),
updated_at=_utcnow(),
)
db.add(sess)
db.flush()