diff --git a/docs/configuration.md b/docs/configuration.md index 28f3c2b8c..fe8d9e0c4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1215,7 +1215,7 @@ should be a valid crontab(5) expression describing when to run. : If set to the string "disable", no emails will be fetched automatically. - Defaults to `*/10 * * * *` or every ten minutes. + Defaults to every ten minutes, with an installation-specific minute offset. #### [`PAPERLESS_TRAIN_TASK_CRON=`](#PAPERLESS_TRAIN_TASK_CRON) {#PAPERLESS_TRAIN_TASK_CRON} diff --git a/src/paperless/settings/custom.py b/src/paperless/settings/custom.py index ef78e8dcb..2bf53a8cc 100644 --- a/src/paperless/settings/custom.py +++ b/src/paperless/settings/custom.py @@ -1,6 +1,7 @@ import datetime import logging import os +from hashlib import sha256 from pathlib import Path from typing import Any @@ -172,6 +173,15 @@ def parse_beat_schedule() -> dict: # Don't add disabled tasks to the schedule if value == "disable": continue + if ( + task["env_key"] == "PAPERLESS_EMAIL_TASK_CRON" + and task["env_key"] not in os.environ + ): + # Spread default polling across the ten-minute interval. + secret = os.environ["PAPERLESS_SECRET_KEY"].encode() + offset = int.from_bytes(sha256(secret).digest()) % 10 + minutes = ",".join(str(minute) for minute in range(offset, 60, 10)) + value = f"{minutes} * * * *" # I find https://crontab.guru/ super helpful # crontab(5) format # - five time-and-date fields diff --git a/src/paperless/tests/settings/test_custom_parsers.py b/src/paperless/tests/settings/test_custom_parsers.py index 5bf251cad..162f8b8ff 100644 --- a/src/paperless/tests/settings/test_custom_parsers.py +++ b/src/paperless/tests/settings/test_custom_parsers.py @@ -168,6 +168,7 @@ class TestParseHostingSettings: def make_expected_schedule( overrides: dict[str, dict[str, Any]] | None = None, disabled: set[str] | None = None, + email_minute: str = "6,16,26,36,46,56", ) -> dict[str, Any]: """ Build the expected schedule with optional overrides and disabled tasks. @@ -185,7 +186,7 @@ def make_expected_schedule( schedule: dict[str, Any] = { "Check all e-mail accounts": { "task": "paperless_mail.tasks.process_mail_accounts", - "schedule": crontab(minute="*/10"), + "schedule": crontab(minute=email_minute), "options": { "expires": mail_expire, "headers": {"trigger_source": "scheduled"}, @@ -266,6 +267,11 @@ class TestParseBeatSchedule: ("env", "expected"), [ pytest.param({}, make_expected_schedule(), id="defaults"), + pytest.param( + {"PAPERLESS_EMAIL_TASK_CRON": "*/10 * * * *"}, + make_expected_schedule(email_minute="*/10"), + id="email-explicit-default", + ), pytest.param( {"PAPERLESS_EMAIL_TASK_CRON": "*/50 * * * mon"}, make_expected_schedule( @@ -304,7 +310,11 @@ class TestParseBeatSchedule: expected: dict[str, Any], mocker: MockerFixture, ) -> None: - mocker.patch.dict(os.environ, env, clear=False) + mocker.patch.dict( + os.environ, + {"PAPERLESS_SECRET_KEY": "test-secret", **env}, + clear=False, + ) schedule = parse_beat_schedule() assert schedule == expected