Steven
Steven6 dk okuma

Kopan Bir Bağlantı Tüm Uygulamanı Çökertmemeli — Ama Bizimki Çökerdi

Backend’imiz toplantı ortasında çevrimdışı olduğunda, yalnızca transkripsiyonu duraklatmakla kalmadı — tüm uygulamayı çökertti. Sebep tek bir işlenmemiş olaydı ve çözüm on satırdı. Bu, GeekBye’ı eskiden onu öldüren şeyler boyunca oturumu açık ve bağlı tutan sürüm kümesidir.

Mühendislik
Güvenilirlik
Electron
GeekBye Releases
Kopan Bir Bağlantı Tüm Uygulamanı Çökertmemeli — Ama Bizimki Çökerdi

Bazı hatalar bir özelliği kesintiye uğratır. En kötüleri etrafındaki her şeyi de yıkar. Erken bir GeekBye sürümünde, sen toplantı ortasındayken backend’imiz çevrimdışı olursa, transkripsiyon yalnızca durmuyordu — tüm uygulama çöküyordu. Bir kopan bağlantı ve tüm pencere gidiyordu.

Bunu düzelten sürüm kümesi — v1.6.8’den v1.6.11’e — "oturumu açık tut, bağlı kal" gibi gösterişsiz işin bir ustalık dersidir. Başrol düzeltmesi on satırdı. İşte o on satırın ne olduğu ve yanında yayınlanan diğer her şey.

Bir kopmayla bir çöküş arasında on satır

Çöküş ses kodunda yaşıyordu. Bir transkripsiyon oturumu WebSocket’lerini söktüğünde, onlarda removeAllListeners() çağırıyordu — makul bir temizlik. Ama işte temizliği felakete çeviren Node.js ayrıntısı: dinleyicisi olmayan bir error olayı görmezden gelinmez. Yakalanmamış bir istisna olarak yeniden fırlatılır.

Yani backend çevrimdışı olduğunda sırayı hayal et: soketin, tetiklenmek üzere kuyruğa alınmış bir error olayı var. Temizlik removeAllListeners() çalıştırır ve onu yakalayacak işleyiciyi söker. Bir an sonra kuyruğa alınmış hata tetiklenir — boşluğa. Node kimsenin dinlemediği bir error olayı görür ve sözleşmesinin izin verdiği tek şeyi yapar: onu yakalanmamış bir istisna olarak fırlatır ve bu da süreci çökertir. Kullanıcının backend’i iki saniye takıldı ve uygulaması yok oldu.

Düzeltme (v1.6.8) neredeyse antiklimaks. Dinleyicileri söktükten hemen sonra, bilinçli olarak boş bir error işleyicisini yeniden bağla:

this.micWebSocket.removeAllListeners()
this.micWebSocket.on('error', () => {}) // absorb late error events

Asıl mesele o boş fonksiyon. Bekleyen hataya inecek bir yer verir — bir alıcı — böylece Node’un bir dinleyicisi olur ve asla yeniden fırlatmaz. Aynı commit, bir bağlantı denemesi yarı yolda başarısız olduğunda yarı açık soketleri de temizledi; böylece bozuk bir bağlantı, tetiklenmeyi bekleyen bir hatayla canlı bir soketi askıda bırakamaz. Commit yorumu bunu açıkça söylüyor: "Bu olmadan, bekleyen hatalar yakalanmamış istisnalar olur ve uygulamayı çökertir." On satır, tam bir çöküşü sessiz, kurtarılabilir bir kopmaya dönüştürdü.

Ders, düzeltmeden daha değerlidir: hâlâ sökmekte olabileceğin bir soket veya akış üzerinde removeAllListeners() çağırdığın her seferinde, önce boş bir error alıcısını yeniden bağla. İşlenmemiş bir error olayı bir günlük satırı değil, bir çöküştür.

Oturumu açık tutmak: önden yenile ve sekmede yenile

"Bağlı kal" yarısının bir "oturumu açık tut" ikizi vardı. Bu kümeden önce, GeekBye’ın kimlik doğrulama token’ı basitçe süresi dolar — bir süre sonra iş akışının ortasında, görebileceğin hiçbir sebep olmadan, törensizce giriş ekranına geri atılırdın.

v1.6.8 bunu her iki yönden de düzeltti:

  • Proaktif: bir zamanlayıcı token’ı süresi dolmadan önce yeniler; süre dolumu eksi akıllı bir tampon (token’ın ömrünün bir kesri, alt ve üst sınırlı) olarak zamanlanır. Fark etmeden önce yenilenmiş olursun.
  • Reaktif: yine de bir istek 401 olarak dönerse — saat kayması, kaçırılan bir zamanlayıcı, soğuk bir dizüstü — istemci bir kez yeniler ve orijinal isteği tam olarak bir kez yeniden dener. Tek bir koruma bayrağı bu yeniden denemeyi sınırlar; böylece kalıcı bir 401 asla sonsuz bir döngüye dönüşemez.

