From 9c9a59dad36b2829ad2d2342bc9de481d6f1fcf7 Mon Sep 17 00:00:00 2001 From: Uno-Takashi Date: Wed, 24 Jun 2026 10:37:31 +0000 Subject: [PATCH] fix: close ghost rooms/users on startup and repair cron env - Add 'close_active_sessions' management command that logically deletes any AnimeRoom / AnimeUser still marked alive. WebSocket session state lives only in the Django process, so a container restart used to leave ghost sessions counted by the public /api/.../alive endpoints forever. - entrypoint.sh now runs the command before starting runserver/gunicorn. - Fix the cron job that drives 'manage.py cleanup': - Use the venv python absolute path (cron's PATH lacked /opt/venv/bin so the job was failing every minute with 'python: not found'). - Snapshot container env to /etc/cron.d/d-party-env.sh and source it from the job, so settings.py can read DEBUG and other compose-only env vars that cron does not inherit. --- entrypoint.sh | 30 +++++++++++++++++-- .../commands/close_active_sessions.py | 29 ++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 streamer/management/commands/close_active_sessions.py diff --git a/entrypoint.sh b/entrypoint.sh index ad77552..dab5f34 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -2,14 +2,38 @@ set -e # Schedule the retention cleanup via system cron (replaces django-crontab). -# manage.py loads the env files itself, so the job needs no extra environment. +# cron runs with a minimal PATH (no /opt/venv/bin), so use the venv python +# explicitly. manage.py loads the env files itself. CRON_SCHEDULE="${CRON_SCHEDULE:-0 0 * * *}" -echo "$CRON_SCHEDULE cd /usr/src/app && python manage.py cleanup >> /var/log/cron.log 2>&1" > /etc/cron.d/d-party-cleanup +PYTHON_BIN="$(command -v python)" +# cron は親プロセスの環境変数を引き継がないため、settings.py が参照する DEBUG +# などのコンテナ env を取りこぼす。コンテナ起動時の env をスナップショットして +# おき、cron ジョブから source する。 +ENV_SNAPSHOT=/etc/cron.d/d-party-env.sh +{ + echo "#!/bin/sh" + # 値に空白等が含まれてもよいよう、各値をシェル用にクォートする。 + env | awk -F= 'NF>=2 { + key=$1 + val=substr($0, length(key)+2) + gsub(/'\''/, "'\''\\'\'''\''", val) + printf "export %s='\''%s'\''\n", key, val + }' +} > "$ENV_SNAPSHOT" +chmod 0644 "$ENV_SNAPSHOT" +{ + echo "PATH=/opt/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + echo "$CRON_SCHEDULE root . $ENV_SNAPSHOT; cd /usr/src/app && $PYTHON_BIN manage.py cleanup >> /var/log/cron.log 2>&1" +} > /etc/cron.d/d-party-cleanup chmod 0644 /etc/cron.d/d-party-cleanup -crontab /etc/cron.d/d-party-cleanup touch /var/log/cron.log cron +# Close any rooms/users that were left "alive" by the previous process. The +# WebSocket session state lives only in the Django process, so without this +# the /api/.../alive endpoints would keep counting ghost sessions forever. +python manage.py close_active_sessions || true + if [ "$DEBUG" = "1" ]; then python manage.py runserver 0.0.0.0:8000 else diff --git a/streamer/management/commands/close_active_sessions.py b/streamer/management/commands/close_active_sessions.py new file mode 100644 index 0000000..9dd6a4a --- /dev/null +++ b/streamer/management/commands/close_active_sessions.py @@ -0,0 +1,29 @@ +"""Close any rooms / users that are still marked alive at startup. + +Channels の WebSocket セッションはプロセス内 (`AnimePartyConsumer`) にのみ存在 +するため、Django プロセス(コンテナ)が落ちると ``disconnect`` が呼ばれず +``AnimeRoom`` / ``AnimeUser`` が ``alive`` のまま残る。次回コンテナ起動時に +これらを論理削除しておかないと、統計 API(``alive`` 件数)に幽霊セッションが +残り続けてしまう。 + +冪等な操作なので、起動毎に無条件で実行してよい。 +""" + +from django.core.management.base import BaseCommand + +from streamer.models import AnimeRoom, AnimeUser + + +class Command(BaseCommand): + help = "Logically delete all AnimeRoom / AnimeUser rows still marked alive." + + def handle(self, *args, **options): + users = AnimeUser.objects.alive().count() + rooms = AnimeRoom.objects.alive().count() + AnimeUser.objects.alive().delete() + AnimeRoom.objects.alive().delete() + self.stdout.write( + self.style.SUCCESS( + f"closed {users} alive user(s) and {rooms} alive room(s)" + ) + )