# تشخیص عمیق down شدن سرور بعد از ~۱۰ سیکل + وریفای و تکمیل فیکس‌های پایداری

## زمینه

روی production (Coolify، تک‌کانتینر با supervisord: nginx + php-fpm + دو messenger worker) workerها هر ساعت با `--time-limit=3600` و exit code 0 عمداً recycle می‌شوند — این عادی است. مشکل واقعی: **بعد از حدود ۱۰ سیکل (~۱۰ ساعت) کل سرویس down می‌شود**. در لاگ‌های ارائه‌شده هیچ crash دیده نمی‌شود (`exit status 0; expected`، `/health` همیشه 200، اسکن‌های بات `GET /*.php` توسط nginx مستقیم 404 می‌شوند و به Symfony نمی‌رسند) — یعنی قاتل بیرون از لاگ اپ است.

فرضیه اصلی (به ترتیب احتمال): **فشار تدریجی حافظه تا OOM** و سپس **پر شدن دیسک**:

1. php-fpm قبلاً بدون `pm.max_requests` بود → پروسه‌ها هرگز recycle نمی‌شدند و نشت حافظه ساعت‌به‌ساعت جمع می‌شد؛ `memory_limit=1024M` هم به یک request اجازه می‌داد 1GB بخورد.
2. Redis (کش اپ + صف messenger، یک instance مشترک) پیش‌فرض `maxmemory=0` → رشد بی‌نهایت.
3. لاگ‌های docker (json-file) بدون `max-size` → پر شدن تدریجی دیسک.

بخشی از فیکس‌ها قبلاً در repo اعمال شده؛ این پرامپت آن‌ها را **وریفای** می‌کند، یک **اسکریپت جمع‌آوری شواهد** برای اجرای روی سرور می‌سازد، و چک‌لیست تنظیمات سمت Coolify را نهایی می‌کند.

## مشکل / هدف

- تشخیص قطعی و مبتنی بر شواهد (نه حدس): OOM؟ دیسک؟ یا چیز دیگر؟
- اطمینان از اینکه همه فیکس‌های پایداری در repo حاضرند (اگر revert شده‌اند، دوباره اعمال شوند).
- تحویل یک اسکریپت آماده که کاربر روی سرور اجرا کند و خروجی‌اش تشخیص را قطعی کند.

## فایل‌های مرتبط

| فایل | نقش |
|------|-----|
| `docker/php/php.ini` | باید `memory_limit = 256M` باشد (فیکس قبلی؛ ممکن است revert شده باشد — وریفای کن) |
| `docker/php/zz-pool.conf` | باید بلوک `pm = dynamic … pm.max_requests = 500` را داشته باشد |
| `docker/supervisord.conf` | هر دو worker بدون `-v`، هر دو با `--memory-limit=128M` |
| `docker-compose.yml` | آینه supervisord (حالت compose) |
| `supervisor.conf` | آینه (نسخه Liara) |
| `.env.coolify.example` | `MESSENGER_TRANSPORT_DSN` نمونه باید `?stream_max_entries=20000` داشته باشد |
| `docs/ops-restarts.md` | runbook — بخش‌های ۷ (OOM) و ۸ باید موجود باشند |
| `docs/collect-diagnostics.sh` | **جدید** — اسکریپت جمع‌آوری شواهد روی سرور |

## وضعیت فعلی

وضعیت انتظاری بعد از فیکس‌های قبلی (`docker/php/zz-pool.conf`):

```ini
[www]
clear_env = no
pm = dynamic
pm.max_children = 8
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 4
pm.max_requests = 500
```

و `docker/php/php.ini`:

```ini
memory_limit = 256M
```

⚠️ کاربر ممکن است `memory_limit` را به 1024M برگردانده باشد — وضعیت فعلی فایل را بخوان و اگر برگشته، با ذکر دلیل (۸ پروسه × 1024M = 8GB سقف نظری روی VPS کوچک) دوباره 256M کن، مگر اینکه کاربر صراحتاً 1024M خواسته باشد — در آن صورت دست نزن و فقط در گزارش ذکر کن.

## وظایف

### ۱. وریفای فیکس‌های repo (idempotent)

هر ۷ فایل جدول بالا را چک کن؛ هر موردی که غایب/revert شده را دوباره اعمال کن:

```bash
grep -n "memory_limit" docker/php/php.ini                         # انتظار: 256M
grep -n "pm.max_requests" docker/php/zz-pool.conf                 # انتظار: 500
grep -n "consume" docker/supervisord.conf docker-compose.yml supervisor.conf
#   انتظار: هیچ ` -v` ای نباشد؛ هر دو worker دارای --time-limit=3600؛ هر دو --memory-limit=128M
grep -n "stream_max_entries" .env.coolify.example                 # انتظار: 20000
grep -n "OOM\|maxmemory" docs/ops-restarts.md                     # بخش ۷ موجود
```

سه فایل supervisor/compose باید آینه هم بمانند.

### ۲. ساخت `docs/collect-diagnostics.sh` — اسکریپت شواهد

اسکریپت shell (POSIX sh، بدون وابستگی خاص) که کاربر روی **هاست سرور** (نه داخل کانتینر) اجرا می‌کند و همه شواهد لازم را یک‌جا چاپ می‌کند:

