scheduler in python

This commit is contained in:
2026-08-04 22:54:10 +02:00
parent 03dbbfe20d
commit e5b9fb8a67
6 changed files with 120 additions and 54 deletions
+2 -26
View File
@@ -1,44 +1,20 @@
FROM python:3.12-slim
ARG SUPERCRONIC_VERSION=v0.2.48
ARG SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/${SUPERCRONIC_VERSION}/supercronic-linux-amd64
ARG SUPERCRONIC_SHA1SUM=016b7c9aebfc8d9fd9526e8ba33b191fc524485f
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& curl -fsSLO "$SUPERCRONIC_URL" \
&& echo "${SUPERCRONIC_SHA1SUM} supercronic-linux-amd64" | sha1sum -c - \
&& chmod +x supercronic-linux-amd64 \
&& mv supercronic-linux-amd64 /usr/local/bin/supercronic \
&& apt-get purge -y curl \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt
# Enthält sowohl den Sync/Mailer-Code als auch die api/-App.
# Welcher Teil tatsächlich läuft, entscheidet der Command in docker-compose.yml,
# nicht das Image selbst -- ein Image, drei mögliche Rollen (sync, mailer, api).
COPY src/ /app/
COPY docker/run-sync.sh /usr/local/bin/run-sync.sh
COPY docker/run-mailer.sh /usr/local/bin/run-mailer.sh
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
COPY docker/crontab.default /etc/crontabs/app-crontab
RUN chmod +x /usr/local/bin/run-sync.sh /usr/local/bin/run-mailer.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
RUN mkdir -p /app/config \
&& useradd -m -u 10001 syncuser \
&& chown -R syncuser:syncuser /app /etc/crontabs
&& chown -R syncuser:syncuser /app
USER syncuser
HEALTHCHECK --interval=5m --timeout=10s --start-period=30s \
CMD test -f /tmp/last_sync_ok || exit 1
# Default-Entrypoint startet den Sync+Mailer-Cron-Modus.
# Der Web/API-Service in docker-compose.yml überschreibt "command" komplett
# mit uvicorn und läuft damit im selben Image in einer anderen Rolle.
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
-2
View File
@@ -1,2 +0,0 @@
*/15 * * * * /usr/local/bin/run-sync.sh >> /proc/1/fd/1 2>> /proc/1/fd/2
0 7 * * * /usr/local/bin/run-mailer.sh >> /proc/1/fd/1 2>> /proc/1/fd/2
+2 -13
View File
@@ -7,18 +7,7 @@ if [ $# -gt 0 ]; then
exec "$@"
fi
MAIL_HOUR="${MAIL_SEND_HOUR:-7}"
echo "Starte icloud-contacts-sync Container"
echo "Sync-Schedule: */15 * * * * (alle 15 Minuten, Delta-Sync)"
echo "Mailer-Schedule: taeglich um ${MAIL_HOUR} Uhr (falls MAILER_ENABLED=true)"
echo "Scheduler: Python-basiert (kein supercronic)"
# Crontab wird zur Laufzeit generiert, damit MAIL_SEND_HOUR konfigurierbar bleibt.
cat > /etc/crontabs/app-crontab <<EOF
*/15 * * * * /usr/local/bin/run-sync.sh >> /proc/1/fd/1 2>> /proc/1/fd/2
0 ${MAIL_HOUR} * * * /usr/local/bin/run-mailer.sh >> /proc/1/fd/1 2>> /proc/1/fd/2
EOF
/usr/local/bin/run-sync.sh || echo "Initialer Sync fehlgeschlagen, Cron laeuft trotzdem weiter"
exec supercronic -json /etc/crontabs/app-crontab
exec python3 /app/scheduler.py
-6
View File
@@ -1,6 +0,0 @@
#!/bin/sh
set -e
cd /app
echo "$(date -Is) - Mailer-Lauf gestartet"
python3 /app/mailer.py
echo "$(date -Is) - Mailer-Lauf beendet"
-7
View File
@@ -1,7 +0,0 @@
#!/bin/sh
set -e
cd /app
echo "$(date -Is) - Sync-Lauf gestartet"
python3 /app/sync.py
echo "$(date -Is) - Sync-Lauf beendet"
touch /tmp/last_sync_ok
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Einfacher Cron-Scheduler fuer Sync und Mailer.
Ersetzt supercronic komplett — keine externe Abhaengigkeit noetig.
Laeuft als PID 1 im Docker-Container und plant:
- Sync: alle 15 Minuten (Delta-Sync)
- Mailer: taeglich um MAIL_SEND_HOUR Uhr (falls MAILER_ENABLED=true)
Der Scheduler faengt SIGTERM/SIGINT ab und beendet sich sauber,
damit Docker den Container korrekt stoppen kann."""
import logging
import os
import signal
import subprocess
import sys
import time
from datetime import datetime, timedelta
logging.basicConfig(
level=os.environ.get("LOG_LEVEL", "INFO"),
format="%(asctime)s [%(levelname)s] %(message)s",
)
logger = logging.getLogger("scheduler")
SYNC_INTERVAL_MINUTES = 15
MAIL_SEND_HOUR = int(os.environ.get("MAIL_SEND_HOUR", "7"))
MAILER_ENABLED = os.environ.get("MAILER_ENABLED", "false").lower() == "true"
_shutdown = False
def _handle_signal(signum, _frame):
global _shutdown
signame = signal.Signals(signum).name
logger.info("Signal %s empfangen, Scheduler faehrt herunter...", signame)
_shutdown = True
def run_script(name: str, script_path: str):
logger.info("--- %s-Lauf gestartet ---", name)
try:
result = subprocess.run(
[sys.executable, script_path],
cwd="/app",
timeout=300,
)
if result.returncode == 0:
logger.info("%s-Lauf erfolgreich beendet", name)
else:
logger.error("%s-Lauf fehlgeschlagen (exit code %d)", name, result.returncode)
except subprocess.TimeoutExpired:
logger.error("%s-Lauf hat 5 Minuten ueberschritten, abgebrochen", name)
except Exception:
logger.exception("Unerwarteter Fehler waehrend %s-Lauf", name)
def run_sync():
run_script("Sync", "/app/sync.py")
try:
with open("/tmp/last_sync_ok", "w") as f:
f.write(datetime.now().isoformat())
except OSError:
pass
def run_mailer():
if not MAILER_ENABLED:
return
run_script("Mailer", "/app/mailer.py")
def next_run_time(hour: int) -> datetime:
now = datetime.now()
target = now.replace(hour=hour, minute=0, second=0, microsecond=0)
if target <= now:
target += timedelta(days=1)
return target
def main():
global _shutdown
signal.signal(signal.SIGTERM, _handle_signal)
signal.signal(signal.SIGINT, _handle_signal)
logger.info("Scheduler gestartet")
logger.info("Sync: alle %d Minuten", SYNC_INTERVAL_MINUTES)
if MAILER_ENABLED:
logger.info("Mailer: taeglich um %d:00 Uhr", MAIL_SEND_HOUR)
else:
logger.info("Mailer: deaktiviert (MAILER_ENABLED=false)")
logger.info("Fuehre initialen Sync aus...")
run_sync()
last_sync = datetime.now()
next_mailer = next_run_time(MAIL_SEND_HOUR)
while not _shutdown:
now = datetime.now()
if now >= last_sync + timedelta(minutes=SYNC_INTERVAL_MINUTES):
run_sync()
last_sync = now
if MAILER_ENABLED and now >= next_mailer:
run_mailer()
next_mailer = next_run_time(MAIL_SEND_HOUR)
time.sleep(10)
logger.info("Scheduler beendet")
if __name__ == "__main__":
main()