fix: Convert Daily Schedule Time to UTC (#3914)

This commit is contained in:
Michael Genson 2024-07-20 16:57:02 -05:00 committed by GitHub
parent e33b62be2a
commit 5b1e827d45
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 66 additions and 33 deletions

View File

@ -4,20 +4,20 @@
### General ### General
| Variables | Default | Description | | Variables | Default | Description |
| ----------------------------- | :-------------------: | ----------------------------------------------------------------------------------- | | ----------------------------- | :-------------------: | --------------------------------------------------------------------------------------------------------- |
| PUID | 911 | UserID permissions between host OS and container | | PUID | 911 | UserID permissions between host OS and container |
| PGID | 911 | GroupID permissions between host OS and container | | PGID | 911 | GroupID permissions between host OS and container |
| DEFAULT_GROUP | Home | The default group for users | | DEFAULT_GROUP | Home | The default group for users |
| BASE_URL | http://localhost:8080 | Used for Notifications | | BASE_URL | http://localhost:8080 | Used for Notifications |
| TOKEN_TIME | 48 | The time in hours that a login/auth token is valid | | TOKEN_TIME | 48 | The time in hours that a login/auth token is valid |
| API_PORT | 9000 | The port exposed by backend API. **Do not change this if you're running in Docker** | | API_PORT | 9000 | The port exposed by backend API. **Do not change this if you're running in Docker** |
| API_DOCS | True | Turns on/off access to the API documentation locally. | | API_DOCS | True | Turns on/off access to the API documentation locally |
| TZ | UTC | Must be set to get correct date/time on the server | | TZ | UTC | Must be set to get correct date/time on the server |
| ALLOW_SIGNUP<super>\*</super> | false | Allow user sign-up without token | | ALLOW_SIGNUP<super>\*</super> | false | Allow user sign-up without token |
| LOG_CONFIG_OVERRIDE | | Override the config for logging with a custom path | | LOG_CONFIG_OVERRIDE | | Override the config for logging with a custom path |
| LOG_LEVEL | info | Logging level (e.g. critical, error, warning, info, debug, trace) | | LOG_LEVEL | info | Logging level (e.g. critical, error, warning, info, debug, trace) |
| DAILY_SCHEDULE_TIME | 23:45 | The time of day to run the daily tasks. | | DAILY_SCHEDULE_TIME | 23:45 | The time of day to run daily server tasks, in HH:MM format. Use the server's local time, *not* UTC |
<super>\*</super> Starting in v1.4.0 this was changed to default to `false` as part of a security review of the application. <super>\*</super> Starting in v1.4.0 this was changed to default to `false` as part of a security review of the application.

View File

@ -1,6 +1,10 @@
import logging
import secrets import secrets
from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import NamedTuple
from dateutil.tz import tzlocal
from pydantic import field_validator from pydantic import field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
@ -9,6 +13,11 @@ from mealie.core.settings.themes import Theme
from .db_providers import AbstractDBProvider, db_provider_factory from .db_providers import AbstractDBProvider, db_provider_factory
class ScheduleTime(NamedTuple):
hour: int
minute: int
def determine_secrets(data_dir: Path, production: bool) -> str: def determine_secrets(data_dir: Path, production: bool) -> str:
if not production: if not production:
return "shh-secret-test-key" return "shh-secret-test-key"
@ -58,6 +67,44 @@ class AppSettings(BaseSettings):
ALLOW_SIGNUP: bool = False ALLOW_SIGNUP: bool = False
DAILY_SCHEDULE_TIME: str = "23:45" DAILY_SCHEDULE_TIME: str = "23:45"
"""Local server time, in HH:MM format. See `DAILY_SCHEDULE_TIME_UTC` for the parsed UTC equivalent"""
_logger: logging.Logger | None = None
@property
def logger(self) -> logging.Logger:
if self._logger is None:
# lazy load the logger, since get_logger depends on the settings being loaded
from mealie.core.root_logger import get_logger
self._logger = get_logger()
return self._logger
@property
def DAILY_SCHEDULE_TIME_UTC(self) -> ScheduleTime:
"""The DAILY_SCHEDULE_TIME in UTC, parsed into hours and minutes"""
# parse DAILY_SCHEDULE_TIME into hours and minutes
try:
hour_str, minute_str = self.DAILY_SCHEDULE_TIME.split(":")
local_hour = int(hour_str)
local_minute = int(minute_str)
except ValueError:
local_hour = 23
local_minute = 45
self.logger.exception(
f"Unable to parse {self.DAILY_SCHEDULE_TIME=} as HH:MM; defaulting to {local_hour}:{local_minute}"
)
# DAILY_SCHEDULE_TIME is in local time, so we convert it to UTC
local_tz = tzlocal()
now = datetime.now(local_tz)
local_time = now.replace(hour=local_hour, minute=local_minute)
utc_time = local_time.astimezone(timezone.utc)
self.logger.debug(f"Local time: {local_hour}:{local_minute} | UTC time: {utc_time.hour}:{utc_time.minute}")
return ScheduleTime(utc_time.hour, utc_time.minute)
# =============================================== # ===============================================
# Security Configuration # Security Configuration

View File

@ -29,20 +29,12 @@ class SchedulerService:
async def schedule_daily(): async def schedule_daily():
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
daily_schedule_time = get_app_settings().DAILY_SCHEDULE_TIME daily_schedule_time = get_app_settings().DAILY_SCHEDULE_TIME_UTC
logger.debug( logger.debug(f"Current time is {now} and DAILY_SCHEDULE_TIME (in UTC) is {daily_schedule_time}")
"Current time is %s and DAILY_SCHEDULE_TIME is %s",
str(now),
daily_schedule_time,
)
try:
hour_target, minute_target = _parse_daily_schedule_time(daily_schedule_time)
except Exception:
logger.exception(f"Unable to parse {daily_schedule_time=}")
hour_target = 23
minute_target = 45
next_schedule = now.replace(hour=hour_target, minute=minute_target, second=0, microsecond=0) next_schedule = now.replace(
hour=daily_schedule_time.hour, minute=daily_schedule_time.minute, second=0, microsecond=0
)
delta = next_schedule - now delta = next_schedule - now
if delta < timedelta(0): if delta < timedelta(0):
next_schedule = next_schedule + timedelta(days=1) next_schedule = next_schedule + timedelta(days=1)
@ -61,12 +53,6 @@ async def schedule_daily():
await run_daily() await run_daily()
def _parse_daily_schedule_time(time):
hour_target = int(time.split(":")[0])
minute_target = int(time.split(":")[1])
return hour_target, minute_target
def _scheduled_task_wrapper(callable): def _scheduled_task_wrapper(callable):
try: try:
callable() callable()