```sh
#!/bin/sh
# ClinicPro production diagnostics — run ON THE HOST as root (or with docker access).
# Usage: sh collect-diagnostics.sh [container-name-or-id]
set -u
C="${1:-$(docker ps --format '{{.Names}}' | grep -i -m1 clinic || true)}"

echo "=== 1. OOM evidence (kernel, last 48h) ==="
journalctl -k --since "48 hours ago" 2>/dev/null | grep -i -E "oom|killed process" | tail -20 \
  || dmesg | grep -i -E "oom|killed process" | tail -20

echo "=== 2. Memory / swap now ==="
free -h

echo "=== 3. Disk ==="
df -h | head -10
du -sh /var/lib/docker/containers/*/*-json.log 2>/dev/null | sort -h | tail -5

echo "=== 4. Container state ==="
docker ps -a --format 'table {{.Names}}\t{{.Status}}'
[ -n "$C" ] && docker inspect --format \
  'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}} RestartCount={{.RestartCount}} StartedAt={{.State.StartedAt}} FinishedAt={{.State.FinishedAt}}' "$C"

echo "=== 5. Live memory per container ==="
docker stats --no-stream

echo "=== 6. php-fpm inside app container ==="
[ -n "$C" ] && docker exec "$C" sh -c 'php -i 2>/dev/null | grep -E "^memory_limit"; ps -o rss,args 2>/dev/null | grep -E "php-fpm|messenger" | grep -v grep'

echo "=== 7. Redis memory (set host/port/pass if needed) ==="
R="$(docker ps --format '{{.Names}}' | grep -i -m1 redis || true)"
[ -n "$R" ] && docker exec "$R" redis-cli info memory 2>/dev/null | grep -E "used_memory_human|maxmemory_human|maxmemory_policy"

echo "=== 8. Docker log driver config ==="
docker info --format '{{.LoggingDriver}}'
cat /etc/docker/daemon.json 2>/dev/null || echo "(no daemon.json)"

echo "=== done — paste this whole output back for diagnosis ==="
```

اجرایی‌اش کن (`chmod +x`). در انتهای `docs/ops-restarts.md` یک خط ارجاع به این اسکریپت اضافه کن.

### ۳. جدول تفسیر شواهد در runbook

به `docs/ops-restarts.md` (بخش ۷) جدول تصمیم اضافه کن:

| شاهد در خروجی اسکریپت | تشخیص | اقدام |
|---|---|---|
| بخش ۱: `Out of memory: Killed process ... php-fpm` | نشت fpm | فیکس‌های pm/memory_limit + redeploy کافی است |
| بخش ۱: `Killed process ... mariadbd` یا `redis` | فشار کل RAM | Redis maxmemory + Coolify memory limit + swap |
| بخش ۴: `OOMKilled=true` | سقف حافظه کانتینر | سقف را بالاتر ببر یا مصرف را کم کن |
| بخش ۳: دیسک ≥ 90% یا json.log چند GB | دیسک پر | log rotation daemon.json + پاکسازی |
| بخش ۷: `maxmemory:0` و `used_memory` رشد کرده | Redis بی‌سقف | `maxmemory 256mb` + `maxmemory-policy volatile-lru` |
| هیچ‌کدام | فرضیه رد شد | خروجی کامل + لاگ ۵۰ خط آخر قبل از down برای تحلیل بعدی |

### ۴. چک‌لیست تنظیمات سمت Coolify (فقط مستندسازی — قابل اجرا از repo نیست)

در همان بخش runbook، چک‌لیست کوتاه:

1. Redeploy بعد از merge این تغییرات (configها فقط با image جدید اعمال می‌شوند).
2. env واقعی Coolify: به `MESSENGER_TRANSPORT_DSN` پارامتر `?stream_max_entries=20000` اضافه شود.
3. Redis resource: `maxmemory 256mb` + `maxmemory-policy volatile-lru` (نه `allkeys-lru` — پیام‌های صف TTL ندارند و evict می‌شوند).
4. App resource → Memory Limit = 1G.
5. `/etc/docker/daemon.json`: `{"log-driver":"json-file","log-opts":{"max-size":"20m","max-file":"3"}}` + restart docker.
6. اگر `free -h` نشان داد swap صفر است: 2G swap بساز.
7. Coolify Notifications برای container stop/unhealthy فعال شود.

## نکات مهم

- هیچ تغییری در رفتار workerها نده — recycle ساعتی عمدی است؛ فقط `--memory-limit=128M` باید روی هر دو باشد.
- این پرامپت PHP/React لمس نمی‌کند → migration و `docs/api/` لازم ندارد؛ فقط configهای deploy + `docs/`.
- اسکریپت باید بدون bash-ism باشد (`sh` خالص) چون روی هاست Ubuntu اجرا می‌شود نه داخل کانتینر alpine.
- بعد از هر تغییر config، سه فایل supervisord/compose/liara آینه بمانند (کامنت بالای فایل‌ها همین را می‌گوید).
- تست repo-side: `docker compose -f docker-compose.yml config -q` (فقط warning env، بدون خطا) و `sh -n docs/collect-diagnostics.sh` برای syntax اسکریپت.
