MWSE/decisions.md

8.4 KiB
Raw Blame History

decisions.md — Port Sırasında Alınan Kararlar

Geri-dönülebilir kararlar burada kayıt altına alındı; CLAUDE.md gereği seçildi, gerekçe yazıldı, devam edildi.

Mimari

  1. Klasör yapısı: Go repo kökünde (go.mod + main.go + internal/). Kullanıcı onayıyla. loadtest/ ayrı modül. frontend/ yerinde. Eski Source/ referans olarak bırakıldı.
  2. WebSocket kütüphanesi: gorilla/websocket v1.5.3. Kullanıcı onayıyla. En yaygın, en okunaklı; tek-yazıcı hub deseni doğrudan uyuyor. Modül önbelleğinde mevcut olduğundan ağ gerekmedi.
  3. Concurrency: RWMutex + bağlantı-başına-tek-yazıcı (actor yerine). İzin verilen iki seçenekten (#22) bu seçildi çünkü gerçek race "tek yazıcı + done seçimli Send" ile zaten çözülüyor; RWMutex versiyonu yeni Go geliştiricisi için belirgin biçimde daha okunaklı. Actor (reply-channel) modeli senkron request/response handler'larına ceremoni ekler. → REVIEW.md'de onaya açık.
  4. Modül yolu: git.saqut.com/saqut/mwse (engine), git.saqut.com/saqut/mwse-loadtest (yük testi). Mantıksal isim; repo dizininin Türkçe karakterli olması etkilemez.
  5. Dolu outbound buffer politikası: mesajı düşür, bağlantıyı KAPATMA. İlk versiyon (gorilla hub örneği gibi) yavaş peer'i komple düşürüyordu; yük testinde bu, ani trafik altında zincirleme kopmalara yol açtı (relay'de %92 kayıp). Best-effort relay için doğru politika: tek frame'i düşür, bağlantıyı koru. Gerçekten ölü peer zaten write-deadline ile temizlenir. Düzeltme sonrası teslim %98.5'e çıktı. Client.Dropped() ile gözlemlenebilir.

Node'daki hataların düzeltilmesi (tel sözleşmesi korunarak)

