diff --git a/backend/Dockerfile b/backend/Dockerfile index 9e4bdfc5..3ba994e1 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -41,7 +41,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 ENV DEBIAN_FRONTEND=noninteractive -# Install runtime dependencies (including GDAL and cron) +# Install runtime dependencies (including GDAL) RUN apt-get update && apt-get install -y --no-install-recommends \ postgresql-client \ gdal-bin \ @@ -49,7 +49,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ nginx \ memcached \ supervisor \ - cron \ && apt-get clean && rm -rf /var/lib/apt/lists/* # Copy Python packages from builder @@ -61,12 +60,8 @@ COPY ./server /code/ COPY ./nginx.conf /etc/nginx/nginx.conf COPY ./supervisord.conf /etc/supervisor/conf.d/supervisord.conf COPY ./entrypoint.sh /code/entrypoint.sh -COPY ./crontab /etc/cron.d/adventurelog-cron RUN chmod +x /code/entrypoint.sh \ - && mkdir -p /code/static /code/media \ - && chmod 0644 /etc/cron.d/adventurelog-cron \ - && crontab /etc/cron.d/adventurelog-cron \ - && touch /var/log/cron.log + && mkdir -p /code/static /code/media # Collect static files RUN python3 manage.py collectstatic --noinput --verbosity 2 @@ -74,5 +69,8 @@ RUN python3 manage.py collectstatic --noinput --verbosity 2 # Expose ports EXPOSE 80 8000 -# Start Supervisor +# Start with an entrypoint that runs init tasks then starts supervisord +ENTRYPOINT ["/code/entrypoint.sh"] + +# Start supervisord to manage processes CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] diff --git a/backend/crontab b/backend/crontab deleted file mode 100644 index 50178834..00000000 --- a/backend/crontab +++ /dev/null @@ -1,8 +0,0 @@ -# AdventureLog cron jobs -SHELL=/bin/sh -PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin - -# Run sync_visited_regions daily at midnight -0 0 * * * cd /code && /usr/local/bin/python3 manage.py sync_visited_regions >> /var/log/cron.log 2>&1 - -# Empty line required at end of cron file diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh index 1031cb10..cb573de9 100644 --- a/backend/entrypoint.sh +++ b/backend/entrypoint.sh @@ -84,8 +84,4 @@ fi cat /code/adventurelog.txt -# Start Gunicorn in foreground -exec gunicorn main.wsgi:application \ - --bind [::]:8000 \ - --workers 2 \ - --timeout 120 +exec "$@" \ No newline at end of file diff --git a/backend/server/run_periodic_sync.py b/backend/server/run_periodic_sync.py new file mode 100644 index 00000000..adb01210 --- /dev/null +++ b/backend/server/run_periodic_sync.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +""" +Periodic sync runner for AdventureLog. +Runs sync_visited_regions management command every 60 seconds. +Managed by supervisord to ensure it inherits container environment variables. +""" +import os +import sys +import time +import logging +import signal +import threading +from datetime import datetime, timedelta +from pathlib import Path + +# Setup Django +sys.path.insert(0, str(Path(__file__).parent)) +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings') + +import django +django.setup() + +from django.core.management import call_command + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s [%(levelname)s] %(message)s', + handlers=[logging.StreamHandler(sys.stdout)] +) +logger = logging.getLogger(__name__) + +INTERVAL_SECONDS = 60 + +# Event used to signal shutdown from signal handlers +_stop_event = threading.Event() + + +def _seconds_until_next_midnight() -> float: + """Return number of seconds until the next local midnight.""" + now = datetime.now() + next_midnight = (now + timedelta(days=1)).replace( + hour=0, minute=0, second=0, microsecond=0 + ) + return (next_midnight - now).total_seconds() + + +def _handle_termination(signum, frame): + """Signal handler for SIGTERM and SIGINT: request graceful shutdown.""" + logger.info(f"Received signal {signum}; shutting down gracefully...") + _stop_event.set() + + +def run_sync(): + """Run the sync_visited_regions command.""" + try: + logger.info("Running sync_visited_regions...") + call_command('sync_visited_regions') + logger.info("Sync completed successfully") + except Exception as e: + logger.error(f"Sync failed: {e}", exc_info=True) + + +def main(): + """Main loop - run sync every INTERVAL_SECONDS.""" + logger.info(f"Starting periodic sync (interval: {INTERVAL_SECONDS}s)") + + # Install signal handlers so supervisord (or other process managers) + # can request a clean shutdown using SIGTERM/SIGINT. + signal.signal(signal.SIGTERM, _handle_termination) + signal.signal(signal.SIGINT, _handle_termination) + + try: + while not _stop_event.is_set(): + # Wait until the next local midnight (or until shutdown) + wait_seconds = _seconds_until_next_midnight() + hours = wait_seconds / 3600.0 + logger.info( + f"Next sync scheduled in {wait_seconds:.0f}s (~{hours:.2f}h) at local midnight" + ) + # Sleep until midnight or until stop event is set + if _stop_event.wait(wait_seconds): + break + + # It's midnight (or we woke up), run the sync once + run_sync() + + # After running at midnight, loop continues to compute next midnight + except Exception: + logger.exception("Unexpected error in periodic sync loop") + finally: + logger.info("Periodic sync worker exiting") + + +if __name__ == '__main__': + try: + main() + except KeyboardInterrupt: + # Fallback in case the signal is delivered as KeyboardInterrupt + logger.info("KeyboardInterrupt received — exiting") + _stop_event.set() + except SystemExit: + logger.info("SystemExit received — exiting") + finally: + logger.info("run_periodic_sync terminated") diff --git a/backend/supervisord.conf b/backend/supervisord.conf index 84b3b8c4..eb72171b 100644 --- a/backend/supervisord.conf +++ b/backend/supervisord.conf @@ -8,7 +8,8 @@ stdout_logfile=/dev/stdout stderr_logfile=/dev/stderr [program:gunicorn] -command=/code/entrypoint.sh +command=/usr/local/bin/gunicorn main.wsgi:application --bind [::]:8000 --workers 2 --timeout 120 +directory=/code autorestart=true stdout_logfile=/dev/stdout stderr_logfile=/dev/stderr @@ -23,8 +24,9 @@ stderr_logfile=/dev/stderr stdout_logfile_maxbytes=0 stderr_logfile_maxbytes=0 -[program:cron] -command=cron -f +[program:sync_visited_regions] +command=/usr/local/bin/python3 /code/run_periodic_sync.py +directory=/code autorestart=true stdout_logfile=/dev/stdout stderr_logfile=/dev/stderr