Golang में Hyrum's Law के लागू होने के उदाहरण
(abenezer.org)- 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 dependencycrypto/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 टिप्पणियां
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 रखनी चाहिए
हालांकि दूसरे नज़रिए से देखें तो 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 नहीं किया जाएगा
वजह यह थी कि original author ने कई async functions को call किया था, और यह मान लिया था कि पुराने धीमे routine के खत्म होने तक वे functions सभी खत्म हो चुके होंगे। असल में क्या हुआ था, यह पता लगाने में बहुत ज्यादा समय लगा
इसलिए PCs में speed कम करने वाला turbo button लगा होता था, और 8-bit computers ने ज़्यादा तेज़ CPU होने के बावजूद पूरे 10 साल तक speed नहीं बढ़ाई। आजकल लगभग सब कुछ दो या उससे अधिक CPUs पर चलता है, इसलिए “पर्याप्त तेज़ है या नहीं” को छोड़कर function execution time पर dependency बहुत कम होती है। embedded में भी single CPU के discontinued होने का अनुभव होने के बाद ऐसी dependencies से बचने की कोशिश की जाती है
यह कहानी होगी कि internal API में HTTP Status 418 को core dependency क्यों बनाया गया, और दी गई constraints के तहत वह सबसे कम खराब choice क्यों थी
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 को सच में बहुत गंभीरता से लिया जाता हैउदाहरण के लिए, कई
GenerateKeyfunctions में algorithm fixed न हो जाए, इसलिएMaybeReadBytehttps://pkg.go.dev/crypto/internal/randutil#MaybeReadByte के जरिए random stream से एक अतिरिक्त byte पढ़ा जाता है। कल ही एक report आई कि nil public key वाला private ECDSA key पहले काम करता था, लेकिन अब नहीं करता; शायद उसे फिर से काम कराना पड़ेगा https://go.dev/issue/70468Map iteration में internal implementation expose न हो, इसलिए random order इस्तेमाल किया जाता है।
rand.Randका output compatibility promise का हिस्सा माना जाता है, इसलिए उसे improve करने के लिए काफी बड़ा effort करना पड़ा https://go.dev/blog/randv2 https://go.dev/blog/chacha8randDocumentation में कौन-से promises लिखे जाएं और कौन-से behavior को “बदल सकता है” बताया जाए, इस पर हमेशा चर्चा होती रहती है। क्योंकि हमें पता है कि जो documented है उसे कभी नहीं बदला जा सकता, और जिसे “बदल सकता है” नहीं कहा गया है उसे भी शायद बदलना मुश्किल होगा https://go-review.googlesource.com/c/go/+/598336/comment/5d6...
फिर भी मुझे लगता है यह एक valuable trade-off है। मैं Go बहुत इस्तेमाल करता हूं और strong backward compatibility पसंद करता हूं, लेकिन अगर इससे Go developers को performance improve करने और features जोड़ने की ज्यादा freedom मिलती है, तो breaking changes की rate थोड़ी बढ़ना भी मैं खुशी से स्वीकार कर सकता हूं
दूसरे ecosystems के users जो झेलते हैं, जैसे Python के मामले में, उसे देखकर लगता है कि ऐसा सोचने वाला मैं अकेला नहीं हूं
MaybeReadByteकईGenerateKeyfunctions में इस्तेमाल होता है, लेकिन लगता है ed25519 में ऐसा नहीं हैed25519.NewKeyFromSeed()आने से पहले, private key से public Ed25519 key derive करने का यही इकलौता तरीका था, और मुझे लगभग पूरा यकीन है कि मैंने ऐसा code लिखा था जो इस पर निर्भर था। मुझे यह ज्यादा पसंद नहीं था, लेकिन करने को बस वही था, इसलिए याद रखना आसान हैहालांकि यह अच्छा है कि
ed25519.GenerateKeydocs साफ बताते हैं कि output deterministic है। Go crypto API में जम चुके behavior को जांचने और बनाए रखने, और नए behavior को जमने से रोकने का काम वाकई बहुत अच्छा हुआ लगता हैकुख्यात A20 line (https://en.wikipedia.org/wiki/A20_line) की तरह, यह टूटा हुआ behavior हमेशा ढोना पड़ेगा
जिस समस्या का खास तौर पर जिक्र है, उसका समाधान 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 निश्चित रूप से मौजूद है, लेकिन उसका असर कम किया जा सकता है
linked search में top cause दिख रहा Grafana को string comparison के बजाय
errors.As(&http.MaxBytesError{})इस्तेमाल करना चाहिए थाHyrum’s Law का मुख्य point यही है कि API को आप कितना भी अच्छी तरह design कर लें, फर्क नहीं पड़ता। लोग contract पर नहीं, behavior पर निर्भर हो जाते हैं
अब भी ऐसा code लिखा जा सकता है जो
err.String() == "no more tea available."check करे। मैं सहमत हूं कि ऐसा नहीं करना चाहिए, लेकिन ऐसा करने से रोकने वाली कोई चीज नहीं हैइसके अलावा
errors.IsGo में अपेक्षाकृत हाल ही में जोड़ा गया था, इसलिए जब लोग errors को इस तरह check कर रहे थे, उस समय literal string check करना ज्यादा आसान था। Go में API provider consumer को.String()return value check करने से नहीं रोक सकताखासकर standard library में तो इसका लगभग कोई बहाना नहीं है
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 रोकने के लिए है
https://www.rfc-editor.org/rfc/rfc8701.html
इस RFC का सबसे शुरुआती draft 2016 के मध्य तक जाता है, और शायद यह term सार्वजनिक रूप से पहली बार वहीं दिखाई दी होगी: https://datatracker.ietf.org/doc/html/draft-davidben-tls-gre...
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...
एक पुरानी नौकरी में मैंने error message की एक typo पकड़कर ठीक की थी, फिर पता चला कि उस typo वाले text पर dependency का जाल इतना गहरा है कि उसे व्यावहारिक रूप से ठीक नहीं किया जा सकता, और आखिरकार मुझे वही typo वाला text वापस रखना पड़ा
यह बात अब भी खटकती है
https://en.wikipedia.org/wiki/HTTP_referer
यह Hyrum के नियम की ही एक तरह की मिसाल है, लेकिन असल में यह बस Go जैसा Go है
अगर error enum type होता, तो consumer सिर्फ string substitution से बदल सकता था। इसके बजाय strings को type की तरह इस्तेमाल किया जा रहा है, इसलिए यह जानना नामुमकिन हो जाता है कि consumer किस तरह dependency बना रहा होगा। हो सकता है वह error string के बीच के सिर्फ 6 अक्षर check करता हो और बदलने पर टूट जाए
दशकों से दूसरी languages में बेहतर विकल्प इस्तेमाल हो रहे हैं, फिर भी यह एक और भयानक और outdated design decision है। शुरुआती गलती और उसे बदल न पाने की मजबूरी मिलकर आपको हमेशा के लिए बांध देती है
दिलचस्प बात यह है कि यह नियम 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 इस समस्या को अलग-अलग हद तक स्वीकार करते हैं। कुछ दिन पहले
jsonpackage में यह comment देखाisValidNumberreport करता है किsvalid JSON number literal है या नहींisValidNumberinternal implementation detail होना चाहिए, लेकिन widely used packageslinknameसे इसे 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 करना पड़ता है