Publish दो बार दबाने पर अब एक ही बार पब्लिश होता है
एक ही ऐप के लिए तीन publish requests 84 मिलीसेकंड के अंतर से हमारे API तक पहुँचीं, और हर एक ने उस ऐप का पूरा deploy शुरू किया। वे एक ही hosting पर एक-दूसरे से दौड़ पड़ीं, और नतीजे में कई releases विफल हुए क्योंकि कोई दूसरा पहले पहुँच चुका था। दिखने वाला एकमात्र संकेत "release failed" ईमेल था; ऐप खुद ठीक चलता रहा। अब जब एक ही ऐप के लिए कोई publish चल रहा हो और दूसरा आए, तो वह दूसरा build शुरू नहीं करता। डेटाबेस तय करता है कि कौन-सा request जीतेगा, और बाकियों को सच बताया जाता है: एक publish पहले से चल रहा है।
क्या हुआ
Mythex पर ऐप पब्लिश करने पर API उस ऐप के लिए एक deployment रिकॉर्ड करता है, और हमारे orchestrator से, जो builds चलाने वाली सेवा है, उसे build और release करने को कहता है। Publish Publish बटन से, किसी दूसरे browser tab से, या तब शुरू हो सकता है जब आप एजेंट से publish करने को कहें। डबल-क्लिक, दो tabs, या दो बार आया एजेंट request, इन सबसे एक ही ऐप के लिए लगभग एक ही समय पर एक से ज़्यादा publish हो सकते हैं।
इस मामले में तीन 84 ms के भीतर आए। हर एक ने पूरा deploy चलाया। Hosting platform पर उसी ऐप के releases एक ही सेकंड में बने, और उनमें से तीन इसलिए विफल हुए क्योंकि दूसरे पहले से चल रहे थे। एक बाद का release अटक गया, और बाद में ऐप काम करने लगा।
यही बात इसे आसानी से छूटने लायक बनाती थी। उपयोगकर्ता को एक चलता हुआ ऐप दिखा। विफलताएँ सिर्फ़ release-failure ईमेल में दिखीं।
कुछ ने रोका क्यों नहीं
Publish कोड में प्रोजेक्ट के चल रहे deployments को देखने वाला एक कदम था। पर वह हर चल रहे deployment को शुरू होने से पहले failed चिह्नित कर देता था, "नए publish द्वारा superseded"। उसका काम क्रैश हुई प्रोसेस के पीछे छूटे builds को साफ़ करना था।
व्यवहार में, बीच build में आया दूसरा publish पहले वाले को मरा घोषित कर देता था, और अपना build शुरू कर देता था। दोनों एक ही ऐप पर पूरे होने तक चलते थे। कोई lock नहीं था; बल्कि एक ऐसा कदम था जो सक्रिय रूप से दौड़ के लिए रास्ता साफ़ करता था।
ऐप के पहले publish के लिए एक और, अजीब लक्षण था। जब तक कोई deployment रिकॉर्ड न हो जिसे दोबारा इस्तेमाल किया जा सके, तो दो समवर्ती पहले publish दोनों एक ही रिकॉर्ड डालने की कोशिश करते थे। डेटाबेस में ऐप के link name (उसका slug, जैसे your-app in your-app.mythex.ai) पर एक unique index है, इसलिए दूसरा insert विफल होता था। API इसे "वह link name पहले से लिया है" में बदल देता था, और उपयोगकर्ता को उस ऐप का नाम बदलने की सलाह देता था, जो उसी क्षण उसी नाम से सफलतापूर्वक पब्लिश हो रहा था।
समाधान: दावा ही lock है
Deployment रिकॉर्ड का दावा अब एक ही शर्तीय डेटाबेस update है: यह पंक्ति लो और उसे building चिह्नित करो केवल तब जब कोई उसे पब्लिश न कर रहा हो। क्योंकि यह एक ही statement है, डेटाबेस उन requests को निपटाता है जो एक ही मिलीसेकंड में आती हैं। ठीक एक request पंक्ति बदलती है। बाकी देखते हैं कि zero rows बदले, रुक जाते हैं, और कभी orchestrator तक नहीं पहुँचते।
Lock को उसके धारक के मरने पर बाहर निकलने का रास्ता भी चाहिए। अगर publish के बीच API restart हो जाए, तो पंक्ति हमेशा "building" रहती और ऐप को दोबारा publish नहीं किया जा सकता था। इसलिए जिस पंक्ति का मालिक heartbeat भेजना बंद कर दे, उसे फिर से claim किया जा सकता है। चलता publish हर 30 सेकंड में पंक्ति को अपडेट करता है, और पाँच मिनट की चुप्पी का मतलब है कि वह मर चुका। यह सीमा heartbeat से आती है, किसी अनुमान से नहीं कि build में कितना समय लगता है। उपयोगकर्ता का अपना Dockerfile बनाना सचमुच लंबा चल सकता है, और धीमा build जो अब भी heartbeat भेज रहा है, अपना claim रखता है।
जो कदम हर चल रहे deployment को शुरू होने से पहले विफल कर देता था, वह अब publish से पहले नहीं चलता। केवल वे पंक्तियाँ साफ़ होती हैं जो कोई मरी हुई प्रोसेस पीछे छोड़ गई।
हारने वाले को सच बताना
जो request हारता है उसे अब एक साफ़ जवाब मिलता है: publish_in_progress, और चल रहे deployment का ID। Publish शुरू कर सकने वाली हर जगह इसे संभालती है:
- Workspace चल रहे build का पीछा करता है, उसके ऊपर त्रुटि नहीं दिखाता।
- एजेंट को बताया जाता है कि इस ऐप का publish पहले से चल रहा है और उसे इंतज़ार करना चाहिए, न कि यह कि कुछ विफल हुआ। पहले यह उसे किसी Dockerfile को "ठीक करने" और दोबारा कोशिश करने की ओर भेज सकता था।
- पहले publish के दौरान link name की टक्कर की जाँच होती है: अगर उस नाम को रखने वाली पंक्ति इसी प्रोजेक्ट का चल रहा publish है, तो जवाब "पहले से publish हो रहा है" होता है, "वह नाम लिया है" नहीं। किसी दूसरे प्रोजेक्ट से असली टक्कर पर अब भी नाम बदलने वाला संदेश आता है।
हमने इसे कैसे जाँचा
हमने एक reproduction script लिखी जो एक असली डेटाबेस और एक नकली orchestrator के खिलाफ़ एक ही ऐप के तीन publish एक साथ चलाती है, और गिनती है कि orchestrator से कितने deploys माँगे गए। इसमें पहला publish और एक live ऐप का दोबारा publish, दोनों शामिल हैं। फ़िक्स से पहले यह दो deploys गिनती थी। बाद में एक।
Script बचाव-रास्ते को भी कवर करती है: वह ऐप की पंक्ति को दस मिनट तक बिना heartbeat के building चिह्नित करती है, फिर दोबारा publish करती है, और जाँचती है कि नया publish पंक्ति को claim करके चलता है।
Unit tests lock से दो बातें माँगते हैं: एक साथ तीन दोबारा publish में ठीक एक आगे बढ़ता है और बाकी दो को बताया जाता है कि publish चल रहा है; और जिस build का मालिक मर गया, उसे claim किया जा सकता है। एक और test जाँचता है कि नया प्रयास पिछले प्रयास का error संदेश साथ नहीं लेता।
हमने क्या सीखा
- Cleanup code दौड़ का रूप ले सकता है। "जो भी चल रहा हो उसे विफल करो, फिर शुरू करो" सफ़ाई जैसा लगता है। दो callers के साथ, यह एक-दूसरे को रद्द करना है।
- डेटाबेस को फ़ैसला करने दें। पहले पढ़ना, फिर लिखना, दूसरे request के लिए गैप छोड़ता है। एक शर्तीय update नहीं छोड़ता।
- हर lock को liveness से जुड़ी समाप्ति चाहिए। उसके बिना, एक क्रैश एक अटके हुए ऐप में बदल जाता है।
- Error संदेश निर्देश होते हैं। "वह नाम लिया है" ने लोगों को गलत काम करने को कहा। सही संदेश सरल था और सच भी।
Publishing के बारे में और docs में: how to publish। और एक संबंधित फ़िक्स: आपका published ऐप रीपब्लिश के दौरान भी चालू रहता है।