MWSE/decisions.md

64 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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**.
6. **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`.
7. **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ü.
8. **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)
9. **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).
10. **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.
11. **`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.
12. **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
13. **`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.