İnce ama kritik kısım: her iki yol da bir mutex arkasında. On istek aynı anda bir 401 alırsa, on yenileme başlatmazlar — hepsi devam eden tek yenilemeyi bekler ve sonra yeniden dener. Ve bir yenileme gerçekten kurtarılamadığında, uygulama oturumu zarifçe sonlandırır — net, on saniyelik bir "oturumunun süresi doldu, lütfen tekrar giriş yap" bildirimi — sessizce giriş ekranına tekmelemek yerine.

Yeniden bağlanma: önce istemciye ekle, sonra backend’e taşı

v1.6.9, kopan bir transkripsiyon bağlantısını ölümcül yerine kurtarılabilir hale getirdi. Backend, yukarı akış konuşma bağlantısının koptuğunu bildirdiğinde, istemci ölümcül bir hata göstermeyi bırakıp bunun yerine yeniden bağlandı — bilinçli olarak sakin bir UX ile: tek bir kehribar rengi "Yeniden bağlanılıyor…" bildirimi (deneme başına bir tane değil), geri geldiğinde yeşil bir "Yeniden bağlanıldı" bildirimi ve nihai "lütfen yeniden başlat" hatası yalnızca her deneme tükendikten sonra.

Sonra bu kümede sevdiğim bir şey: bir sürüm sonra, v1.6.10’da, o istemci tarafı yeniden bağlanma mantığını sildik — yaklaşık 113 satır — ve yeniden bağlanmayı, artık istemcinin yalnızca yansıttığı sessiz "yeniden bağlanılıyor" durum mesajları yayan backend’e taşıdık. Kurtarmayı istemcide kurduk, doğru sahibinin backend olduğuna karar verdik ve taşıdık. Bu bir kararsızlık değil; zor bir problemin dürüst yaşam döngüsü. Güvenilirlik mantığının bir bağlantının her iki ucuna bölünmesi kötü bir kokudur — bir sahip seç. (Backend sonunda bunun kesin sahibi oldu; bu da yapay zekâ not alıcının neden kötü Wi-Fi’de durduğu yazısında anlatılan yeniden bağlanma hikâyesidir.)

Aynı sürüm, kayıt süresi sınırlarını insancıllaştırdı: ham bir hata yerine, kaydın devam etmesine izin veren bir uyarı bildirimi ve gerçekten durduğunda net bir "Kayıt Sınırına Ulaşıldı" mesajı — dostane cümle yüzeye çıkarıldı, iç önek çıkarıldı.

Hepsini yönetecek tek bir yeniden deneme

v1.6.11’e gelindiğinde, aynı geri çekilmeli yeniden deneme mantığı üç farklı yere kopyala-yapıştır edilmişti — kimlik doğrulama, güncellemeler, oturum sökme. Böylece neyin yeniden denemeye değer olduğu hakkında tek bir ortak kurala sahip tek bir modülde birleştirildi: geçici ağ hataları (bağlantı sıfırlanması, zaman aşımı, DNS takılması, soket kopması) üstel geri çekilme ile yeniden denenir; gerçek bir sunucu hatası hızlıca başarısız olur, çünkü gerçek bir 4xx’i yeniden denemek yalnızca kullanıcının zamanını harcar. Bir sınıflandırıcı, birkaç adlandırılmış ön ayar, üç çağıran bunun üzerine taşındı.

Bu kümenin öğrettikleri

  1. İşlenmemiş bir error olayı bir çöküştür, bir günlük satırı değil. Node, dinleyicisi olmayan error olaylarını yeniden fırlatır. Hâlâ sökmekte olabileceğin bir sokette dinleyicileri kaldır ve boş bir error alıcısını yeniden bağlamalısın — yoksa bir kopan bağlantı tüm uygulamayı da beraberinde götürür.
  2. Kimlik bilgilerini süre dolmadan önce ve bir 401’de reaktif olarak yenile — her ikisinde de bir mutex ile. Proaktif seni oturumda tutar; reaktif uç durumları yakalar; mutex, N eşzamanlı çağıranın tam olarak tek bir yenilemeye yol açması demektir ve bir yeniden deneme sınırı, bozuk bir token’ın sonsuza dek döngüye giremeyeceği anlamına gelir.
  3. Yalnızca geçici hatalarda yeniden dene ve sınıflandırıcıyı merkezileştir. "Bu yeniden denemeye değer mi?" tek bir yere ait tek bir karardır. Gerçek bir hatayı yeniden denemek yalnızca daha yavaş bir başarısızlıktır.
  4. Yeniden bağlanmanın sahibinin kim olduğuna karar ver. Onu istemciye ekledik, sonra backend’e taşıdık. Her iki uçta yaşayan kurtarma mantığı bir hata üreticisidir; ona tek bir ev ver.

Bu, GeekBye v2’ye dönüşen güvenilirlik hikâyesinin üçüncü bölümüdür. Önceki bölüm için, bkz. 5.000 satır ses kodunu sildik ve transkriptler iki kez görünmeye başladı (v1.6.0); yeniden bağlanmanın sonunda nerede yerleştiği için, yapay zekâ not alıcın neden kötü Wi-Fi’de durur; ve tüm yay için, yazılımı kusursuzluğa kadar yayınlamanın anatomisi.