2 puan yazan GN⁺ 2024-11-23 | 1 yorum | WhatsApp'ta paylaş
  • Go kod tabanındaki net/http içinde, MaxBytesError.Error() tarafından döndürülen hata dizesi "http: request body too large" ifadesinin Hyrum's Law nedeniyle değiştirilemeyeceğini belirten bir yorum bulunuyor
  • Hyrum's Law, bir API'nin yeterince çok kullanıcısı olduğunda, resmi sözleşmede yer almayan gözlemlenebilir davranışlara bile birilerinin bağımlı hale geleceğini söyleyen ilkedir
  • Hata mesajı gibi önemsiz görünen dizeler bile dış kod tam ifadeye göre çalışıyorsa, değiştirildiği anda mevcut kodu bozabilir
  • Go içinde crypto/rsa ve internal/weak paketlerinde de benzer yorumlar var; bunlar rastgele akış davranışının ya da kesinleşmemiş semantiğin sabitlenmesi riskini ele alıyor
  • Bu sorun yalnızca Go'ya özgü değil; bu yüzden herkese açık API'ler ve kütüphaneler, istenmeyen davranışların fiilen standart haline gelmesini önleyecek şekilde tasarlanmalı

Go kodunda görülen Hyrum's Law

  • net/http/request.go içindeki MaxBytesError.Error() şu dizeyi döndürüyor
    • "http: request body too large"
    • İlgili yorumda “Due to Hyrum's law, this text cannot be changed.” yazıyor
  • Hyrum's Law, adını Hyrum Wright'tan alan bir ilke ve hyrumslaw.com'daki tanım şu şekilde
    • Bir API'nin kullanıcı sayısı yeterince arttığında, sözleşmenin neyi garanti ettiğinden bağımsız olarak sistemin tüm gözlemlenebilir davranışlarına birileri bağımlı hale gelir
  • MaxBytesError örneğinin özü, hata mesajının tam ifadesinin dış kod tarafından kullanılabiliyor olması
    • Küçük bir ifade değişikliği bile mevcut kodu bozabilir
    • http: request body too large arama sonuçlarında bu diziyi kullanan açık kaynak Go kodları görülebiliyor

Diğer Go paketleri ve dış kod tabanı örnekleri

  • crypto/rsa içindeki rastgele akış bağımlılığı

    • crypto/rsa/rsa.go içindeki EncryptOAEP için Hyrum's Law ile ilgili bir yorum bulunuyor
    • Bu fonksiyon rastgele akış için deterministik çalışma garantisi vermiyor, ancak MaybeReadByte uygulanmadığı için birilerinin mevcut davranışa bağımlı hale gelmesi mümkün
    • crypto/rsa/pss.go içindeki SignPSS de aynı bağlamda bir yorum içeriyor
    • Her iki durumda da iyi tanımlanmış sayıda rastgele bayt, iyi tanımlanmış bir şekilde şifreli metne ya da imzaya dahil edildiğinden, bu kabul edilebilir bir taahhüt olarak ele alınıyor
  • internal/weak içindeki semantik sabitlenme riski

    • internal/weak, bu pakete ve referans fonksiyonlarına go:linkname ile erişimin toolchain tarafından açıkça yasaklandığını belirtiyor
    • Bu paketin semantiği öneri sürecinden geçmedi; işlevsellik açığa çıkarılırsa Hyrum's Law nedeniyle mevcut semantik sabitlenebilir
  • Go dışında da tekrarlanan bir örüntü

    • Hyrum's Law atıfları yalnızca Go ile sınırlı değil
    • grep.app üzerindeki çok dilli arama sonuçlarında farklı dillerden örnekler görülebiliyor
    • Python'daki urllib.parse ve Pixar OpenUSD'deki array.h de ilgili kod tabanı örnekleri
    • JavaScript'in evrimi de birçok tuhaf ve amaçlanmamış davranışa yönelik yaygın bağımlılıkların fiilen standart haline gelmesinin bir örneği

Değişiklik yapmadan önce kontrol edilmesi gerekenler

  • Kod değiştirirken yalnızca belgelenmiş API'yi değil, dış kodun bağımlı olabileceği gözlemlenebilir davranışları da hesaba katmak gerekir
  • En baştan, istenmeyen davranışlara bağımlılık ihtimalini azaltan sistem tasarımına ihtiyaç vardır

