intervall

This commit is contained in:
Bastian Wagner
2026-08-15 21:44:13 +02:00
parent f7b04337ce
commit ff5dab6f0d
10 changed files with 438 additions and 17 deletions

View File

@@ -1,22 +1,80 @@
import asyncio
import logging
from datetime import datetime, timedelta, timezone
from typing import Callable
from app.db.repositories import SchedulerSettingsRepository
from app.sync.manager import SyncAlreadyRunning
logger = logging.getLogger(__name__)
def is_daytime(hour: int, day_start_hour: int, night_start_hour: int) -> bool:
"""True if `hour` falls within [day_start_hour, night_start_hour),
handling a day period that wraps past midnight (e.g. day_start=20,
night_start=6). Equal start hours are treated as "always day"."""
if day_start_hour == night_start_hour:
return True
if day_start_hour < night_start_hour:
return day_start_hour <= hour < night_start_hour
return hour >= day_start_hour or hour < night_start_hour
class DayNightIntervalProvider:
"""Reads the admin-configurable day/night interval from the DB on every
call, so a change made via the UI takes effect on the scheduler's very
next tick without an app restart."""
def __init__(self, session_factory, *, default_minutes: int, now: Callable[[], datetime] | None = None) -> None:
self.session_factory = session_factory
self.default_minutes = default_minutes
self._now = now or datetime.now
def __call__(self) -> float:
with self.session_factory() as session:
row = SchedulerSettingsRepository(session).get_or_create(default_minutes=self.default_minutes)
day_start_hour = row.day_start_hour
night_start_hour = row.night_start_hour
day_interval_minutes = row.day_interval_minutes
night_interval_minutes = row.night_interval_minutes
hour = self._now().hour
minutes = (
day_interval_minutes
if is_daytime(hour, day_start_hour, night_start_hour)
else night_interval_minutes
)
return minutes * 60
class SyncScheduler:
def __init__(self, manager, *, interval_seconds: float) -> None:
def __init__(
self,
manager,
*,
interval_seconds: float,
interval_provider: Callable[[], float] | None = None,
) -> None:
self.manager = manager
self.interval_seconds = interval_seconds
self.interval_provider = interval_provider
self._task: asyncio.Task | None = None
self._stop = asyncio.Event()
self.last_tick = None
self.next_tick = None
async def run_once(self) -> None:
def _current_interval(self) -> float:
if self.interval_provider is None:
return self.interval_seconds
try:
return float(self.interval_provider())
except Exception:
logger.warning("interval_provider failed, falling back to default interval", exc_info=True)
return self.interval_seconds
async def run_once(self, interval: float | None = None) -> None:
if interval is None:
interval = self._current_interval()
self.last_tick = datetime.now(timezone.utc)
try:
results = await self.manager.sync_all_enabled()
@@ -36,13 +94,14 @@ class SyncScheduler:
except Exception:
logger.exception("sync_all_enabled failed during scheduled tick")
finally:
self.next_tick = datetime.now(timezone.utc) + timedelta(seconds=self.interval_seconds)
self.next_tick = datetime.now(timezone.utc) + timedelta(seconds=interval)
async def _run(self) -> None:
while not self._stop.is_set():
await self.run_once()
interval = self._current_interval()
await self.run_once(interval)
try:
await asyncio.wait_for(self._stop.wait(), timeout=self.interval_seconds)
await asyncio.wait_for(self._stop.wait(), timeout=interval)
except TimeoutError:
pass