diff --git a/CHANGELOG.md b/CHANGELOG.md index b263b4f..9c456b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,9 +37,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 8.1 handles FrankenPHP worker mode natively via its built-in `FrankenPhpWorkerRunner`. The `extra.runtime` override in `composer.json` was removed so the runtime auto-detects FrankenPHP. - Note: the old package's `FRANKENPHP_LOOP_MAX` env var (default 500 - requests per worker) is no longer read; worker lifecycle is now managed - by FrankenPHP itself. + The old package's `FRANKENPHP_LOOP_MAX` env var is no longer read; + an equivalent recycle limit is restored via the new `MAX_REQUESTS` + setting below. + +### Added +- **`MAX_REQUESTS` worker-thread recycle limit** — The `Caddyfile` now + sets FrankenPHP's native `max_requests` from the `MAX_REQUESTS` + environment variable: each PHP worker thread is gracefully restarted + after N requests while others keep serving, containing slow memory + growth across long uptime. The image default is **500** (matching the + previous `runtime/frankenphp-symfony` default), baked in as a Docker + build arg and overridable at runtime (`MAX_REQUESTS=0` disables + restarts). Arbitrary `frankenphp`-block configuration is still + possible via the stock `FRANKENPHP_CONFIG` env var. ## [1.0.0] — v1.0 Release diff --git a/Caddyfile b/Caddyfile index dd48371..8ba514d 100644 --- a/Caddyfile +++ b/Caddyfile @@ -1,3 +1,17 @@ +{ + frankenphp { + # Restart each PHP worker thread after this many requests, containing + # slow memory growth across long uptime. Preserves the 7.4-era default + # loop count of runtime/frankenphp-symfony (500) after the Symfony 8.1 + # upgrade. Set MAX_REQUESTS=0 to disable restarts. The Dockerfile bakes + # in the default of 500 via build arg; override at runtime with: + # docker run -e MAX_REQUESTS=5000 ... + # For full control, the stock FRANKENPHP_CONFIG env var can inject any + # directive under this block instead. + max_requests {$MAX_REQUESTS} + } +} + http:// root public/ rewrite index.php diff --git a/Dockerfile b/Dockerfile index b83dbf3..b52b9ad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,6 +45,15 @@ ENV APP_DEBUG=0 ENV APP_ENV=prod ENV APP_SHARE_DIR=/data/preauth +# worker thread lifecycle: restart each PHP thread after N requests to +# contain slow memory growth. Matches the previous default loop count of +# runtime/frankenphp-symfony (removed in the Symfony 8.1 upgrade). +# Expose as a build arg so images can bake in a different default; +# MAX_REQUESTS=0 disables restarts. Runtime override: the same env var is +# read by the Caddyfile placeholder. +ARG MAX_REQUESTS=500 +ENV MAX_REQUESTS=$MAX_REQUESTS + # load application into final image WORKDIR /app COPY --from=build /data/preauth /data/preauth diff --git a/docs/example.env b/docs/example.env index 96da772..34d7459 100644 --- a/docs/example.env +++ b/docs/example.env @@ -24,6 +24,14 @@ # once blocked, do we respond with "I'm a teapot", false to use "Too many requests" #TEAPOT=true # default enabled, boolean +# --- server / worker options --- + +# (container/deployment only) restart each FrankenPHP worker thread after +# this many requests, containing memory growth across long uptime; +# matching the default from the old runtime/frankenphp-symfony package. +# 0 disables restarts. consumed by the Caddyfile, not the PHP app. +#MAX_REQUESTS=500 # default 500 + # --- remote-user header --- # Controls the value sent in the Remote-User header on successful auth. # session: the session id (default, backward-compatible) diff --git a/docs/symfony-8.1-upgrade-plan.md b/docs/symfony-8.1-upgrade-plan.md index d7cdc81..e625716 100644 --- a/docs/symfony-8.1-upgrade-plan.md +++ b/docs/symfony-8.1-upgrade-plan.md @@ -1,12 +1,29 @@ # Upgrade Plan: Symfony 7.4 → 8.1 -**Status:** Planning +**Status:** ✅ Implemented on branch `feat/symfony-8.1-upgrade-plan` +(Phases 0–3 & state audit complete; Phases 4–5 = staging + release) **Target:** Symfony `8.1.*` (all symfony components) -**Current:** Symfony `7.4.*` +**Was:** Symfony `7.4.*` → **resolved 8.1.2–8.1.6** **Prepared:** 2026-09-07 --- +## Implementation results + +| Phase | Result | +|-------|--------| +| 0 Deprecation sweep | ✅ Clean — suite runs with `failOnDeprecation=true`, zero hits on 7.4; the 8.x jump needed **no app code changes**. | +| 1 Composer bump | ✅ `runtime/frankenphp-symfony` removed, `extra.runtime` deleted, all `symfony/*` at `8.1.*` (framework-bundle 8.1.6, twig-bundle 8.1.2); ride-alongs PHPUnit 13.3.2, Twig 3.28, otphp 11.5. Boots on **v8.1.6**. | +| 2 Config refresh | ✅ `config/reference.php` is gitignored, auto-regenerated by Flex. Prod `cache:clear`+`cache:warmup`, `lint:container`/`lint:yaml`/`lint:twig` all pass. | +| 3 Tests | ✅ **295 tests / 612 assertions green** on 8.1; php-cs-fixer 0 fixable files. | +| State audit | ✅ All `src/` services are `final readonly` with ctor-injected deps — no mutable state, kernel reuse under `FrankenPhpWorkerRunner` is safe. | +| Loop-max parity | ✅ `Caddyfile` sets `max_requests {$MAX_REQUESTS}`; default **500** baked into the image via Docker build arg (matches old package default), runtime-overridable. See §2 note. | + +Phases 4–5 (staging smoke + release) are pending — everything else in +this document describes what was planned **and is now done**. + +--- + ## 1. Why we can leapfrog 8.0 Symfony 7.4 and 8.0 were released simultaneously (Nov 2025) and are @@ -191,7 +208,7 @@ twig-bundle, uid, yaml) and tick each item against this codebase. | 1 | **composer bump**: branch `feat/symfony-8.1`; edit composer.json per §3–§4; `composer remove runtime/frankenphp-symfony`; `composer update`; re-sync recipes. | Installs clean on PHP 8.5; `bin/console about` shows 8.1.x. | | 2 | **Config refresh**: regenerate `config/reference.php`; review framework/twig/rate-limiter defaults; commit config changes. | `cache:clear` + warmup pass in dev & prod envs. | | 3 | **Tests**: full phpunit suite + php-cs-fixer; fix failures (expected: minor — event/type related). | Suite green in CI. | -| 4 | **Staging smoke**: build image, run under FrankenPHP worker mode; verify TOTP login flow, backup codes, rate limiting (burst + teapot mode), public paths, central-auth subdomain flow; watch memory across >500 requests (old loop_max default no longer applies — see §2 note). | No state leaks across worker requests; healthcheck passes. | +| 4 | **Staging smoke**: build image, run under FrankenPHP worker mode; verify TOTP login flow, backup codes, rate limiting (burst + teapot mode), public paths, central-auth subdomain flow; watch memory across >500 requests to confirm threads recycle via the Caddyfile `max_requests` setting (see §2 note). | No state leaks across worker requests; worker threads recycle at the configured request count; healthcheck passes. | | 5 | **Docs + release**: update readme/DESIGN_CONSIDERATIONS ("symfony 8.1, built-in FrankenPHP runtime"); tag a minor release per CHANGELOG conventions. | Release published; image rebuilt & pushed. | **Rollback:** the upgrade is a single composer.lock + config diff. diff --git a/readme.md b/readme.md index 1127023..58fa532 100644 --- a/readme.md +++ b/readme.md @@ -113,6 +113,7 @@ for the complete reference. |----------|---------|-------------| | `IP_TTL` | `0` | Seconds to allow all traffic from an IP after login (0 = disabled). | | `TEAPOT` | `1` | Respond with 418 instead of 429 when rate-limited (boolean). | +| `MAX_REQUESTS` | `500` | Restart each FrankenPHP worker thread after this many requests to contain memory growth (`0` = unlimited). Maps to the Caddyfile `max_requests` directive. | ### Remote-User Header