1 yorum

 
GN⁺ 2024-11-23
Hacker News yorumları
  • Hyrum Yasası yararlı bir gözlem, ancak ona takılıp yanlış sonuçlara varmamak gerekir
    Bir fonksiyonun toplam çalışma süresi de gözlemlenebilir bir özellik olduğundan, fonksiyonu daha hızlı çalışacak şekilde optimize etmek bile kırıcı bir değişiklik sayılabilir. Sonuçta kuyruk birden fazla hızlı boşalıp deadlock oluşabilir. Yine de kullanıcıların %99,99999999'u, hiçbir çaba harcamadan kodun hızlanmasından hoşlanacaktır
    Sonuçta neyin kırıcı değişiklik olduğu teknik bir sözleşmeden ziyade sosyal bir sözleşme olmak zorundadır. Aksi halde kelimenin tam anlamıyla hiçbir şeyi değiştiremezsiniz. Kütüphane yazarları API'de değişmeyecek kısımları belgelemeli, makul davranmalı ve kullanıcılara empati göstermeli; kütüphane kullanıcıları da belgelenmemiş bir arayüzü kritik bağımlılık haline getirmenin kendi sorumlulukları olduğunu anlamalı ve yazarlara empati göstermelidir

    • Açık kaynak kütüphane yazarları için yukarıdakilerin hepsinin doğru olduğunu düşünüyorum
      Ancak başka bir açıdan bakınca Hyrum Yasası ne teknik ne de sosyal bir sözleşmedir; yeterince yaygın kullanılan sistemlerde ortaya çıkan beliren bir teknik özelliktir
      Bu özelliğe nasıl yanıt verileceği sosyal bağlama bağlıdır. Bir FOSS bakımcısıysanız, bir optimizasyon %99,99'u hızlandırıyor ve yalnızca %0,01'in kodunu düzeltmesi ya da yeni API'ye geçmesi gerekiyorsa bunu yayımlarsınız. Büyük bir teknoloji şirketindeyseniz hem optimizasyonu yapmak hem de şirket içinde %0'ın bile kırılmamasını sağlamak zorundasınız; bu yüzden birkaç ekiple çalışıp bir uzlaşma noktası bulursunuz. Kurumsal yazılım şirketiyseniz, yalnızca %0,1 kırılsa bile o kullanıcı en büyük 5 sözleşmeden biriyse yayımlamazsınız
    • Bir zamanlar çok verimsiz bir rutini yaklaşık 100 saniyeden 0,1 saniyeye indirmiştim; bu yüzden raporlama sistemi bozuldu
      Çünkü asıl yazar birkaç asenkron fonksiyonu çağırmış ve eski, yavaş rutin bittiği sıralarda bu fonksiyonların hepsinin de bitmiş olacağını varsaymıştı. Tam olarak ne olduğunu anlamak inanılmaz uzun sürdü
    • 1980'lerde gerçekten böyle sorunlar vardı
      Bu yüzden PC'lerde hızı düşüren bir turbo düğmesi bulunurdu; 8 bit bilgisayarlarda daha hızlı CPU'lar olmasına rağmen on yıl boyunca hız artırılmadı. Günümüzde neredeyse her şey iki ya da daha fazla CPU üzerinde çalıştığından, yeterince hızlı olup olmadığı dışında fonksiyon çalışma süresine bağımlılık neredeyse hiç yok. Gömülü sistemlerde bile tek bir CPU'nun üretimden kalkmasını yaşadıktan sonra bu tür bağımlılıklardan kaçınmaya çalışılıyor
    • Bir gün load bearing teapot hakkında bir lightning talk yapmak istiyorum
      Dahili bir API'de HTTP Status 418'i neden kritik bağımlılık haline getirdiğimizin ve verilen kısıtlar altında bunun neden en az kötü seçenek olduğunun hikâyesi
    • Fonksiyonun toplam çalışma süresi gibi şeyler fonksiyon yazarının kontrolü altında olmadığından, bu mantık neredeyse absürt görünüyor
      Çalışma ortamı, o anki sistem yükü, GC'nin çalışması gibi şeylerin hepsi etkileyebilir
      Özetle, makinede ortaya çıkan beliren davranışı amaçlanan arayüz ya da herhangi türden bir sözleşme olarak görmüyorum. Dolayısıyla biri istenmeyen bir davranışa bağımlı olmuş olsa bile, ince bir bug'ı düzeltmenin kırıcı değişiklik sayılmaması gibi, bunu da kırıcı değişiklik olarak görmem
      Bu durum daha çok Go'nun geriye dönük uyumluluğa ne kadar güçlü bağlı olduğunun kanıtı gibi görünüyor
  • Haha, crypto/rsa yorumunu ben yazdım. Go’da Hyrum Yasası ve geriye dönük uyumluluk https://go.dev/doc/go1compat gerçekten çok ciddiye alınıyor.
    Örneğin çeşitli GenerateKey fonksiyonlarında algoritmanın sabitlenmemesi için MaybeReadByte https://pkg.go.dev/crypto/internal/randutil#MaybeReadByte ile rastgele akıştan fazladan bir bayt okunuyor. Daha dün de nil public key içeren bir private ECDSA key’in eskiden çalıştığı ama artık çalışmadığına dair bir rapor geldi; muhtemelen tekrar çalışır hâle getirmemiz gerekecek https://go.dev/issue/70468
    Map üzerinde dolaşma, iç uygulamanın açığa çıkmaması için rastgele sıra kullanıyor. rand.Rand çıktısı uyumluluk vaadinin bir parçası sayıldığı için onu iyileştirmek adına epey büyük bir çaba gerekti https://go.dev/blog/randv2 https://go.dev/blog/chacha8rand
    Dokümantasyona hangi vaatleri yazacağımızı ve hangi davranışlar için “değişebilir” diye açıkça belirteceğimizi sürekli tartışıyoruz. Çünkü dokümante edilmiş şeylerin asla değiştirilemeyeceğini, “değişebilir” diye açıkça belirtilmemiş olanları değiştirmenin de muhtemelen zor olduğunu biliyoruz https://go-review.googlesource.com/c/go/+/598336/comment/5d6...

    • Map dolaşma sırasının değiştirilmesi, belirli bir sıraya bağımlı olunmasını engelleyerek gelecekteki kırıcı değişiklikleri azaltmaya yardımcı olur; ancak değişiklik yapıldığı sırada, önceki sıra davranışına bağımlı kodlar için kırıcı bir değişiklikti.
      Yine de bunun değerli bir ödünleşim olduğunu düşünüyorum. Go’yu çok kullanıyorum ve güçlü geriye dönük uyumluluğu seviyorum; ama Go geliştiricilerinin performansı iyileştirme ve özellik ekleme özgürlüğü artacaksa kırıcı değişiklik oranının biraz yükselmesini de memnuniyetle kabul ederim.
      Diğer ekosistem kullanıcılarının katlandığı cehenneme, örneğin Python gibi durumlara bakınca, bu düşüncede yalnız olmadığımı düşünüyorum.
    • MaybeReadByte’ın çeşitli GenerateKey fonksiyonlarında kullanıldığını söyledim ama ed25519’da öyle yapılmıyor gibi görünüyor.
      ed25519.NewKeyFromSeed() ortaya çıkmadan önce private key’den public Ed25519 key türetmenin tek yolu buydu ve neredeyse kesin olarak buna bağımlı kod yazmıştım. Pek hoşuma gitmemişti ama yapılabilecek tek şey oydu; o yüzden hatırlaması kolay.
      Yine de ed25519.GenerateKey dokümanının çıktının deterministik olduğunu açıkça belirtmesi iyi. Go kriptografi API’lerinde yerleşmiş davranışları araştırıp koruma ve yeni yerleşmelerin oluşmasını engelleme konusunda gerçekten iyi iş çıkardıklarını düşünüyorum.
    • nil key örneği, böyle durumları desteklemenin ne kadar akıl kârı olduğunu sorgulatıyor.
      Kötü şöhretli A20 line (https://en.wikipedia.org/wiki/A20_line) gibi, bu bozuk davranışı sonsuza dek taşımak zorunda kalıyorsunuz.
    • İronik biçimde, eskiden Go ile bir yük dengeleyici yazmıştım ve rastgele map dolaşma sırasına dayanıyordu.
    • Go’nun en az takdir edilen yönlerinden biri bu. 12 yıl önce yazılmış kod hâlâ olduğu gibi çalışıyor.
  • Özel olarak bahsedilen sorunun çözümü, string tabanlı hatalar kullanmak yerine sentinel error kullanmak https://thomas-guettler.de/go/wrapping-and-sentinel-errors
    Daha genel olarak, API tüketicisinin teknik olmayan string’lere az da olsa bağımlı olmak isteyeceği kodlar üretmemek gerekir. Önceden tanımlı hata değerleri, tipler ya da teknik olmayan string’i içeren sabitler gibi dilin birinci sınıf yapı taşları kullanılırsa, API tüketicisi string’i doğrudan hardcode etmek yerine dönüş değerini sabitle karşılaştırabilir.
    Hyrum Yasası kesinlikle var, ama etkisi azaltılabilir.

    • Can sıkıcı olan, söz konusu hatanın zaten sentinel error olması.
      Bağlantılı aramada en üst neden gibi görünen Grafana, string karşılaştırması yerine errors.As(&http.MaxBytesError{}) kullanmalıydı.
      Hyrum Yasası’nın özü, API’yi ne kadar iyi tasarlarsanız tasarlayın bunun fark etmemesi. İnsanlar sözleşmeye değil, davranışa bağımlı hâle geliyor.
    • Bu örnekte sorumluluk sağlayıcıda değil, tüketicide.
      Hâlâ err.String() == "no more tea available." kontrolü yapan kod yazabilirsiniz. Bunu yapmamak gerektiğine katılıyorum, ama bunu yapmayı engelleyen bir şey yok.
      Üstelik errors.Is Go’ya nispeten yakın zamanda eklendiği için, insanların hataları bu şekilde kontrol ettiği dönemde literal string’i kontrol etmek daha kolaydı. Go’da API sağlayıcısı, tüketicinin .String() dönüş değerini kontrol etmesini engelleyemez.
    • Birkaç yıl öncesine kadar string hata karşılaştırması bunu yapmanın tek yoluydu ve Go’nun geriye dönük uyumluluk vaadi var.
    • Ham hata string’ini kontrol eden kod düpedüz kötü koddur ve Go’nun geriye dönük uyumluluk garantisinin dışında tutulmalıdır.
      Özellikle standart kütüphanede bunun için neredeyse hiçbir mazeret yok.
    • Sorun Go’nun ilk tasarımında. Uzun süre boyunca string tabanlı hatalar tek yoldu; yanlış hatırlamıyorsam bazı standart kütüphane paketlerinde hâlâ var, tüm ekosistemi saymıyorum bile.
      Programlama dillerinin tarihini bilerek görmezden gelip “yaparken tasarlarız” yaklaşımını seçerseniz sonuç bu olur.
  • Hyrum yasasına karşı koymanın yolları da ilginç bir konu
    Bir olasılık, insanların bağımlı olmamasını istediğiniz yerlere rastgelelik katmak
    Yanlış hatırlamıyorsam QUIC protokolü bunu yapıyor. Mevcut sürümde kullanılmayan bir alan var, ancak yönlendiricilerin o alan üzerinden paketleri tanımaya başlamasını engellemek için spesifikasyon, bunun null byte değil rastgele bir değer olarak ayarlanmasını şart koşuyor
    Kaynak muhtemelen burası: https://www.rfc-editor.org/rfc/rfc9000#section-17.2.1
    “Unused alanının değeri sunucu tarafından rastgele bir değere ayarlanır. İstemci bu alanın değerini mutlaka yok saymalıdır. [...] QUIC'in diğer sürümlerinin benzer bir tavsiyede bulunmayabileceğine dikkat edin”
    Bildiğim kadarıyla buna greasing deniyor ve ossification'ı önlemek için yapılıyor

    • GREASE, RFC 8701'de oluşturulmuş bir kısaltma; “Generate Random Extensions And Sustain Extensibility” anlamına geliyor ve başlangıçta TLS bağlamında kullanılmıştı
      https://www.rfc-editor.org/rfc/rfc8701.html
      Bu RFC'nin en eski taslağı 2016 ortalarına kadar gidiyor ve muhtemelen terimin kamuya açık olarak ilk kez ortaya çıktığı zaman da bu: https://datatracker.ietf.org/doc/html/draft-davidben-tls-gre...
    • Harika. QUIC'e epey aşinayım ama bunu bilmiyordum
      10 yıl sonra uyanıp o bitlere gerçekten ihtiyaç duyulduğunu, ama 10 markadan 20 yönlendirici modelinin o bitlerin mutlaka belirli bir şekilde olması gerektiğine karar vermiş olduğunu görmek kadar berbat bir şey yok
      Karşı tarafta checksum ya da şifreleme olup da bitlere dokunulunca bozuluyorsa bonus puan. Middlebox'ların “akıllı hack”leri gerçekten baş belası
  • Bu, stringly typed yazılımın iyi bir örneği
    Go tasarımcıları exception istemiyordu, ama panic/recover ile hâlâ benzer bir şey var ve tipsiz hatalar zararlı. Öte yandan pattern matching olmadan tipli hatalar nasıl ele alınabilir? Çünkü çoğu dildeki catch, ilkel bir pattern matching biçimi
    https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...

    • Go'da tipli hatalar var. Sadece bu durumda kullanılmamış
  • Eskiden çalıştığım bir yerde hata mesajındaki bir yazım hatasını fark edip düzeltmiştim; sonra da o hatalı metne bağımlı bağlantı ağının o kadar derin olduğunu gördük ki pratikte düzeltmek mümkün değildi ve sonunda yazım hatalı metne geri dönmek zorunda kaldık
    Hâlâ aklıma takılır

  • Bu bir tür Hyrum yasası, ama aslında sadece Go'ya özgü Go
    Hata bir enum tipi olsaydı, tüketici yalnızca string substitution ile değiştirebilirdi. Bunun yerine string'i tip gibi kullandığınız için, tüketicinin buna nasıl bağımlı olabileceğini bilemezsiniz. Hata string'inin ortasındaki 6 karakteri kontrol ediyor olabilir ve değiştirirseniz bozulabilir
    Onlarca yıldır başka dillerde daha iyi alternatifler kullanılıyor olmasına rağmen, bir başka korkunç ve anakronik tasarım kararı. İlk hatalar değiştirilemezlikle birleşince sonsuza dek bağlanıp kalıyorsunuz

    • Ne yazık ki o yorum özünde yanlış. Çoğu durumda string'in kendisi resmi API idi
  • Bu yasanın sağlamlık ilkesi, yani Postel yasasının tam tersi olması ilginç
    “Gönderirken muhafazakâr, alırken hoşgörülü ol”
    Girdiyi hoşgörüyle kabul ediyorsanız, hangi şekillerde hoşgörülü davrandığınızı anlamalı ve en azından dahili olarak belgelemelisiniz. Hyrum yasası yüzünden, büyük ölçekli kod tabanı değişikliklerinden sonra bile bu yolların hepsini sonsuza dek desteklemek zorunda kalırsınız
    Tam da bu yüzden “aldığına karşı hoşgörülü” API'ler oluşturmaktan kaçınıyorum

    • Ben de o tarafı tercih ediyorum
      API'ye alınan veriler için ölçütleri gevşek tutarsanız, sonunda o veriyi hangi kanonik biçime dönüştüreceğinize karar vermek zorunda kalırsınız. Ve bu karar neredeyse her zaman bir şekilde kullanıcıyı şaşırtan davranışlara yol açıyor gibi görünüyor
  • Paket yazarlarının bu sorunu kabullenme derecesi farklı gibi görünüyor. Birkaç gün önce json paketinde şöyle bir yorum gördüm
    isValidNumber, s'nin geçerli bir JSON sayı literali olup olmadığını bildirir
    isValidNumber dahili bir uygulama ayrıntısı olmalı, ancak yaygın kullanılan paketler ona linkname ile erişiyor
    hall of shame'in önde gelen üyeleri arasında github.com/bytedance/sonic de var

  • API yayımlarken öğrendiklerim
    İstemciler, yayımlayanın amaçladığı yol olmasa bile işlerini bitirmek için ne gerekiyorsa yapar. İstemciler dokümantasyonu okumaz. Yeterince çok istemci bir davranışa bağımlı hâle gelirse bug da API'nin parçası olur. API çağrısı sayısı önemle mutlaka örtüşmez
    Bu yüzden API geliştirirken mümkün olduğunca beta API'leri erken çıkarıp nasıl kullanıldığını izleyerek sürprizleri azaltmaya çalışıyorum. Çoğu durumda önceki sürümü desteklemeyi sürdürerek majör sürümü yükseltiyorum. Bunun için API'nin SLA'sını tanımlamak gerekiyor