Contoh Penerapan Hyrum's Law di Golang
(abenezer.org)- Dalam basis kode Go,
net/httpmemiliki komentar bahwa string error"http: request body too large"yang dikembalikan olehMaxBytesError.Error()tidak dapat diubah karena Hyrum's Law - Hyrum's Law adalah prinsip bahwa ketika pengguna API sudah cukup banyak, seseorang akan bergantung bahkan pada perilaku teramati yang tidak termasuk dalam kontrak resmi
- Bahkan string yang tampak sepele seperti pesan error pun, jika kode eksternal beroperasi dengan mencocokkan frasa persisnya, dapat merusak kode yang sudah ada saat diubah
- Di internal Go, komentar serupa juga ada di
crypto/rsadaninternal/weak, yang membahas risiko perilaku stream acak atau semantik yang belum final menjadi tetap - Karena ini bukan masalah yang terbatas pada Go, API publik atau library perlu dirancang agar perilaku yang tidak disengaja tidak mengeras seolah-olah menjadi standar
Hyrum's Law yang ditemukan dalam kode Go
MaxBytesError.Error()dinet/http/request.gomengembalikan string berikut"http: request body too large"- Komentar terkait berbunyi “Due to Hyrum's law, this text cannot be changed.”
- Hyrum's Law adalah prinsip yang dinamai dari Hyrum Wright, dan definisi di hyrumslaw.com adalah sebagai berikut
- Ketika pengguna API sudah cukup banyak, terlepas dari apa yang dijanjikan dalam kontrak, seseorang akan bergantung pada semua perilaku sistem yang dapat diamati
- Inti dari kasus
MaxBytesErroradalah bahwa frasa persis dari pesan error dapat digunakan oleh kode eksternal- Perubahan frasa sekecil apa pun dapat merusak kode yang sudah ada
- Dari hasil pencarian
http: request body too large, terlihat kode open source Go yang menggunakan string tersebut
Contoh dari paket Go lain dan basis kode eksternal
-
Ketergantungan pada stream acak di
crypto/rsaEncryptOAEPdicrypto/rsa/rsa.gomemiliki komentar terkait Hyrum's Law- Fungsi ini tidak menjanjikan eksekusi deterministik terhadap stream acak, tetapi karena
MaybeReadBytetidak diterapkan, ada kemungkinan seseorang bergantung pada perilaku saat ini SignPSSdicrypto/rsa/pss.gojuga berisi komentar dengan konteks yang sama- Dalam kedua kasus, sejumlah byte acak yang terdefinisi dengan baik dimasukkan ke dalam ciphertext atau tanda tangan dengan cara yang terdefinisi dengan baik, sehingga diperlakukan sebagai janji yang dapat ditoleransi
-
Risiko semantik
internal/weakmenjadi tetapinternal/weakmenyatakan bahwa toolchain secara eksplisit melarang akses ke paket tersebut dan fungsi referensinya melaluigo:linkname- Semantik paket ini belum melalui proses proposal, dan jika fungsinya diekspos, semantik yang ada dapat menjadi tetap karena Hyrum's Law
-
Pola yang berulang di luar Go
- Penyebutan Hyrum's Law tidak terbatas pada Go
- Dari hasil pencarian multibahasa di grep.app, dapat dilihat contoh dalam berbagai bahasa
urllib.parsemilik Python danarray.hmilik Pixar OpenUSD juga merupakan contoh basis kode terkait- Evolusi JavaScript juga terkait dengan kasus ketika ketergantungan luas pada berbagai perilaku aneh dan tidak disengaja secara de facto menjadi standar
Hal yang perlu diperiksa sebelum mengubah
- Saat mengubah kode, perlu mempertimbangkan bukan hanya API yang terdokumentasi, tetapi juga perilaku teramati yang mungkin menjadi ketergantungan kode eksternal
- Diperlukan desain sistem yang sejak awal mengurangi kemungkinan ketergantungan pada perilaku yang tidak disengaja
1 komentar
Komentar Hacker News
Hukum Hyrum adalah pengamatan yang berguna, tetapi jangan terpaku padanya lalu menarik kesimpulan yang keliru
Total waktu eksekusi sebuah fungsi juga merupakan properti yang dapat diamati, jadi bahkan mengoptimalkan fungsi agar lebih cepat pun bisa dianggap sebagai perubahan yang merusak. Misalnya, antrean tiba-tiba dikosongkan terlalu cepat sehingga terjadi deadlock. Meski begitu, 99,99999999% pengguna kemungkinan akan senang jika kode menjadi lebih cepat tanpa usaha apa pun
Pada akhirnya, apa yang disebut perubahan yang merusak tidak bisa tidak merupakan kontrak sosial, bukan kontrak teknis. Kalau tidak, secara harfiah tidak ada yang bisa diubah. Penulis library harus mendokumentasikan bagian API yang tidak akan berubah, bertindak secara masuk akal, dan berempati kepada pengguna; sementara pengguna library harus memahami bahwa menjadikan antarmuka yang tidak terdokumentasi sebagai dependensi inti adalah tanggung jawab mereka sendiri, dan juga berempati kepada penulisnya
Namun dari sudut pandang lain, Hukum Hyrum bukanlah kontrak teknis maupun kontrak sosial, melainkan properti teknis emergen yang muncul pada sistem yang digunakan dalam skala cukup besar
Cara menanggapi properti itu bergantung pada konteks sosial. Jika Anda maintainer FOSS, ketika sebuah optimasi membuat 99,99% pengguna lebih cepat dan hanya 0,01% yang perlu memperbaiki kode atau pindah ke API baru, Anda akan merilisnya. Jika Anda perusahaan teknologi besar, Anda harus melakukan optimasi, tetapi juga tidak boleh ada 0% pun yang rusak di dalam perusahaan, sehingga Anda bekerja sama dengan beberapa tim untuk mencari kompromi. Jika Anda perusahaan software enterprise, meski hanya 0,1% yang rusak, bila pengguna itu termasuk salah satu dari 5 kontrak terbesar, Anda tidak akan merilisnya
Penyebabnya, penulis aslinya memanggil beberapa fungsi asinkron, lalu berasumsi bahwa saat rutin lama yang lambat itu selesai, semua fungsi tersebut juga sudah selesai. Butuh waktu sangat lama untuk mencari tahu persis apa yang terjadi
Karena itu PC dilengkapi tombol turbo untuk menurunkan kecepatan, dan komputer 8-bit tidak menaikkan kecepatannya selama satu dekade penuh meski memiliki CPU yang lebih cepat. Sekarang hampir semuanya berjalan di dua CPU atau lebih, sehingga selain sekadar cukup cepat, hampir tidak ada lagi yang bergantung pada waktu eksekusi fungsi. Bahkan di embedded, setelah mengalami CPU tunggal dihentikan produksinya, orang berusaha menghindari dependensi semacam itu
Ceritanya tentang mengapa HTTP Status 418 dijadikan dependensi inti dalam API internal, dan mengapa di bawah batasan yang ada itu adalah pilihan yang paling tidak buruk
Lingkungan operasi, beban sistem saat itu, eksekusi GC, dan lain-lain semuanya bisa berpengaruh
Singkatnya, saya tidak menganggap perilaku emergen yang muncul dari mesin sebagai antarmuka yang disengaja atau kontrak jenis apa pun. Jadi, meskipun seseorang bergantung pada perilaku yang tidak disengaja, seperti halnya memperbaiki bug halus tidak dianggap sebagai perubahan yang merusak, ini pun tidak saya anggap sebagai perubahan yang merusak
Kasus ini lebih terlihat sebagai bukti bahwa Go sangat kuat berkomitmen pada kompatibilitas ke belakang
Haha, komentar
crypto/rsaitu saya yang menulisnya. Di Go, Hukum Hyrum dan kompatibilitas mundur https://go.dev/doc/go1compat benar-benar ditanggapi dengan seriusMisalnya, pada beberapa fungsi
GenerateKey, mereka membaca satu byte tambahan dari stream acak denganMaybeReadBytehttps://pkg.go.dev/crypto/internal/randutil#MaybeReadByte agar algoritmanya tidak terkunci. Baru kemarin juga ada laporan bahwa private ECDSA key dengan public key nil dulu berfungsi tetapi sekarang tidak, jadi mungkin harus dibuat berfungsi lagi https://go.dev/issue/70468Iterasi map menggunakan urutan acak agar implementasi internal tidak terekspos. Output
rand.Randdianggap sebagai bagian dari janji kompatibilitas, sehingga diperlukan upaya yang cukup besar untuk memperbaikinya https://go.dev/blog/randv2 https://go.dev/blog/chacha8randSelalu ada diskusi tentang janji apa yang harus ditulis dalam dokumentasi dan perilaku mana yang harus dinyatakan “dapat berubah”. Sebab mereka tahu bahwa hal yang sudah didokumentasikan sama sekali tidak bisa diubah, dan hal yang tidak dinyatakan “dapat berubah” pun mungkin sulit diubah https://go-review.googlesource.com/c/go/+/598336/comment/5d6...
Meski begitu, saya melihatnya sebagai kompromi yang bernilai. Saya banyak memakai Go dan menyukai kompatibilitas mundur yang kuat, tetapi jika itu memberi developer Go lebih banyak kebebasan untuk meningkatkan performa dan menambahkan fitur, saya bersedia menerima rasio breaking change yang sedikit lebih tinggi
Melihat neraka yang harus ditanggung pengguna ekosistem lain, misalnya Python, saya rasa pemikiran seperti ini bukan hanya milik saya
MaybeReadBytedipakai di beberapa fungsiGenerateKey, tetapi sepertinya itu tidak dilakukan di ed25519Sebelum
ed25519.NewKeyFromSeed()ada, itu adalah satu-satunya cara untuk menurunkan public Ed25519 key dari private key, dan saya hampir pasti pernah menulis kode yang bergantung padanya. Saya tidak terlalu menyukainya, tetapi karena hanya itu yang bisa dilakukan, mudah diingatNamun bagus bahwa dokumentasi
ed25519.GenerateKeymenyatakan output-nya deterministik. Saya rasa mereka benar-benar telah bekerja dengan baik dalam menyelidiki dan mempertahankan perilaku yang sudah mengeras di API kriptografi Go, sekaligus mencegah penguncian perilaku baruSeperti A20 line yang terkenal buruk (https://en.wikipedia.org/wiki/A20_line), perilaku rusak ini jadi harus dibawa selamanya
Secara spesifik, solusi untuk masalah yang disebutkan adalah tidak memakai error berbasis string, melainkan memakai sentinel error https://thomas-guettler.de/go/wrapping-and-sentinel-errors
Secara lebih umum, jangan membuat kode yang membuat konsumen API tergoda untuk bergantung sedikit pun pada string nonteknis. Dengan memakai komponen kelas satu dalam bahasa, seperti nilai error yang sudah didefinisikan, tipe, atau konstanta yang berisi string nonteknis, konsumen API bisa membandingkan nilai kembalian dengan konstanta alih-alih meng-hardcode string secara langsung
Hukum Hyrum jelas ada, tetapi dampaknya bisa dikurangi
Grafana, yang tampaknya menjadi penyebab teratas dalam pencarian terkait, seharusnya memakai
errors.As(&http.MaxBytesError{})alih-alih perbandingan stringInti dari Hukum Hyrum adalah, sebagus apa pun API dirancang, itu tidak terlalu berpengaruh. Orang akan bergantung pada perilaku, bukan pada kontrak
Anda tetap bisa menulis kode yang memeriksa
err.String() == "no more tea available.". Saya setuju bahwa itu tidak seharusnya dilakukan, tetapi tidak ada yang mencegahnyaSelain itu,
errors.Isditambahkan ke Go relatif belum lama ini, jadi pada saat orang memeriksa error dengan cara seperti ini, memeriksa string literal memang lebih mudah. Di Go, penyedia API tidak bisa mencegah konsumen memeriksa nilai kembalian.String()Terutama di standard library, hampir tidak ada alasan yang bisa dibenarkan
Inilah yang terjadi jika Anda sengaja mengabaikan sejarah bahasa pemrograman lalu memilih pendekatan “mari kita rancang sambil jalan”
Cara melawan Hukum Hyrum juga merupakan topik yang menarik
Salah satu kemungkinannya adalah memasukkan randomness pada bagian yang tidak diharapkan untuk diandalkan orang
Kalau ingatan saya benar, protokol QUIC melakukan hal seperti ini. Di versi saat ini ada field yang tidak digunakan, tetapi agar router tidak mulai mengidentifikasi paket berdasarkan field itu, spesifikasinya mengharuskan field tersebut disetel ke nilai acak, bukan byte null
Sumbernya mungkin di sini: https://www.rfc-editor.org/rfc/rfc9000#section-17.2.1
“Nilai field Unused disetel oleh server ke nilai arbitrer. Klien harus mengabaikan nilai field ini. [...] Perhatikan bahwa versi QUIC lain mungkin tidak memberikan rekomendasi serupa”
Setahu saya hal seperti ini disebut greasing, dan dimaksudkan untuk mencegah ossification
https://www.rfc-editor.org/rfc/rfc8701.html
Draf paling awal RFC ini berasal dari pertengahan 2016, dan kemungkinan besar itulah kemunculan publik pertama istilah ini: https://datatracker.ietf.org/doc/html/draft-davidben-tls-gre...
Tidak ada yang lebih mengerikan daripada bangun 10 tahun kemudian ketika bit-bit itu benar-benar dibutuhkan, lalu mendapati 20 model router dari 10 merek sudah memutuskan bahwa bit-bit tersebut harus dalam bentuk tertentu
Nilai tambah kalau di sisi lain ada checksum atau enkripsi sehingga semuanya rusak bila bit disentuh. “Peretasan cerdas” dari middlebox benar-benar bikin pusing
Ini contoh bagus dari perangkat lunak stringly typed
Para perancang Go tidak menginginkan exception, tetapi dengan
panic/recovermasih ada sesuatu yang mirip, dan error tanpa tipe itu berbahaya. Sebaliknya, bagaimana menangani error bertipe tanpa pattern matching? Karenacatchdi kebanyakan bahasa pada dasarnya adalah pattern matching yang primitifhttps://learn.microsoft.com/en-us/dotnet/csharp/language-ref...
Dulu di suatu tempat kerja, saya menemukan typo pada pesan error dan memperbaikinya, lalu menyadari bahwa jejaring yang bergantung pada teks bertypo itu terlalu dalam sehingga praktis tidak bisa diperbaiki, dan akhirnya saya harus mengembalikannya ke teks yang bertypo
Sampai sekarang masih mengganggu pikiran
https://en.wikipedia.org/wiki/HTTP_referer
Ini semacam Hukum Hyrum, tetapi sebenarnya hanya Go ala Go
Jika error-nya bertipe enum, konsumen bisa mengubahnya hanya dengan substitusi string. Sebaliknya, karena string dipakai seolah-olah tipe, kita tidak bisa tahu bagaimana konsumen mungkin bergantung padanya. Bisa saja mereka hanya memeriksa 6 huruf di tengah string error, lalu perubahan akan membuatnya rusak
Ini satu lagi keputusan desain yang buruk dan anakronistis, padahal alternatif yang lebih baik sudah digunakan di bahasa lain selama puluhan tahun. Jika kesalahan awal berpadu dengan ketidakmungkinan untuk mengubahnya, kita akan terikat selamanya
Menarik bahwa hukum ini tepat berkebalikan dengan prinsip robustness, yaitu Hukum Postel
“Konservatif saat mengirim, liberal saat menerima”
Jika menerima input secara liberal, kita harus memahami dalam hal apa kita bersikap liberal dan setidaknya mendokumentasikannya secara internal. Karena Hukum Hyrum, semua cara itu jadi harus didukung selamanya, bahkan setelah perubahan besar pada codebase
Justru karena alasan itulah saya tidak mau membuat API yang “liberal dalam menerima”
Jika kriteria data yang diterima API dibuat longgar, pada akhirnya kita harus memutuskan bagaimana memoles data itu menjadi suatu bentuk kanonis. Dan keputusan itu hampir selalu tampaknya berujung pada perilaku yang mengejutkan pengguna dalam satu atau lain cara
Tampaknya tiap penulis paket punya tingkat penerimaan yang berbeda terhadap masalah ini. Beberapa hari lalu saya melihat komentar seperti ini di paket
jsonisValidNumbermelaporkan apakahsmerupakan literal angka JSON yang validisValidNumberseharusnya merupakan detail implementasi internal, tetapi paket-paket yang banyak digunakan mengaksesnya lewatlinknameAnggota utama hall of shame mencakup
github.com/bytedance/sonicHal-hal yang saya pelajari saat merilis API
Klien akan melakukan apa pun yang diperlukan untuk menyelesaikan pekerjaan mereka, meskipun bukan dengan cara yang dimaksud penerbit. Klien tidak membaca dokumentasi. Jika cukup banyak klien bergantung pada suatu perilaku, bug pun menjadi bagian dari API. Jumlah panggilan API tidak selalu sejalan dengan tingkat kepentingannya
Karena itu, saat mengembangkan API, saya berusaha merilis API beta sedini mungkin, melihat bagaimana penggunaannya, dan mengurangi kejutan. Dalam kebanyakan kasus, saya menaikkan versi mayor sambil tetap mendukung versi sebelumnya. Untuk itu, SLA API harus didefinisikan