रूप 1: स्टेट-रिपेयर। रूप 2: वेस्टफुल रिपोर्ट।
एक मीटर्ड हार्ट घड़ी पर धड़कता है। ज़रूरत पर नहीं। बदलाव पर नहीं। टाइमर पर।
दो रूप, एक मूल कारण: सही डिज़ाइन के स्थान पर शेड्यूल्ड जॉब का इस्तेमाल।
रूप 1: स्टेट-रिपेयर
एक स्टेट ट्रांजिशन एटॉमिक रूप से पूरा होने में विफल रहता है। ट्रांजिशन को ठीक करने के बजाय, एक बैकग्राउंड जॉब देरी से चलता है और रिकंसाइल करता है। रिकंसिलिएशन विंडो के दौरान उपयोगकर्ता टूटी हुई स्टेट देखते हैं।
GitHub उदाहरण (2026-04-08): एक पुल रिक्वेस्ट का अपस्ट्रीम रिपॉजिटरी प्राइवेट हो गया। GitHub ने स्टेट ट्रांजिशन की कोशिश की: PR को बंद करना, ब्रांच स्टेटस अपडेट करना, मर्ज स्टेटस क्लियर करना। ट्रांजिशन एटॉमिक रूप से पूरा नहीं हुआ। PR स्टेटस में एक साथ 'branch-forced-closed' और 'Merge status cannot be loaded' दिखाई दिया। कुछ मिनट बाद एक Sidekiq बैकग्राउंड जॉब चली और रिकंसीलिएशन पूरा किया। ऑब्जर्वर्स को इस विंडो के दौरान ब्रोकन स्टेट दिखाई दिया।
The Metered Heart: Sidekiq जॉब एक शेड्यूल पर चली। यह इसलिए नहीं चली कि GitHub ने ब्रोकन स्टेट डिटेक्ट किया; यह इसलिए चली क्योंकि टाइमर फायर हुआ। रियल टाइम में PR देख रहे यूजर को अगली जॉब एक्जीक्यूशन तक एक ऐसा PR दिखाई दिया जो खुद से विरोधाभास कर रहा था।
Form 2: Wasteful Report
एक रिपोर्ट या एग्रीगेशन फिक्स्ड इंटरवल पर स्क्रैच से रीकंप्यूट होती है। कोई कैश चेक नहीं। कोई idempotency गार्ड नहीं। कोई इंक्रीमेंटल अपडेट नहीं। हर एक्जीक्यूशन: एक फुल स्कैन।
उदाहरण: एक नाइटली क्रॉन जॉब जो हर यूजर का टोटल परचेज अमाउंट शुरुआत से सभी ऑर्डर्स स्कैन करके रीकंप्यूट करती है। एक डेली एनालिटिक्स जॉब जो रॉ इवेंट लॉग्स से डैशबोर्ड रीजनरेट करती है। एक वीकली समरी ईमेल जो एक्टिविटी टेबल की हर रो क्वेरी करती है।
हर जॉब फायर होती है चाहे पिछली एक्जीक्यूशन के बाद डेटा बदला हो या नहीं। हर जॉब फुल हिस्ट्री स्कैन करती है भले ही सिर्फ पिछले 24 घंटों में नया डेटा हो। हर जॉब इंक्रीमेंटल डिज़ाइन की जगह शेड्यूल्ड रिपीटिशन का इस्तेमाल करती है।
The Shared Root
A Metered Heart अपनी स्टेट के बारे में सच्चाई नहीं बता सकता। उसे सिर्फ घड़ी का पता होता है। Form 1: स्टेट रिपेयर जॉब T+5 मिनट पर चलती है, भले ही T+0 पर स्टेट ब्रोकन हो या नहीं। Form 2: रिपोर्ट जॉब 2 AM पर चलती है, भले ही कल के बाद कोई डेटा बदला हो या नहीं।
घड़ी इस बात की जानकारी नहीं रखती कि क्या करने की आवश्यकता है। एक इवेंट वह जानकारी रखता है: 'एक स्टेट ट्रांजिशन अभी फेल हुआ,' 'नए ऑर्डर अभी आए हैं।' एक Metered Heart उस जानकारी को फेंक देता है और उसे एक शेड्यूल से बदल देता है।
Capital Drain
एक Metered Heart जीवित पूंजी को नष्ट करता है: इंजीनियर जो टूटे स्टेट इंसिडेंट्स के लिए ऑन-कॉल रहते हैं। सामाजिक विश्वास को कमजोर करता है: यूजर्स असंगत डेटा देखते हैं और उन डिफेक्ट्स की रिपोर्ट करते हैं जो खुद-ब-खुद ठीक हो जाते हैं। अन्य MOADs को बढ़ाता है: एक स्टेट-रिपेयर जॉब जो टूटे स्टेट को खोजने के लिए सभी रिकॉर्ड्स को स्कैन करती है, अक्सर MOAD-0001 (O(N²) स्कैन) को शामिल करती है। एक रिपोर्ट जॉब जो कोल्ड डेटा को रीकंप्यूट करती है, MOAD-0005 (कैश स्टैम्पीड) को ट्रिगर कर सकती है। MOAD-0009 अन्य डिफेक्ट्स को बढ़ाता है।
The Shared Root
Form 1 और Form 2 सतह पर अलग दिखते हैं: एक स्टेट को रिपेयर करता है, दूसरा डेटा को रीकंप्यूट करता है। रूट कॉज उन्हें जोड़ता है।
Fire on Change, Not on Clock
Event-driven डिज़ाइन तब सक्रिय होता है जब कोई बदलाव होता है। स्टेट चेंज ही इवेंट है। इवेंट ही ट्रिगर है।
Form 1: the atomic transition replaces the repair job.
यदि कोई स्टेट ट्रांजिशन सिस्टम को एक टूटी हुई इंटरमीडिएट स्टेट में छोड़ सकता है, तो दोष ट्रांजिशन में है, न कि रिपेयर जॉब की अनुपस्थिति में। ट्रांजिशन को एटॉमिक (या ट्रांजेक्शनल) रूप से पूरा करने के लिए ठीक करें। जब ट्रांजिशन एटॉमिक रूप से पूरा होता है, तो टूटी हुई स्टेट कभी अस्तित्व में नहीं आती। रिपेयर जॉब के पास ठीक करने के लिए कुछ भी नहीं बचता।
# DEFECT: non-atomic transition leaves broken state
def close_pr_on_repo_private(pr_id):
pr = PR.get(pr_id)
pr.status = 'branch-forced-closed' # step 1: partial state
pr.save() # visible to users NOW
# ... other steps may fail ...
pr.merge_status = 'not_applicable'
pr.save() # step 2: now consistent
# Sidekiq job reconciles if step 2 fails
# FIX: atomic transition; no intermediate state visible
def close_pr_on_repo_private(pr_id):
with db.transaction():
pr = PR.get(pr_id)
pr.status = 'branch-forced-closed'
pr.merge_status = 'not_applicable'
pr.save() # दोनों फ़ील्ड्स एटॉमिकली कमिट होती हैं; कभी भी आधा लिखा नहीं जाता
Form 2: incremental update पूरे recompute को बदल देता है।
एक रिपोर्ट जो स्क्रैच से recompute करती है, इसलिए फायर होती है क्योंकि पुराना डेटा + नया डेटा = नया रिजल्ट। लेकिन पुराना रिजल्ट + डेल्टा = वही नया रिजल्ट, जो incrementally कैलकुलेट किया गया। इवेंट: नया डेटा आया। ट्रिगर: सिर्फ नए डेटा के लिए aggregate अपडेट करें।
# DEFECT: schedule पर full recompute
def nightly_totals_job():
for user in all_users():
total = sum(o.amount for o in user.orders) # सभी समय का स्कैन
user.total_purchases = total
user.save()
# FIX: इवेंट-ड्रिवन इंक्रीमेंटल अपडेट
def on_order_placed(order):
order.user.total_purchases += order.amount # केवल डेल्टा
order.user.save()
इंक्रीमेंटल अपडेट तब फायर होता है जब कोई ऑर्डर आता है, न कि सुबह 2 बजे। यह केवल प्रभावित यूज़र को अपडेट करता है। यह केवल नए ऑर्डर को पढ़ता है, न कि सभी समय के सभी ऑर्डर को। नाइटली जॉब गायब हो जाती है।
क्यों Form 1 एक टूटी हुई ट्रांज़िशन को दिखाता है
एक Form 1 Metered Heart दिखाता है कि एक स्टेट ट्रांज़िशन अधूरी छोड़ दी गई थी। रिपेयर जॉब इसलिए मौजूद है क्योंकि एक इंजीनियर ने टूटी हुई स्टेट को नोटिस किया और ट्रांज़िशन को ठीक करने के बजाय एक रिकंसिलिएशन मैकेनिज़्म जोड़ दिया। रिपेयर जॉब: एक टूटी हुई आर्किटेक्चरल डिसीजन पर पैच।
MOAD-0009 एक एम्प्लिफायर के रूप में
MOAD-0009 अन्य MOADs को बढ़ाता है। एक स्टेट-रिपेयर जॉब जो टूटी हुई स्टेट ढूंढने के लिए सभी रिकॉर्ड्स को स्कैन करता है: MOAD-0001 (प्रति जॉब रन O(N) या O(N²) स्कैन)। एक रिपोर्ट जॉब जो सब कुछ कोल्ड से रीकंप्यूट करती है: MOAD-0005 (जब जॉब शुरू होती है और एक वार्म अपस्ट्रीम को हिट करती है तो कैश स्टैम्पीड)। MOAD-0009 केवल खुद से नुकसान नहीं पहुँचाता; यह अन्य MOADs को एक शेड्यूल पर डिलीवर करता है।
निदान करें और पुनः डिज़ाइन करें
एक टीम रात 2 बजे एक नाइटली क्रॉन जॉब चलाती है। जॉब सभी यूज़र्स के सभी ऑर्डर्स को स्कैन करती है और प्रत्येक यूज़र की कुल खरीद राशि को शुरू से रीकंप्यूट करती है। जॉब को 4 घंटे लगते हैं। सुबह 6 बजे तक, डैशबोर्ड ताज़ा टोटल दिखाता है। सुबह 2 बजे से 6 बजे के बीच, डैशबोर्ड कल के टोटल दिखाता है।