Golang'da Hyrum's Law'ın uygulama örnekleri
(abenezer.org)- Go kod tabanındaki
net/httpiç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/rsaveinternal/weakpaketlerinde 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.goiçindekiMaxBytesError.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 largearama sonuçlarında bu diziyi kullanan açık kaynak Go kodları görülebiliyor
Diğer Go paketleri ve dış kod tabanı örnekleri
-
crypto/rsaiçindeki rastgele akış bağımlılığıcrypto/rsa/rsa.goiçindekiEncryptOAEPiçin Hyrum's Law ile ilgili bir yorum bulunuyor- Bu fonksiyon rastgele akış için deterministik çalışma garantisi vermiyor, ancak
MaybeReadByteuygulanmadığı için birilerinin mevcut davranışa bağımlı hale gelmesi mümkün crypto/rsa/pss.goiçindekiSignPSSde 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/weakiçindeki semantik sabitlenme riskiinternal/weak, bu pakete ve referans fonksiyonlarınago:linknameile 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.parseve Pixar OpenUSD'dekiarray.hde 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
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
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
Çü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ü
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
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
Ç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/rsayorumunu 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
GenerateKeyfonksiyonlarında algoritmanın sabitlenmemesi içinMaybeReadBytehttps://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/70468Map ü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/chacha8randDokü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...
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şitliGenerateKeyfonksiyonları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.GenerateKeydokü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.Kötü şöhretli A20 line (https://en.wikipedia.org/wiki/A20_line) gibi, bu bozuk davranışı sonsuza dek taşımak zorunda kalıyorsunuz.
Ö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.
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.
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.IsGo’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.Özellikle standart kütüphanede bunun için neredeyse hiçbir mazeret yok.
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
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...
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/recoverile 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 dildekicatch, ilkel bir pattern matching biçimihttps://learn.microsoft.com/en-us/dotnet/csharp/language-ref...
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
https://en.wikipedia.org/wiki/HTTP_referer
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
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
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
jsonpaketinde şöyle bir yorum gördümisValidNumber,s'nin geçerli bir JSON sayı literali olup olmadığını bildiririsValidNumberdahili bir uygulama ayrıntısı olmalı, ancak yaygın kullanılan paketler onalinknameile erişiyorhall of shame'in önde gelen üyeleri arasında
github.com/bytedance/sonicde varAPI 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