Hand originals to nginx after auth so PHP is not in the byte path. Keep DOWNLOAD_ACCEL_ENABLED off until the internal location is verified.
81 lines
3.4 KiB
Markdown
81 lines
3.4 KiB
Markdown
# M13 — Artwork Download X-Accel-Redirect Offload
|
||
|
||
Application tests and nginx snippet only. **Do not enable in production until Stage A nginx is verified.**
|
||
|
||
## Pre-M13 evidence
|
||
|
||
`/download/artwork/{id}` (M12 analyzer, skinbase.org):
|
||
|
||
| | |
|
||
| --- | --- |
|
||
| count | 2363 |
|
||
| p50 / p95 / p99 / max | 0.535 / 1.526 / 3.335 / **16.037** s |
|
||
| avg request_time | 0.7175 s |
|
||
| avg upstream_response_time | 0.3762 s |
|
||
| avg bytes | ~528 KB |
|
||
|
||
Slow requests show PHP finishing in ~0.3–0.5 s while `request_time` stays large: FPM is stuck sending the body.
|
||
|
||
After X-Accel: `upstream_response_time` is still Laravel work; `request_time` can still include slow clients. The win is **freeing PHP-FPM**, not always a tiny nginx `request_time`.
|
||
|
||
## Existing application
|
||
|
||
- Route: `GET /download/artwork/{id}` → `ArtworkDownloadController`
|
||
- Flag: `DOWNLOAD_ACCEL_ENABLED` default **false**
|
||
- Path: `DOWNLOAD_ACCEL_PATH=/internal/originals`
|
||
- Laravel originals root: `uploads.local_originals_root` (symlink `SkinbaseNova/storage` → shared)
|
||
- Fallback: `response()->download()` — **must stay**
|
||
|
||
Controller already authorizes, checks file, records analytics, then optionally sets `X-Accel-Redirect`.
|
||
|
||
## nginx mapping
|
||
|
||
Production vhost is **not** in this repo: `/etc/nginx/sites-enabled/skinbase.org.conf`.
|
||
|
||
Snippet: `deploy/nginx/download-accel.conf`
|
||
|
||
```nginx
|
||
location ^~ /internal/originals/ {
|
||
internal;
|
||
alias /opt/www/virtual/SkinbaseNova.releases/shared/storage/app/originals/artworks/;
|
||
}
|
||
```
|
||
|
||
Alias uses the **canonical shared tree** so a release switch cannot point nginx at a missing release directory. Laravel still uses `local_originals_root`; the X-Accel URI is only the relative hash path (`/27/21/….zip`).
|
||
|
||
`internal;` is mandatory: a browser GET to `/internal/originals/...` must 404.
|
||
|
||
No sendfile/tcp/buffering changes in M13.
|
||
|
||
## Rollout (operator — not executed here)
|
||
|
||
### Stage A — nginx only (`DOWNLOAD_ACCEL_ENABLED=false`)
|
||
|
||
1. Backup: `sudo cp /etc/nginx/sites-enabled/skinbase.org.conf /etc/nginx/sites-enabled/skinbase.org.conf.bak-$(date +%Y%m%d-%H%M%S)`
|
||
2. Insert the location inside the HTTPS `server { }` (before `location /`).
|
||
3. `sudo nginx -t` — stop if it fails.
|
||
4. `sudo systemctl reload nginx` — **not** restart.
|
||
5. Direct access: `curl -I https://skinbase.org/internal/originals/<relative-path>` → **404**.
|
||
6. Optional: `sudo -u www-data test -r <absolute-original> && echo READABLE`
|
||
|
||
### Stage B — Laravel flag
|
||
|
||
7. Set `DOWNLOAD_ACCEL_ENABLED=true` (path already `/internal/originals`).
|
||
8. `sudo -u skinbase php artisan config:cache` if config is cached.
|
||
9. Verify with tinker as `skinbase` (flag true, path `/internal/originals`).
|
||
10. Public: `curl -fL https://skinbase.org/download/artwork/<ID>` → 200, attachment, size matches original.
|
||
11. Range: `curl -H 'Range: bytes=0-99'` → **206**, `Content-Range: bytes 0-99/<size>`, 100-byte body.
|
||
12. Repeat direct internal URL → still 404.
|
||
|
||
Application deploy: `bash ./sync.sh`. Never `git pull` on production.
|
||
|
||
## Rollback
|
||
|
||
First: `DOWNLOAD_ACCEL_ENABLED=false` then `sudo -u skinbase php artisan config:cache` if needed. PHP fallback returns immediately. Nginx location can stay.
|
||
|
||
If nginx must be reverted: restore the `.bak-*` vhost, `nginx -t`, reload.
|
||
|
||
## Analyzer
|
||
|
||
Keep M12 `skinbase-performance.log`. After activation, `upstream_response_time` should stay near Laravel work; do not expect `request_time` to collapse for slow clients.
|