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)" + ) + )