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
|
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
|
WORKDIR /app
|
||||||
|
|
||||||
COPY requirements.txt /app/requirements.txt
|
COPY requirements.txt /app/requirements.txt
|
||||||
RUN pip install --no-cache-dir -r /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 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/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
COPY docker/crontab.default /etc/crontabs/app-crontab
|
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||||
|
|
||||||
RUN chmod +x /usr/local/bin/run-sync.sh /usr/local/bin/run-mailer.sh /usr/local/bin/entrypoint.sh
|
|
||||||
|
|
||||||
RUN mkdir -p /app/config \
|
RUN mkdir -p /app/config \
|
||||||
&& useradd -m -u 10001 syncuser \
|
&& useradd -m -u 10001 syncuser \
|
||||||
&& chown -R syncuser:syncuser /app /etc/crontabs
|
&& chown -R syncuser:syncuser /app
|
||||||
USER syncuser
|
USER syncuser
|
||||||
|
|
||||||
HEALTHCHECK --interval=5m --timeout=10s --start-period=30s \
|
HEALTHCHECK --interval=5m --timeout=10s --start-period=30s \
|
||||||
CMD test -f /tmp/last_sync_ok || exit 1
|
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"]
|
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 "$@"
|
exec "$@"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
MAIL_HOUR="${MAIL_SEND_HOUR:-7}"
|
|
||||||
|
|
||||||
echo "Starte icloud-contacts-sync Container"
|
echo "Starte icloud-contacts-sync Container"
|
||||||
echo "Sync-Schedule: */15 * * * * (alle 15 Minuten, Delta-Sync)"
|
echo "Scheduler: Python-basiert (kein supercronic)"
|
||||||
echo "Mailer-Schedule: taeglich um ${MAIL_HOUR} Uhr (falls MAILER_ENABLED=true)"
|
|
||||||
|
|
||||||
# Crontab wird zur Laufzeit generiert, damit MAIL_SEND_HOUR konfigurierbar bleibt.
|
exec python3 /app/scheduler.py
|
||||||
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
|
|
||||||
|
|||||||
@@ -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