2 पॉइंट द्वारा GN⁺ 2024-11-23 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • Go कोडबेस के net/http में एक टिप्पणी है कि MaxBytesError.Error() द्वारा लौटाया जाने वाला error string "http: request body too large" को Hyrum's Law की वजह से बदला नहीं जा सकता
  • Hyrum's Law वह सिद्धांत है कि जब किसी API के उपयोगकर्ता पर्याप्त संख्या में हो जाते हैं, तो कोई-न-कोई व्यक्ति उन observable behaviors पर भी निर्भर हो जाएगा जो आधिकारिक contract का हिस्सा नहीं हैं
  • error message जैसी मामूली दिखने वाली string भी, अगर बाहरी code उसी सटीक वाक्यांश के अनुसार काम करता हो, तो बदलते ही मौजूदा code को तोड़ सकती है
  • Go के अंदर crypto/rsa और internal/weak में भी ऐसी ही टिप्पणियाँ हैं, जो random stream behavior या अभी तय न हुई semantics के स्थिर हो जाने के जोखिम को दिखाती हैं
  • यह समस्या सिर्फ Go तक सीमित नहीं है, इसलिए public API या library को ऐसे डिज़ाइन करना चाहिए कि अनजाने behavior व्यवहारिक रूप से standard बनकर स्थायी न हो जाए

Go कोड में दिखा Hyrum's Law

  • net/http/request.go के MaxBytesError.Error() से यह string लौटती है
    • "http: request body too large"
    • संबंधित टिप्पणी में लिखा है: “Due to Hyrum's law, this text cannot be changed.”
  • Hyrum's Law का नाम Hyrum Wright पर रखा गया है, और hyrumslaw.com पर इसकी परिभाषा इस प्रकार दी गई है
    • अगर किसी API के उपयोगकर्ता पर्याप्त संख्या में हों, तो contract में क्या वादा किया गया है इससे अलग, सिस्टम के हर observable behavior पर कोई-न-कोई निर्भर हो जाएगा
  • MaxBytesError मामले का मुख्य बिंदु यह है कि error message की सटीक wording बाहरी code में इस्तेमाल हो सकती है
    • wording में छोटा-सा बदलाव भी मौजूदा code को तोड़ सकता है
    • http: request body too large के search result में इस string का उपयोग करने वाला Go open source code देखा जा सकता है

दूसरे Go package और बाहरी codebase के उदाहरण

  • crypto/rsa की random stream dependency

    • crypto/rsa/rsa.go के EncryptOAEP में Hyrum's Law से जुड़ी टिप्पणी है
    • यह function random stream के लिए deterministic execution का वादा नहीं करती, लेकिन MaybeReadByte लागू नहीं करती, इसलिए संभव है कि कोई वर्तमान behavior पर निर्भर हो
    • crypto/rsa/pss.go के SignPSS में भी इसी संदर्भ की टिप्पणी है
    • दोनों ही मामलों में अच्छी तरह परिभाषित संख्या के random bytes ciphertext या signature में अच्छी तरह परिभाषित तरीके से शामिल होते हैं, इसलिए इसे स्वीकार्य वादे की तरह माना जाता है
  • internal/weak में semantics के स्थिर हो जाने का जोखिम

    • internal/weak में लिखा है कि toolchain, go:linkname के जरिए इस package और reference function तक पहुँच को स्पष्ट रूप से मना करता है
    • इस package की semantics proposal process से नहीं गुज़री हैं, और अगर functionality को expose किया गया तो Hyrum's Law की वजह से मौजूदा semantics स्थायी हो सकती हैं
  • Go के बाहर भी दोहराया जाने वाला pattern

    • Hyrum's Law का ज़िक्र सिर्फ Go तक सीमित नहीं है
    • grep.app के बहुभाषी search result में कई भाषाओं के उदाहरण देखे जा सकते हैं
    • Python के urllib.parse और Pixar OpenUSD के array.h भी संबंधित codebase उदाहरण हैं
    • JavaScript का evolution भी ऐसे कई अजीब और अनचाहे behaviors पर व्यापक dependency के de facto standard बन जाने के उदाहरण से जुड़ता है

बदलाव करने से पहले क्या देखना चाहिए

  • code बदलते समय documented API के साथ-साथ उन observable behaviors पर भी विचार करना चाहिए जिन पर बाहरी code निर्भर हो सकता है
  • शुरुआत से ही ऐसे system design की ज़रूरत है जो अनचाहे behavior पर dependency बनने की संभावना को कम करे

