mirror of
https://github.com/skoelle/icloud-contacts-sync.git
synced 2026-09-17 15:30:24 +00:00
scheduler in python
This commit is contained in:
+2
-26
@@ -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"]
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user