From d41120c3ce5b65aef951556c317144d589d680cd Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Tue, 22 Sep 2026 01:08:18 +0300 Subject: [PATCH] =?UTF-8?q?fix(liste):=20=C3=BCretim=20sonras=C4=B1=20sunu?= =?UTF-8?q?cu=20yan=C4=B1t=20vermezse=20502=20sayfas=C4=B1na=20gidilmiyor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iPad raporu: liste üretildikten sonra ekran "Bad Gateway" gösterdi ve tarayıcı `listem.txt` indirdi. Sıra şu: liste DB'ye yazıldı → `router.replace` → Next'in RSC isteği ters vekilden 502 aldı → Next tam-sayfa gezinmeye düştü (WebKit düz metin cevabı dosya olarak kaydetti) → 6 sn'lik emniyet supabı `window.location.replace` ile aynı 502'yi ekrana getirdi. Üç değişiklik: - Dockerfile runner katmanına `--max-old-space-size=768`. V8 cgroup sınırını görmez, heap üst sınırını host RAM'ine göre seçer; `mem_limit: 1g` ile birleşince V8 daha GC yapmadan konteyner OOM ile ölüyor ve açık istek 502'ye düşüyor. Sınır builder katmanında vardı, runner'a miras kalmıyordu. - `/api/saglik`: DB/oturum/AI'ya dokunmayan en ucuz uç. Dockerfile HEALTHCHECK bunu yokluyor — süreç ölüp açılırken `docker ps` "unhealthy" gösterir, bir sonraki 502'nin kaynağı tahmine kalmaz. - Emniyet supabı tam-sayfa gezinmeden önce sunucuyu yokluyor. Sunucu yoksa ekranda kalınıp "listen oluşturuldu ve kaydedildi, kredin yeniden harcanmaz" ekranı + sunucuyu yeniden yoklayan buton gösteriliyor; `liste_uretim_hata {kod:"SUNUCUYA_ULASILAMIYOR"}` olayı düşüyor. Sunucu yokken `router.refresh` de aynı tuzağa düştüğü için o dalda atlanıyor. Co-Authored-By: Claude Opus 5 --- Dockerfile | 16 +++ src/app/api/saglik/route.ts | 18 ++++ .../rapor/components/liste-uretici.tsx | 97 ++++++++++++++++++- 3 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 src/app/api/saglik/route.ts diff --git a/Dockerfile b/Dockerfile index 859397c..e9b3684 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,6 +48,15 @@ WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 +# V8, cgroup bellek sınırını GÖRMEZ: heap üst sınırını konteynerin değil host'un +# RAM'ine göre seçer (host 4 GB ise ~2 GB). docker-compose'daki mem_limit=1g ile +# birleşince sonuç, V8 daha GC yapmadan kernel'ın konteyneri OOM ile öldürmesi +# olur; o anda açık olan istek ters vekilde 502 "Bad Gateway" olarak düşer +# (22 Eyl 2026: liste üretiminin hemen ardından alınan 502 raporu). Sınır elle +# verilir: mem_limit'in ~%75'i; kalanı native taraf (better-sqlite3, mmap'lı +# SQLite sayfaları, Node'un kendi RSS'i) için ayrılır. mem_limit değişirse bu +# sayı da değişmeli. +ENV NODE_OPTIONS="--max-old-space-size=768" RUN addgroup --system --gid 1001 nodejs && \ adduser --system --uid 1001 nextjs @@ -81,4 +90,11 @@ EXPOSE 3000 ENV PORT=3000 ENV HOSTNAME="0.0.0.0" +# Konteyner gerçekten istek karşılıyor mu (alpine'de curl yok, Node 20'nin +# global fetch'i kullanılır). Uç bilerek en ucuz olanı: app/api/saglik DB'ye, +# oturuma, AI'ya dokunmaz. Süreç OOM ile ölüp yeniden açılırken `docker ps` +# durumu "unhealthy" gösterir — 502'lerin kaynağı tahmine kalmaz. +HEALTHCHECK --interval=30s --timeout=5s --start-period=45s --retries=3 \ + CMD node -e "fetch('http://127.0.0.1:3000/api/saglik').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" + CMD ["/app/docker-entrypoint.sh"] diff --git a/src/app/api/saglik/route.ts b/src/app/api/saglik/route.ts new file mode 100644 index 0000000..fef6991 --- /dev/null +++ b/src/app/api/saglik/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from "next/server"; + +// Sunucu ayakta mı? Bilerek en ucuz uç: DB'ye, oturuma, AI'ya dokunmaz, gövdesi +// sabittir. İki yerden sorulur: +// 1) Docker HEALTHCHECK (bkz. Dockerfile): konteyner OOM ile öldüğünde ya da +// açılışta henüz hazır olmadığında "unhealthy" görünür. +// 2) /listem üretim ekranı (bkz. features/rapor/components/liste-uretici.tsx): +// liste kurulduktan sonra yönlendirme ekranı çevirmediyse, ters vekilin +// (Traefik) düz metin 502 sayfasına tam-sayfa gezinmeden önce sunucunun +// yaşadığı buradan sorulur. +// `dynamic` segment ayarı Cache Components açıkken kaldırıldı (Next 16), o +// yüzden yalnızca no-store başlığı verilir. +export function GET() { + return NextResponse.json( + { ok: true }, + { headers: { "Cache-Control": "no-store" } }, + ); +} diff --git a/src/features/rapor/components/liste-uretici.tsx b/src/features/rapor/components/liste-uretici.tsx index 6e599e7..0ac335d 100644 --- a/src/features/rapor/components/liste-uretici.tsx +++ b/src/features/rapor/components/liste-uretici.tsx @@ -44,12 +44,32 @@ const GECIS_SINIRI_MS = 6000; // kesilmez, cevap gelirse akış normal devam eder. const UZUN_SURE_MS = 90_000; +/** Sunucu ayakta mı (bkz. app/api/saglik). Ağ hatası ve 5xx "hayır" sayılır. + * AbortSignal.timeout Safari 16'da geldi; eski iPad'lerde yokluğu yoklamayı + * "sunucu ölü" sanmaya çevirmesin diye varlığı denetlenir. */ +async function sunucuAyaktaMi(): Promise { + try { + const yanit = await fetch("/api/saglik", { + cache: "no-store", + signal: + typeof AbortSignal.timeout === "function" + ? AbortSignal.timeout(5000) + : undefined, + }); + return yanit.ok; + } catch { + return false; + } +} + type Durum = | { ad: "uretiliyor" } // İstek hâlâ yolda ama beklenenden uzun sürdü: iskelet + bilgi notu | { ad: "uzun-surdu" } // Liste kuruldu; yönlendiricinin /listem'e geçmesi bekleniyor | { ad: "geciliyor" } + // Liste kuruldu ve kaydedildi ama sunucu yanıt vermiyor (ters vekil 502) + | { ad: "sunucu-yok" } | { ad: "kredi-yetersiz" } | { ad: "hata"; mesaj: string } | { ad: "profil-yok" }; @@ -75,6 +95,8 @@ export function ListeUretici({ // ya iade edildi; sonraki deneme taze id ile başlamalı — bkz. sonuc/actions.ts). const requestIdRef = useRef(null); const basladi = useRef(false); + // "geciliyor" hâlinden sunucu-yok ekranına düşüldü mü (bkz. emniyet supabı) + const gecisBasarisiz = useRef(false); async function uret(profil: TercihProfili) { if (!requestIdRef.current) requestIdRef.current = crypto.randomUUID(); @@ -133,16 +155,36 @@ export function ListeUretici({ // ?uret=1'e dönüp üretimi yeniden başlatmasın. Ayrı olay adı OlayAdi // birliğini (lib/analitik.ts) genişletmeyi gerektirdiğinden supap, // liste_uretim_hata'nın `kod: "GECIS_ZORLANDI"` değeriyle ayırt edilir. + // + // Gezinmeden ÖNCE sunucuya sorulur (22 Eyl 2026, iPad + Chrome raporu): + // konteyner üretimin hemen ardından ölürse (ör. bellek sınırı) hem Next'in + // RSC isteği hem bizim tam-sayfa gezinmemiz ters vekilin düz metin "Bad + // Gateway" cevabına düşüyor — WebKit bunu sayfa yerine dosya olarak indirip + // kullanıcıya listem.txt veriyordu. Liste o anda DB'ye YAZILMIŞ durumda; + // doğru davranış ekranda kalıp bunu dürüstçe söylemek. useEffect(() => { if (durum.ad !== "geciliyor") return; - const zamanlayici = setTimeout(() => { + let iptal = false; + const zamanlayici = setTimeout(async () => { + if (!(await sunucuAyaktaMi())) { + if (iptal) return; + olay("liste_uretim_hata", { kod: "SUNUCUYA_ULASILAMIYOR" }); + // Cleanup'taki router.refresh'i de kapatır: sunucu yokken refresh + // Next'i tam-sayfa gezinmeye düşürür, tam kaçındığımız yol. + gecisBasarisiz.current = true; + setDurum({ ad: "sunucu-yok" }); + return; + } + if (iptal) return; olay("liste_uretim_hata", { kod: "GECIS_ZORLANDI" }); window.location.replace("/listem"); }, GECIS_SINIRI_MS); return () => { + iptal = true; clearTimeout(zamanlayici); - // "geciliyor" son durumdur; bu temizlik yalnızca bileşen ekrandan - // kalkarken, yani gezinme işlendikten sonra koşar — kuyruk boşken + if (gecisBasarisiz.current) return; + // Buraya yalnızca bileşen ekrandan kalkarken, yani gezinme işlendikten + // sonra gelinir (sunucu-yok dalı yukarıda dönüyor) — kuyruk boşken // gönderilen refresh güvenle çalışır ve layout'taki kredi pill'ini // (gezinme layout verisini yenilemez) günceller. router.refresh(); @@ -205,6 +247,8 @@ export function ListeUretici({ ); } + if (durum.ad === "sunucu-yok") return ; + if (durum.ad === "hata") { return (
@@ -236,6 +280,53 @@ export function ListeUretici({ return ; } +/** Liste kuruldu ve kaydedildi ama sunucu yanıt vermiyor. Tam-sayfa gezinme + * burada kullanıcı isteğiyle, ancak sunucunun yaşadığı doğrulandıktan sonra + * yapılır: aksi hâlde ters vekilin düz metin 502 cevabı ekrana (iOS'ta indirme + * klasörüne) düşüyor. /listem'e ?uret=1 olmadan gitmek üretimi yeniden + * başlatmaz; kredi ikinci kez yanmaz. */ +function SunucuYokEkrani() { + const [deniyor, setDeniyor] = useState(false); + const [halaYok, setHalaYok] = useState(false); + + async function dene() { + setDeniyor(true); + setHalaYok(false); + if (await sunucuAyaktaMi()) { + window.location.replace("/listem"); + return; + } + setDeniyor(false); + setHalaYok(true); + } + + return ( +
+

+ Listen hazır, sunucuya şu an ulaşamıyoruz +

+

+ Listen oluşturuldu ve hesabına kaydedildi — kredin yeniden harcanmaz. + Sunucu bir an yanıt vermedi; birkaç saniye sonra listeni açabilirsin. +

+ +

+ {halaYok + ? "Sunucu hâlâ yanıt vermiyor. Birkaç saniye sonra tekrar dene; listen kayıtlı, kaybolmaz." + : "Bu sayfayı kapatsan da listen hesabında durur."} +

+
+ ); +} + function UretimGovdesi({ uzunSurdu }: { uzunSurdu: boolean }) { const mesaj = useUretimMesaji();