1 टिप्पणियां

 
GN⁺ 2024-11-23
Hacker News की टिप्पणियां
  • Hyrum का नियम एक उपयोगी observation है, लेकिन उससे चिपककर गलत निष्कर्ष नहीं निकालने चाहिए
    किसी function का कुल execution time भी एक observable property है, इसलिए function को तेज़ बनाने के लिए optimize करना भी breaking change माना जा सकता है। आखिर अचानक queue बहुत जल्दी खाली हो जाए और deadlock बन जाए, ऐसा भी हो सकता है। फिर भी 99.99999999% users बिना किसी extra effort के code के तेज़ हो जाने को पसंद करेंगे
    आखिर में, क्या चीज़ breaking change है यह technical contract नहीं, बल्कि social contract ही हो सकता है। वरना सचमुच कुछ भी बदला नहीं जा सकेगा। library author को API में जो हिस्से नहीं बदलेंगे उन्हें document करना चाहिए, reasonable तरीके से व्यवहार करना चाहिए और users के प्रति empathy रखनी चाहिए; और library users को समझना चाहिए कि undocumented interface को core dependency बनाना उनकी अपनी ज़िम्मेदारी है, और authors के प्रति भी empathy रखनी चाहिए

    • open source library author के लिए मुझे लगता है कि ऊपर कही गई सारी बातें सही हैं
      हालांकि दूसरे नज़रिए से देखें तो Hyrum का नियम technical contract या social contract नहीं, बल्कि पर्याप्त रूप से ज्यादा इस्तेमाल होने वाले system में दिखाई देने वाली emergent technical property है
      उस property पर कैसे respond करना है, यह social context पर निर्भर करता है। अगर आप FOSS maintainer हैं, तो optimization से 99.99% users का code तेज़ होता है और सिर्फ 0.01% को code ठीक करना या नए API पर migrate करना पड़ता है, तो आप उसे release कर देंगे। बड़ी tech company में optimization भी करना है और company के अंदर 0% भी टूटना नहीं चाहिए, इसलिए कई teams के साथ मिलकर compromise ढूंढना पड़ता है। enterprise software company में अगर सिर्फ 0.1% टूटता है, लेकिन वह user top 5 contracts में से एक है, तो release नहीं किया जाएगा
    • पहले मैंने एक बेहद inefficient routine को लगभग 100 seconds से 0.1 seconds तक घटाया था, और उसकी वजह से reporting system टूट गया
      वजह यह थी कि original author ने कई async functions को call किया था, और यह मान लिया था कि पुराने धीमे routine के खत्म होने तक वे functions सभी खत्म हो चुके होंगे। असल में क्या हुआ था, यह पता लगाने में बहुत ज्यादा समय लगा
    • 1980 के दशक में सचमुच ऐसी समस्या थी
      इसलिए PCs में speed कम करने वाला turbo button लगा होता था, और 8-bit computers ने ज़्यादा तेज़ CPU होने के बावजूद पूरे 10 साल तक speed नहीं बढ़ाई। आजकल लगभग सब कुछ दो या उससे अधिक CPUs पर चलता है, इसलिए “पर्याप्त तेज़ है या नहीं” को छोड़कर function execution time पर dependency बहुत कम होती है। embedded में भी single CPU के discontinued होने का अनुभव होने के बाद ऐसी dependencies से बचने की कोशिश की जाती है
    • कभी load bearing teapot पर lightning talk देना चाहता हूं
      यह कहानी होगी कि internal API में HTTP Status 418 को core dependency क्यों बनाया गया, और दी गई constraints के तहत वह सबसे कम खराब choice क्यों थी
    • function के कुल execution time जैसी चीज़ function author के control में नहीं होती, इसलिए यह logic लगभग absurd लगता है
      operating environment, उस समय system load, GC execution वगैरह सब असर डाल सकते हैं
      संक्षेप में, machine से पैदा होने वाले emergent behavior को मैं intended interface या किसी तरह का contract नहीं मानता। इसलिए भले ही किसी ने unintended behavior पर dependency बनाई हो, जैसे subtle bug fix करना breaking change नहीं माना जाता, वैसे ही इसे भी breaking change नहीं मानता
      इस case में यह सबसे बढ़कर इस बात का सबूत लगता है कि Go backward compatibility के प्रति बहुत मज़बूती से committed है
  • हाहा, crypto/rsa वाली टिप्पणी मैंने लिखी थी। Go में Hyrum’s Law और backward compatibility https://go.dev/doc/go1compat को सच में बहुत गंभीरता से लिया जाता है
    उदाहरण के लिए, कई GenerateKey functions में algorithm fixed न हो जाए, इसलिए MaybeReadByte https://pkg.go.dev/crypto/internal/randutil#MaybeReadByte के जरिए random stream से एक अतिरिक्त byte पढ़ा जाता है। कल ही एक report आई कि nil public key वाला private ECDSA key पहले काम करता था, लेकिन अब नहीं करता; शायद उसे फिर से काम कराना पड़ेगा https://go.dev/issue/70468
    Map iteration में internal implementation expose न हो, इसलिए random order इस्तेमाल किया जाता है। rand.Rand का output compatibility promise का हिस्सा माना जाता है, इसलिए उसे improve करने के लिए काफी बड़ा effort करना पड़ा https://go.dev/blog/randv2 https://go.dev/blog/chacha8rand
    Documentation में कौन-से promises लिखे जाएं और कौन-से behavior को “बदल सकता है” बताया जाए, इस पर हमेशा चर्चा होती रहती है। क्योंकि हमें पता है कि जो documented है उसे कभी नहीं बदला जा सकता, और जिसे “बदल सकता है” नहीं कहा गया है उसे भी शायद बदलना मुश्किल होगा https://go-review.googlesource.com/c/go/+/598336/comment/5d6...

    • Map iteration order में बदलाव भविष्य के breaking changes को कम करने में मदद करता है, क्योंकि लोग किसी खास order पर निर्भर नहीं रह पाते; लेकिन बदलाव के समय यह उन code के लिए breaking change था जो पुराने order behavior पर निर्भर थे
      फिर भी मुझे लगता है यह एक valuable trade-off है। मैं Go बहुत इस्तेमाल करता हूं और strong backward compatibility पसंद करता हूं, लेकिन अगर इससे Go developers को performance improve करने और features जोड़ने की ज्यादा freedom मिलती है, तो breaking changes की rate थोड़ी बढ़ना भी मैं खुशी से स्वीकार कर सकता हूं
      दूसरे ecosystems के users जो झेलते हैं, जैसे Python के मामले में, उसे देखकर लगता है कि ऐसा सोचने वाला मैं अकेला नहीं हूं
    • मैंने कहा था कि MaybeReadByte कई GenerateKey functions में इस्तेमाल होता है, लेकिन लगता है ed25519 में ऐसा नहीं है
      ed25519.NewKeyFromSeed() आने से पहले, private key से public Ed25519 key derive करने का यही इकलौता तरीका था, और मुझे लगभग पूरा यकीन है कि मैंने ऐसा code लिखा था जो इस पर निर्भर था। मुझे यह ज्यादा पसंद नहीं था, लेकिन करने को बस वही था, इसलिए याद रखना आसान है
      हालांकि यह अच्छा है कि ed25519.GenerateKey docs साफ बताते हैं कि output deterministic है। Go crypto API में जम चुके behavior को जांचने और बनाए रखने, और नए behavior को जमने से रोकने का काम वाकई बहुत अच्छा हुआ लगता है
    • nil key वाला मामला सवाल उठाता है कि ऐसे cases तक support करना कितना समझदारी भरा है
      कुख्यात A20 line (https://en.wikipedia.org/wiki/A20_line) की तरह, यह टूटा हुआ behavior हमेशा ढोना पड़ेगा
    • विडंबना यह है कि मैंने पहले Go में एक load balancer लिखा था, और वह random map iteration order पर निर्भर था
    • यह Go के सबसे underrated पहलुओं में से एक है। 12 साल पहले लिखा code आज भी बस काम करता है
  • जिस समस्या का खास तौर पर जिक्र है, उसका समाधान string-based errors नहीं बल्कि sentinel errors इस्तेमाल करना है https://thomas-guettler.de/go/wrapping-and-sentinel-errors
    अधिक सामान्य रूप से, ऐसा code नहीं बनाना चाहिए जिससे API consumer को non-technical strings पर थोड़ा भी निर्भर होने का मन हो। पहले से defined error values, types, या non-technical string रखने वाले constants जैसे language के first-class constructs इस्तेमाल करने पर API consumer सीधे string hardcode करने के बजाय return value की तुलना constant से कर सकता है
    Hyrum’s Law निश्चित रूप से मौजूद है, लेकिन उसका असर कम किया जा सकता है

    • चिढ़ाने वाली बात यह है कि जिस error की बात हो रही है वह पहले से ही sentinel error है
      linked search में top cause दिख रहा Grafana को string comparison के बजाय errors.As(&http.MaxBytesError{}) इस्तेमाल करना चाहिए था
      Hyrum’s Law का मुख्य point यही है कि API को आप कितना भी अच्छी तरह design कर लें, फर्क नहीं पड़ता। लोग contract पर नहीं, behavior पर निर्भर हो जाते हैं
    • इस example में जिम्मेदारी provider की नहीं, consumer की है
      अब भी ऐसा code लिखा जा सकता है जो err.String() == "no more tea available." check करे। मैं सहमत हूं कि ऐसा नहीं करना चाहिए, लेकिन ऐसा करने से रोकने वाली कोई चीज नहीं है
      इसके अलावा errors.Is Go में अपेक्षाकृत हाल ही में जोड़ा गया था, इसलिए जब लोग errors को इस तरह check कर रहे थे, उस समय literal string check करना ज्यादा आसान था। Go में API provider consumer को .String() return value check करने से नहीं रोक सकता
    • कुछ साल पहले तक string error comparison ही यह करने का इकलौता तरीका था, और Go में backward compatibility promise है
    • raw error string check करने वाला code बस खराब code है, और उसे Go की backward compatibility guarantee से बाहर रखा जाना चाहिए
      खासकर standard library में तो इसका लगभग कोई बहाना नहीं है
    • समस्या Go के शुरुआती design में है। लंबे समय तक string-based errors ही इकलौता तरीका थे, और अगर मेरी याद सही है तो कुछ standard library packages में अभी भी वे बचे हैं; पूरे ecosystem की तो बात ही छोड़िए
      programming languages के इतिहास को जानबूझकर ignore करके “बनाते-बनाते design कर लेंगे” वाला approach अपनाने पर यही होता है
  • Hyrum के नियम से मुकाबला करने के तरीके भी एक दिलचस्प विषय हैं
    एक संभावना यह है कि जिन हिस्सों पर आप नहीं चाहते कि लोग निर्भर हों, उनमें randomness डाल दी जाए
    अगर मुझे सही याद है तो QUIC protocol ऐसा करता है। मौजूदा version में एक field है जिसका उपयोग नहीं होता, लेकिन router उस field से packet की पहचान करना शुरू न कर दें, इसलिए specification मांगती है कि उसे null byte नहीं बल्कि random value पर set किया जाए
    स्रोत शायद यहां है: https://www.rfc-editor.org/rfc/rfc9000#section-17.2.1
    “Unused field की value server किसी भी value पर set करता है। client को इस field value को अनिवार्य रूप से ignore करना चाहिए। [...] ध्यान दें कि QUIC के अन्य version शायद ऐसी ही recommendation न दें”
    मेरी समझ में इसे greasing कहा जाता है, और यह ossification रोकने के लिए है

    • GREASE RFC 8701 में बनाया गया acronym है, जिसका मतलब “Generate Random Extensions And Sustain Extensibility” है, और शुरुआत में इसे TLS context में इस्तेमाल किया गया था
      https://www.rfc-editor.org/rfc/rfc8701.html
      इस RFC का सबसे शुरुआती draft 2016 के मध्य तक जाता है, और शायद यह term सार्वजनिक रूप से पहली बार वहीं दिखाई दी होगी: https://datatracker.ietf.org/doc/html/draft-davidben-tls-gre...
    • शानदार। मैं QUIC से काफी परिचित हूं, लेकिन यह बात नहीं जानता था
      10 साल बाद जागकर यह पता चलने से बुरा कुछ नहीं कि वे bits सचमुच जरूरी हो गए हैं, लेकिन 10 brands के 20 router models ने तय कर लिया है कि वे bits किसी खास तरीके से ही होने चाहिए
      अगर दूसरी तरफ checksum या encryption हो और bit छेड़ते ही सब टूट जाए, तो bonus points। middleboxes की “smart hacking” सच में सिरदर्द है
  • यह stringly typed software का अच्छा उदाहरण है
    Go के designers exceptions नहीं चाहते थे, लेकिन panic/recover के साथ अब भी वैसी ही कोई चीज मौजूद है, और untyped errors नुकसानदेह हैं। उल्टा, pattern matching के बिना typed errors को handle कैसे किया जा सकता है? क्योंकि ज्यादातर languages का catch एक बुनियादी pattern matching ही होता है
    https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...

    • Go में typed errors हैं। बस इस case में उनका इस्तेमाल नहीं किया गया
  • एक पुरानी नौकरी में मैंने error message की एक typo पकड़कर ठीक की थी, फिर पता चला कि उस typo वाले text पर dependency का जाल इतना गहरा है कि उसे व्यावहारिक रूप से ठीक नहीं किया जा सकता, और आखिरकार मुझे वही typo वाला text वापस रखना पड़ा
    यह बात अब भी खटकती है

    • कहीं आप Phillip Hallam-Baker तो नहीं? :)
      https://en.wikipedia.org/wiki/HTTP_referer
    • कम-से-कम इस लेख के context में अभी Golang को ठीक करने का समय है
  • यह Hyrum के नियम की ही एक तरह की मिसाल है, लेकिन असल में यह बस Go जैसा Go है
    अगर error enum type होता, तो consumer सिर्फ string substitution से बदल सकता था। इसके बजाय strings को type की तरह इस्तेमाल किया जा रहा है, इसलिए यह जानना नामुमकिन हो जाता है कि consumer किस तरह dependency बना रहा होगा। हो सकता है वह error string के बीच के सिर्फ 6 अक्षर check करता हो और बदलने पर टूट जाए
    दशकों से दूसरी languages में बेहतर विकल्प इस्तेमाल हो रहे हैं, फिर भी यह एक और भयानक और outdated design decision है। शुरुआती गलती और उसे बदल न पाने की मजबूरी मिलकर आपको हमेशा के लिए बांध देती है

    • दुर्भाग्य से वह comment मूल रूप से गलत है। कई मामलों में string खुद official API थी
  • दिलचस्प बात यह है कि यह नियम robustness principle, यानी Postel's Law, के ठीक उलट है
    “भेजते समय conservative रहो, receive करते समय liberal”
    अगर input को उदारता से स्वीकार करते हैं, तो यह समझना और कम-से-कम internally document करना होगा कि आप किन तरीकों से उदार थे। Hyrum के नियम की वजह से, बड़े codebase बदलावों के बाद भी आपको उन सभी तरीकों को हमेशा support करना पड़ता है
    इसी वजह से मैं “जो receive करो उसमें liberal” API बनाना नहीं चाहता

    • मैं भी उसी तरफ झुकता हूं
      API में आने वाले data के criteria ढीले रखेंगे, तो अंत में आपको यह तय करना ही पड़ेगा कि उस data को किस canonical form में massage किया जाए। और लगता है कि वह decision लगभग हमेशा किसी-न-किसी तरह user के लिए surprising behavior में बदल जाता है
  • लगता है package authors इस समस्या को अलग-अलग हद तक स्वीकार करते हैं। कुछ दिन पहले json package में यह comment देखा
    isValidNumber report करता है कि s valid JSON number literal है या नहीं
    isValidNumber internal implementation detail होना चाहिए, लेकिन widely used packages linkname से इसे access कर रहे हैं
    hall of shame के प्रमुख members में github.com/bytedance/sonic शामिल है

  • API ship करते हुए सीखी गई बातें
    clients अपना काम पूरा करने के लिए जो भी जरूरी हो करेंगे, भले ही publisher ने वैसा intend न किया हो। clients documentation नहीं पढ़ते। अगर पर्याप्त clients किसी behavior पर depend करने लगें, तो bug भी API का हिस्सा बन जाता है। API calls की संख्या जरूरी नहीं कि importance से मेल खाए
    इसलिए API develop करते समय मैं कोशिश करता हूं कि beta API जितनी जल्दी हो सके निकालूं और देखूं कि लोग उसे कैसे इस्तेमाल करते हैं, ताकि surprises कम हों। ज्यादातर cases में पुराने version को support करते हुए major version bump करता हूं। इसके लिए API का SLA define करना पड़ता है