Переглянути джерело

Fix R01-N27: backgrounded session-file GC loop in entrypoint

PHP's built-in session GC is probabilistic — on a low-traffic deployment
stale session files accumulate for days past gc_maxlifetime (8h).

bin/docker-entrypoint.sh now spawns a child loop after migrations:
sleep $SESSION_GC_INTERVAL_SECONDS (default 3600), then
`find $SESSION_PATH -mindepth 1 -type f -mmin +$SESSION_GC_MAX_AGE_MINUTES
-delete` (default 480 minutes = 8h). The loop is a child of the
entrypoint's PID 1, so `docker stop` propagates and reaps it together
with Apache. No new package — `find` is already in the php:8.3-apache
base image.

admin-manual §5.7 documents the loop, the two env-var knobs, and a
copy-pasteable cron snippet for bare-metal deployments that don't run
the container's entrypoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chiappa 2 днів тому
батько
коміт
32d03fc5db
2 змінених файлів з 59 додано та 0 видалено
  1. 28 0
      bin/docker-entrypoint.sh
  2. 31 0
      doc/admin-manual.md

+ 28 - 0
bin/docker-entrypoint.sh

@@ -11,9 +11,37 @@
 set -euo pipefail
 
 APP_ROOT="${APP_ROOT:-/var/www/html}"
+# R01-N27: session-storage path (defaults match Dockerfile + .env.example).
+SESSION_PATH="${SESSION_PATH:-/var/www/data/sessions}"
+# R01-N27: session-file lifetime in minutes. Default 480 (= 8h), matching
+# `session.gc_maxlifetime` set by `SessionGuard::start()`. Operators may
+# override via the container env (e.g. tighten on a public deployment).
+SESSION_GC_MAX_AGE_MINUTES="${SESSION_GC_MAX_AGE_MINUTES:-480}"
+# R01-N27: how often to sweep, in seconds. Default 3600 (= once per hour).
+SESSION_GC_INTERVAL_SECONDS="${SESSION_GC_INTERVAL_SECONDS:-3600}"
 
 echo "[entrypoint] running deploy-time migrations…"
 php "${APP_ROOT}/bin/migrate.php"
 
+# R01-N27: PHP's built-in session GC fires probabilistically off request
+# traffic, so a low-traffic deployment keeps stale session files for days
+# past `gc_maxlifetime`. This backgrounded loop deletes session files
+# older than $SESSION_GC_MAX_AGE_MINUTES every $SESSION_GC_INTERVAL_SECONDS.
+# It is a child of this script's PID 1, so a `docker stop` propagates and
+# tears it down cleanly along with Apache. No new package dependency — only
+# coreutils' `find`, already present in the php:8.3-apache base image.
+if [ -d "${SESSION_PATH}" ]; then
+    echo "[entrypoint] starting session GC loop (path=${SESSION_PATH}, max-age=${SESSION_GC_MAX_AGE_MINUTES}m, every ${SESSION_GC_INTERVAL_SECONDS}s)"
+    (
+        while true; do
+            sleep "${SESSION_GC_INTERVAL_SECONDS}"
+            # `-mmin +N` matches files older than N minutes; `-type f` avoids
+            # touching the directory itself; errors swallowed so a transient
+            # filesystem hiccup does not kill the loop.
+            find "${SESSION_PATH}" -mindepth 1 -type f -mmin +"${SESSION_GC_MAX_AGE_MINUTES}" -delete 2>/dev/null || true
+        done
+    ) &
+fi
+
 echo "[entrypoint] starting: $*"
 exec "$@"

+ 31 - 0
doc/admin-manual.md

@@ -531,6 +531,37 @@ not a security issue (the rejection is the correct OIDC behaviour),
 but it can be confusing. **Complete one sign-in at a time**, or close
 the older tab before starting a fresh login.
 
+### 5.7 Session-file garbage collection (R01-N27)
+
+PHP's built-in session GC is probabilistic — it only fires on request
+traffic with a small probability per request. On a quiet deployment the
+session directory accumulates stale files for days past the configured
+`gc_maxlifetime` (8h). The container's entrypoint compensates by
+launching a backgrounded `find … -mmin +480 -delete` sweep every hour:
+
+```bash
+# bin/docker-entrypoint.sh — see source for the full loop.
+find "${SESSION_PATH}" -mindepth 1 -type f -mmin +"${SESSION_GC_MAX_AGE_MINUTES}" -delete
+```
+
+Two env vars override the defaults:
+
+| Variable                       | Default | Meaning                                            |
+|--------------------------------|--------:|----------------------------------------------------|
+| `SESSION_GC_MAX_AGE_MINUTES`   | `480`   | Delete session files older than N minutes (= 8h). |
+| `SESSION_GC_INTERVAL_SECONDS`  | `3600`  | Sweep every N seconds (= once per hour).          |
+
+The loop is a child of PID 1 inside the container, so `docker stop`
+propagates and reaps it together with Apache. No additional packages
+are installed — `find` ships with the `php:8.3-apache` base image.
+
+If you deploy outside Docker (bare PHP-FPM host), wire an equivalent
+sweep yourself, e.g. a system cron entry:
+
+```cron
+17 * * * * www-data find /var/www/data/sessions -mindepth 1 -type f -mmin +480 -delete
+```
+
 ---
 
 ## 6. Troubleshooting