11 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).
Data-sync alt sistemleri (#43 / #44 / #45)
-
services.Registerartık*Registrydöndürür ve...Optionalır. Eski çağrılar (Register(hub)) dönüşü yok sayarak çalışmaya devam eder. Registry, ömür yönetimi gereken store'ları (notify, datastore) main'e açar; main bunların janitor'larını başlatır ve shutdown'da durdurur (goroutine sızıntısı yok). 3. taraf entegrasyonları (#44 trigger)WithNotifyTriggerile enjekte edilir; varsayılan no-op (IPPressure'dakiAnnouncerdeseniyle aynı). -
#43 Notify (store-and-forward) + #44 Suit:
internal/notifysaf, transport-bağımsız store (enjekte edilebilir clock). Hedef offline ise mesaj kuyruğa girer, bağlanıncanotifysinyali ile teslim edilir; her bildirimdetraceid →notify/statusile sorgulanır. Leak yok: her bildirimde TTL (vars. 24s) + hedef başına sınır (vars. 1024, en eski düşer) + periyodik janitor. Suit (#44):suit:truebildirime clientnotify/replyile cevap verir; cevap 3. taraf sunucuyaNotifyTriggerile manuel tetiklenir (poll yok) ve origin client online isenotify/replysinyali ile bildirilir. -
#45 Datastore + Active/Passive Sync:
internal/datastoresaf paket (ws bağımlılığı yok — data kurallarını hub/socket olmadan test edilebilir kılar; servis katmanı abone id'lerini çözüp sinyal atar).- Active sync / collection (CRUD broadcast):
data/open/data/set/data/delete/data/get. Sunucu otoriter kopyayı tutar; her mutasyon datastore kilidi altında monoton seq ile damgalanır → çakışma çözümü arrival-time (kilidi en son alan kazanır), seq her broadcast'te taşınır, client'lar yakınsar. Set/delete diğer aboneleredata/opsinyali ile yayılır (origin hariç; o seq'i kendi yanıtından öğrenir). - Passive sync (merge pool):
sync/open/sync/push/sync/pull. Öğeler kanonik JSON hash'iyle dedupe edilir; push yalnızca yeni delta'yısync/addile yayar → ortak havuzda toplanır, hepsi eşit olana kadar push/pull sürer. - Datastore tipi:
kind:"temp"|"permanent"(alan adı bilinçli olarakkind;typeWSTS handler seçici olduğu için kullanılamaz). temp TTL ile expire, permanent kalıcı. id boşsa public id üretilir. - Leak yok: temp store/pool TTL + janitor; disconnect'te
UnsubscribeAllile abonelik kalıntısı bırakılmaz.
- Active sync / collection (CRUD broadcast):
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.