Kullanıcı talimatı: "tüm fonksiyonların aynı olması önemli değil; amaç/iş aynı kalsın." Node kaynağındaki bariz bug'lar doğru çalışacak şekilde yeniden yazıldı; mesaj isimleri/şekilleri (SDK'nın gördüğü tel) korundu:

  • Pairing akışı (Auth): Node'da accept/pair yanlış tarafın pairs setini kontrol ediyordu (akış asla tamamlanmıyordu). Doğru el sıkışma uygulandı: request/pair isteyen tarafı kaydeder + hedefe request/pair sinyali; accept/pair isteyenin gerçekten istek attığını doğrular + accepted/pair sinyali. Frontend'in beklediği {from, info} yükü gönderiliyor.
  • is/reachable: Node otherPeer.pairs.has(to) (kendi id'sini) kontrol ediyordu — her zaman false. Doğrusu: hedef pairing istemiyorsa VEYA gönderenle eşleşmişse erişilebilir.
  • Davet sistemi (Room accept/invite-room / reject/invite-room): Node Set üzerinde .includes/.filter çağırıyordu (çalışmaz, HANDLER_ERROR) ve joinType=='invite' kontrolü ters çevrilmişti. Doğru akış: sadece davet odaları, sadece bekleme listesindeki id'ler.
  • closeroom: Node room.owner === client.id ile Client nesnesini string'e kıyaslıyordu (her zaman false). Go'da OwnerID string tutuluyor; doğru kıyas.
  • create-room doğrulaması: Node tanımsız CreateRoomValidate değişkenine referans veriyordu (throw). joi şemasının niyetini taşıyan hafif bir doğrulama yazıldı. (Not: Node'un doğrulama hatasında döndürdüğü messages anahtarı —tekil değil— korundu.)
  • joinroom invite dalı: Node davet gönderdikten sonra NOT-FOUND-ROOM döndürüyordu (yanıltıcı). {status:"success", message:"INVITE-REQUESTED"} ile değiştirildi.
  • pack/to pairing kontrolü: Node otherPeer.pairs.has(to) (kendi id'si) kullanıyordu. Doğrusu hedefin gönterene pairing'i: other.HasPair(c.ID).

Bilinçli sadakat (Node davranışı korundu)

  • request/to → 'E' yanıtı: DÜZELTİLDİ (#33) — aşağıdaki §"#33" bölümüne bakın. Eskiden generic dispatcher action 'R' için hemen [null,id,'E'] yanıtlıyor, eşin asıl cevabını (response/to) eziyordu. Artık handler nil dönerse dispatcher yanıt göndermez.
  • auth/info çift gönderim: Hem pair hem roompair olan bir peer iki pair/info alır (Node ile aynı).
  • Heartbeat: 10sn saQut ping; pong "saQut" değilse bağlantı kapanır (Node ile aynı).

Ölçek ayarı & leak sağlamlaştırma (yüksek bağlantı + bitmek bilmeyen trafik)

Hedef: çok yüksek bağlantı sayısı + sürekli mesaj trafiği; limitleri/poolları yüksek tut, belleği bol kullan ama leak yok.

  1. Yapılandırılabilir, yüksek limitler (internal/config ConnConfig, env ile):
    • MWSE_OUTBOUND_BUFFER (varsayılan 1024) — bağlantı başına gönderim kuyruğu. Bellek notu: kanal ~24 B/slot önceden ayrılır → ~24 KiB/bağlantı (100k bağlantıda ~2.4 GiB boştayken). Düşük bellekli host'ta düşür, burst'lü üreticide yükselt.
    • MWSE_MAX_MESSAGE_SIZE (varsayılan 16 MiB) — büyük tünel payload'ları (#30) için yüksek.
    • MWSE_READ_BUFFER / MWSE_WRITE_BUFFER, MWSE_PING_INTERVAL, MWSE_PONG_WAIT, MWSE_WRITE_WAIT.
  2. gorilla WriteBufferPool (paylaşımlı sync.Pool) — yazma scratch buffer'ları bağlantılar arasında yeniden kullanılır; yüksek bağlantı sayısında büyük bellek tasarrufu, leak yok (GC yönetir). 150 bağlantı + ağır trafikte RSS ~43 MB ölçüldü.
  3. Dolu buffer'da düşür-ama-kapatma (bkz. #5): yüksek buffer + bu politika → relay teslimat %98.5'ten %99.98'e çıktı (150 bağlantıda 630974 gönderim, 99 düşüş).

Leak düzeltmeleri (sınırsız büyümeyi önle)

  1. Pairing ters-indeksi (pairedBy). Önceki halde tek-yönlü bekleyen istekler (hedef yanıt vermeden koparsa) uzun ömürlü istemcinin pairs setinde kalıcı çöp bırakıyordu → churn altında sınırsız büyüme. Çözüm: her istemci hem giden (pairs) hem gelen (pairedBy) kenarları tutar; AddPair/RemovePair iki tarafı da günceller (kilitler iç içe değil → deadlock yok); disconnect'te ForgetPeer ile X'e değen TÜM kenarlar O(derece) temizlenir (tüm istemcileri taramadan).
  2. Davet bekleme listesi temizliği. İstemci waiting (beklediği oda id'leri) tutar; disconnect'te ilgili odaların waitingInvited setinden düşülür → ölü id birikmez.
  3. realloc artık farklı adres verir. Node "önce release sonra lock" yapıyordu → tek istemcide aynı adresi döndürüyordu (işlevsiz). Düzeltme: önce yeni adresi al, sonra eskiyi bırak (eski rezerve kaldığından yeni mutlaka farklı); adres alanı tükenmişse eskiyi koru, fail dön.
  4. Goroutine sızıntısı yok: writePump/pingLoop/readLoop hepsi done/conn-close ile çıkar; gerçekten ölü peer write-deadline ile temizlenir.

#27 paritesi — eklenen

  1. ifexistsJoin: create-room'da isim çakışırsa hata yerine mevcut odaya katıl (tek tur "create or join").

#33 — EventPool WOM (askıda kalan promise) düzeltmesi

Kök neden: İki yönlü hata. (a) Engine dispatcher, numeric-id + action 'R' olan HER frame'e anında [result, id, 'E'] yanıtlıyordu — handler nil dönse bile [null,id,'E']. (b) SDK EventPool.request() WOM (fire-and-forget) paketler için bile bir waiter (promise) kaydediyordu. Sonuç: request/to gibi cevabı out-of-band (response/to) gelen paketlerde, sahte [null,id,'E'] waiter'ı erken çözüp siliyor, gerçek cevap kayboluyordu; saf WOM paketlerde ise (ör. pack/to) gereksiz waiter kalıyordu.

Çözüm: 14. Engine: dispatcher artık handler nil döndürürse yanıt göndermez (server.go). nil = "yanıtım yok / cevap out-of-band gelecek". Tüm gerçek request handler'ları non-nil döner (reply alır); yalnızca relay/WOM handler'ları (pack/to handshake'siz, request/to, response/to, pack/room handshake'siz) nil döner ve sessiz kalır. Bu, tel şekillerini değiştirmez; sadece SDK'nın kullanamadığı sahte frame'i kaldırır. 15. SDK: EventPool.only(msg) eklendi — WOM yolu, waiter bırakmaz. Peer.send (pack/to) ve Room.send handshake'siz dalı artık only() kullanır. request() yalnızca cevap bekleyen paketlere ayrıldı. Public API (peer.send/room.send imzaları) değişmedi.

Regression: internal/ws TestServerNoReplyOnNilResult; internal/services TestRequestResponseRoundTrip (out-of-band cevap aynı id ile geri geliyor, erken yanıt yok).

Session varsayılanları

packrecaive/packsending/notifyPairInfo/notifyRoomInfo varsayılanları NewClient constructor'ında set ediliyor (listener sırasından bağımsız her zaman mevcut). Session servisinin connect hook'u parite için yine de bunları yeniden uyguluyor.