From f331d0b3ba39321a39f575191252b00dfaeb692e Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:00:59 -0700 Subject: [PATCH] Tweakhancement: add jitter to IMAP polling schedule --- docs/configuration.md | 2 +- src/paperless/settings/custom.py | 9 +++++++++ src/paperless/tests/settings/test_custom_parsers.py | 9 ++++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 28f3c2b8c..69b7018e8 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 a random minute offset selected at startup. #### [`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..5b659ee26 100644 --- a/src/paperless/settings/custom.py +++ b/src/paperless/settings/custom.py @@ -1,6 +1,7 @@ import datetime import logging import os +import random from pathlib import Path from typing import Any @@ -172,6 +173,14 @@ 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. + offset = random.randrange(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..c3019c7e2 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 = "3,13,23,33,43,53", ) -> 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( @@ -305,6 +311,7 @@ class TestParseBeatSchedule: mocker: MockerFixture, ) -> None: mocker.patch.dict(os.environ, env, clear=False) + mocker.patch("paperless.settings.custom.random.randrange", return_value=3) schedule = parse_beat_schedule() assert schedule == expected