2 poin oleh GN⁺ 2024-11-23 | 1 komentar | Bagikan ke WhatsApp
  • Dalam basis kode Go, net/http memiliki komentar bahwa string error "http: request body too large" yang dikembalikan oleh MaxBytesError.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/rsa dan internal/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() di net/http/request.go mengembalikan 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 MaxBytesError adalah 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/rsa

    • EncryptOAEP di crypto/rsa/rsa.go memiliki komentar terkait Hyrum's Law
    • Fungsi ini tidak menjanjikan eksekusi deterministik terhadap stream acak, tetapi karena MaybeReadByte tidak diterapkan, ada kemungkinan seseorang bergantung pada perilaku saat ini
    • SignPSS di crypto/rsa/pss.go juga 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/weak menjadi tetap

    • internal/weak menyatakan bahwa toolchain secara eksplisit melarang akses ke paket tersebut dan fungsi referensinya melalui go: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.parse milik Python dan array.h milik 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

 
GN⁺ 2024-11-23
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

    • Menurut saya, semua hal di atas benar untuk penulis library open source
      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
    • Dulu saya pernah memangkas sebuah rutin yang sangat tidak efisien dari sekitar 100 detik menjadi 0,1 detik, dan akibatnya sistem pelaporan rusak
      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
    • Pada 1980-an, masalah seperti itu benar-benar ada
      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
    • Suatu hari saya ingin memberikan lightning talk tentang load bearing teapot
      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
    • Hal seperti total waktu eksekusi fungsi tidak berada di bawah kendali penulis fungsi, jadi logika ini terasa hampir absurd
      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/rsa itu saya yang menulisnya. Di Go, Hukum Hyrum dan kompatibilitas mundur https://go.dev/doc/go1compat benar-benar ditanggapi dengan serius
    Misalnya, pada beberapa fungsi GenerateKey, mereka membaca satu byte tambahan dari stream acak dengan MaybeReadByte https://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/70468
    Iterasi map menggunakan urutan acak agar implementasi internal tidak terekspos. Output rand.Rand dianggap sebagai bagian dari janji kompatibilitas, sehingga diperlukan upaya yang cukup besar untuk memperbaikinya https://go.dev/blog/randv2 https://go.dev/blog/chacha8rand
    Selalu 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...

    • Perubahan urutan iterasi map membantu mengurangi perubahan yang merusak di masa depan dengan mencegah ketergantungan pada urutan tertentu, tetapi pada saat perubahan itu dilakukan, ia adalah breaking change bagi kode yang bergantung pada perilaku urutan sebelumnya
      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
    • Saya mengatakan MaybeReadByte dipakai di beberapa fungsi GenerateKey, tetapi sepertinya itu tidak dilakukan di ed25519
      Sebelum 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 diingat
      Namun bagus bahwa dokumentasi ed25519.GenerateKey menyatakan 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 baru
    • Kasus nil key membuat saya bertanya-tanya seberapa masuk akalnya mendukung kasus seperti ini
      Seperti A20 line yang terkenal buruk (https://en.wikipedia.org/wiki/A20_line), perilaku rusak ini jadi harus dibawa selamanya
    • Ironisnya, dulu saya pernah menulis load balancer dengan Go, dan bergantung pada urutan iterasi map yang acak
    • Ini salah satu bagian Go yang paling diremehkan. Kode yang ditulis 12 tahun lalu masih berjalan begitu saja
  • 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

    • Hal yang menjengkelkan adalah error yang dimaksud sebenarnya sudah berupa sentinel error
      Grafana, yang tampaknya menjadi penyebab teratas dalam pencarian terkait, seharusnya memakai errors.As(&http.MaxBytesError{}) alih-alih perbandingan string
      Inti dari Hukum Hyrum adalah, sebagus apa pun API dirancang, itu tidak terlalu berpengaruh. Orang akan bergantung pada perilaku, bukan pada kontrak
    • Dalam contoh ini, tanggung jawab ada pada konsumen, bukan penyedia
      Anda tetap bisa menulis kode yang memeriksa err.String() == "no more tea available.". Saya setuju bahwa itu tidak seharusnya dilakukan, tetapi tidak ada yang mencegahnya
      Selain itu, errors.Is ditambahkan 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()
    • Beberapa tahun lalu, membandingkan string error adalah satu-satunya cara untuk melakukan ini, dan Go memiliki janji kompatibilitas mundur
    • Kode yang memeriksa string error mentah hanyalah kode buruk, dan seharusnya dikecualikan dari jaminan kompatibilitas mundur Go
      Terutama di standard library, hampir tidak ada alasan yang bisa dibenarkan
    • Desain awal Go-lah yang bermasalah. Untuk waktu yang lama, error berbasis string adalah satu-satunya cara, dan kalau ingatan saya benar, sebagian paket standard library masih memilikinya, belum lagi seluruh ekosistemnya
      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

    • GREASE adalah akronim yang dibuat di RFC 8701, berarti “Generate Random Extensions And Sustain Extensibility”, dan awalnya digunakan dalam konteks TLS
      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...
    • Bagus sekali. Saya cukup familier dengan QUIC, tapi tidak tahu soal ini
      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/recover masih ada sesuatu yang mirip, dan error tanpa tipe itu berbahaya. Sebaliknya, bagaimana menangani error bertipe tanpa pattern matching? Karena catch di kebanyakan bahasa pada dasarnya adalah pattern matching yang primitif
    https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...

    • Go punya error bertipe. Hanya saja tidak digunakan dalam kasus ini
  • 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

  • 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

    • Sayangnya komentar itu pada dasarnya keliru. Dalam banyak kasus, string itu sendiri adalah API resmi
  • 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”

    • Saya juga lebih suka pendekatan itu
      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 json
    isValidNumber melaporkan apakah s merupakan literal angka JSON yang valid
    isValidNumber seharusnya merupakan detail implementasi internal, tetapi paket-paket yang banyak digunakan mengaksesnya lewat linkname
    Anggota utama hall of shame mencakup github.com/bytedance/sonic

  • Hal-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