8.4 KiB
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
- Klasör yapısı: Go repo kökünde (
go.mod+main.go+internal/). Kullanıcı onayıyla.loadtest/ayrı modül.frontend/yerinde. EskiSource/referans olarak bırakıldı. - WebSocket kütüphanesi:
gorilla/websocketv1.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. - 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ı +
doneseçimliSend" 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. - 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. - 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/pairyanlış tarafınpairssetini kontrol ediyordu (akış asla tamamlanmıyordu). Doğru el sıkışma uygulandı:request/pairisteyen tarafı kaydeder + hedeferequest/pairsinyali;accept/pairisteyenin gerçekten istek attığını doğrular +accepted/pairsinyali. Frontend'in beklediği{from, info}yükü gönderiliyor. is/reachable: NodeotherPeer.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): NodeSetüzerinde.includes/.filterçağırıyordu (çalışmaz, HANDLER_ERROR) vejoinType=='invite'kontrolü ters çevrilmişti. Doğru akış: sadece davet odaları, sadece bekleme listesindeki id'ler. closeroom: Noderoom.owner === client.idile Client nesnesini string'e kıyaslıyordu (her zaman false). Go'daOwnerID stringtutuluyor; doğru kıyas.create-roomdoğrulaması: Node tanımsızCreateRoomValidatedeğ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üğümessagesanahtarı —tekil değil— korundu.)joinroominvite dalı: Node davet gönderdikten sonraNOT-FOUND-ROOMdöndürüyordu (yanıltıcı).{status:"success", message:"INVITE-REQUESTED"}ile değiştirildi.pack/topairing kontrolü: NodeotherPeer.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)
DÜZELTİLDİ (#33) — aşağıdaki §"#33" bölümüne bakın. Eskiden generic dispatcher action 'R' için hemenrequest/to→ 'E' yanıtı:[null,id,'E']yanıtlıyor, eşin asıl cevabını (response/to) eziyordu. Artık handlernildönerse dispatcher yanıt göndermez.auth/infoçift gönderim: Hem pair hem roompair olan bir peer ikipair/infoalır (Node ile aynı).- Heartbeat: 10sn
saQutping; 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.
- Yapılandırılabilir, yüksek limitler (
internal/configConnConfig, 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.
- 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ü. - 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)
- Pairing ters-indeksi (
pairedBy). Önceki halde tek-yönlü bekleyen istekler (hedef yanıt vermeden koparsa) uzun ömürlü istemcininpairssetinde 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/RemovePairiki tarafı da günceller (kilitler iç içe değil → deadlock yok); disconnect'teForgetPeerile X'e değen TÜM kenarlar O(derece) temizlenir (tüm istemcileri taramadan). - Davet bekleme listesi temizliği. İstemci
waiting(beklediği oda id'leri) tutar; disconnect'te ilgili odalarınwaitingInvitedsetinden düşülür → ölü id birikmez. reallocartı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.- 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
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.