Go में idempotent एन्डपॉइंट्स: कुंजियाँ, डेडुप टेबल, और रीट्राई-सेफ हैंडलर्स
Go में idempotent एन्डपॉइंट्स डिज़ाइन करें — idempotency कुंजियाँ, PostgreSQL डेडुप टेबल और retry-safe हैंडलर्स ताकि पेमेंट, इम्पोर्ट और वेबहुक सुरक्षित रहें।

क्यों retries डुप्लिकेट बनाते हैं (और idempotency क्यों मायने रखता है)
Retries तब भी होते हैं जब कुछ "गलत" नहीं होता। क्लाइंट का टाइमआउट हो सकता है जबकि सर्वर अभी भी काम कर रहा हो। मोबाइल कनेक्शन गिर जाता है और ऐप फिर कोशिश करता है। एक job runner 502 पाता है और स्वतः वही अनुरोध फिर से भेज देता है। at-least-once delivery (queues और webhooks में सामान्य) के साथ, डुप्लिकेट सामान्य हैं।
इसीलिए idempotency महत्वपूर्ण है: बार-बार किए गए अनुरोधों का अंतिम परिणाम एकल अनुरोध जितना ही होना चाहिए।
कुछ शब्द आपस में मिल सकते हैं:
- Safe: इसे कॉल करने से स्टेट नहीं बदलता (जैसे पढ़ना)।
- Idempotent: इसे कई बार कॉल करने से वही प्रभाव होता है जैसे एक बार कॉल करने पर होता है।
- At-least-once: sender तब तक retry करता है जब तक यह “लग न जाए,” इसलिए receiver को डुप्लिकेट्स संभालने होंगे।
Idempotency के बिना, retries असल नुकसान कर सकते हैं। एक payment endpoint दो बार चार्ज कर सकता है अगर पहली चार्ज सफल हो गया लेकिन क्लाइंट तक प्रतिक्रिया नहीं पहुँची। एक import endpoint retry पर डुप्लिकेट रो बन सकता है। एक webhook handler एक ही इवेंट को दो बार प्रोसेस कर सकता है और दो ईमेल भेज सकता है।
मुख्य बात: idempotency एक API अनुबंध है, निजी इम्प्लीमेंटेशन डिटेल नहीं। क्लाइंट्स को यह जानना चाहिए कि वे क्या retry कर सकते हैं, कौन सी कुंजी भेजनी है, और डुप्लिकेट मिलने पर क्या प्रतिक्रिया मिलेगी। अगर आप व्यवहार चुपचाप बदलते हैं, तो आप retry लॉजिक तोड़ देते हैं और नए विफलता मोड बनाते हैं।
Idempotency मॉनिटरिंग और reconciliation को भी प्रतिस्थापित नहीं करता। डुप्लिकेट दरें ट्रैक करें, “replay” निर्णय लॉग करें, और समय-समय पर बाहरी सिस्टम्स (जैसे payment provider) की तुलना अपनी डेटाबेस से करें।
प्रत्येक एन्डपॉइंट के लिए idempotency स्कोप और नियम चुनें
टेबिल्स या मिडलवेयर जोड़ने से पहले तय करें कि “एक जैसा अनुरोध” का क्या अर्थ है और सर्वर ग्राहक के retry पर क्या वादा करता है।
अधिकांश समस्याएँ POST पर आती हैं क्योंकि यह अक्सर कुछ बनाता है या साइड-इफेक्ट ट्रिगर करता है (कार्ड चार्ज करना, संदेश भेजना, import शुरू करना)। PATCH को भी idempotency की जरूरत हो सकती है अगर वह साइड-इफेक्ट ट्रिगर करता है, न कि सिर्फ़ फील्ड अपडेट। GET को स्टेट नहीं बदलना चाहिए।
स्कोप परिभाषित करें: कहां कुंजी यूनिक है
ऐसा स्कोप चुनें जो आपके बिज़नेस नियमों से मेल खाता हो। बहुत व्यापक स्कोप वैध काम रोक देगा। बहुत संकीर्ण डुप्लिकेट्स की अनुमति देगा।
सामान्य स्कोप:
- प्रति endpoint + customer
- प्रति endpoint + external object (उदाहरण के लिए, invoice_id या order_id)
- प्रति endpoint + tenant (multi-tenant सिस्टम्स के लिए)
- प्रति endpoint + payment method + amount (केवल यदि आपके product नियम इसकी अनुमति देते हैं)
उदाहरण: “Create payment” एन्डपॉइंट के लिए, कुंजी को प्रति ग्राहक यूनिक बनाइए। “Ingest webhook event” के लिए, इसे payment provider के event ID तक स्कोप करें (provider से ग्लोबल यूनिक पहचान)।
डुप्लीकेट पर आप क्या लौटाएँगे यह तय करें
जब डुप्लिकेट आता है, तो पहली सफल कोशिश जैसा ही परिणाम लौटाएँ। व्यवहारिक रूप से इसका मतलब है वही HTTP स्टेटस कोड और वही response body रीप्ले करना (या कम से कम वही resource ID और स्टेट)।
क्लाइंट्स इस पर निर्भर करते हैं। यदि पहली बार सफल हुआ पर नेटवर्क गिर गया, तो retry को दूसरी चार्ज या दूसरी import job नहीं बनानी चाहिए।
रिटेंशन विंडो चुनें
कुंजियाँ समाप्त होनी चाहिए। उन्हें यथार्थवादी retries और देरी वाले jobs को कवर करने के लिए पर्याप्त लंबा रखें।
- Payments: 24 से 72 घंटे सामान्य है।
- Imports: यदि यूज़र बाद में retry कर सकते हैं तो एक हफ्ता ठीक हो सकता है।
- Webhooks: भेजने वाले के retry policy के साथ मेल रखें।
“एक जैसा अनुरोध” परिभाषित करें: explicit key बनाम body hash
एक explicit idempotency key (हेडर या फ़ील्ड) आमतौर पर सबसे साफ़ नियम है।
बॉडी हैश एक बैकअप के रूप में मदद कर सकता है, पर यह हल्के बदलावों (फ़ील्ड ऑर्डर, whitespace, timestamps) से टूट जाता है। अगर आप hashing उपयोग करते हैं, तो इनपुट को normalize करें और उन फ़ील्ड्स के प्रति कड़ा रहें जिन्हें आप शामिल करते हैं।
Idempotency कुंजियाँ: व्यवहार में कैसे काम करती हैं
एक idempotency key क्लाइंट और सर्वर के बीच एक सरल अनुबंध है: “अगर आप इस कुंजी को फिर से देखें, तो इसे वही अनुरोध समझो।” यह retry-safe APIs के लिए सबसे व्यावहारिक टूल्स में से एक है।
कुंजी ग्राहक या सर्वर दोनों तरफ से आ सकती है, पर अधिकांश APIs के लिए इसे क्लाइंट-जेनरेटेड होना चाहिए। क्लाइंट जानता है कि वह वही क्रिया कब retry कर रहा है, इसलिए वह वही कुंजी पुन: प्रयोजित कर सकता है। सर्वर-जनरेटेड कुंजियाँ तब मदद करती हैं जब आप पहले एक “draft” रिसोर्स बनाते हैं (जैसे import job) और फिर क्लाइंट को उसी जॉब ID के ज़रिये retry करने देते हैं, पर वे पहले अनुरोध में मदद नहीं करतीं।
यादृच्छिक, अनुमान नहीं लगाए जाने वाले स्ट्रिंग का उपयोग करें। कम-से-कम 128 बिट रैंडमनेस का लक्ष्य रखें (उदाहरण के लिए 32 hex chars या UUID)। समय-आधारित या user IDs से कुंजियाँ न बनाएं।
सर्वर पर, कुंजी को इतना संदर्भ के साथ स्टोर करें कि गलत उपयोग का पता चल सके और मूल परिणाम रीप्ले किया जा सके:
- किसने कॉल की (account या user ID)
- यह किस endpoint या ऑपरेशन पर लागू है
- महत्वपूर्ण request फ़ील्ड्स का हैश
- वर्तमान स्टेटस (
in-progress,succeeded,failed) - रीप्ले करने के लिए response (status code और body)
कुंजी को सामान्यतः एक user (या API token) + endpoint के अनुसार स्कोप करें। यदि वही कुंजी अलग payload के साथ दोबारा इस्तेमाल हुई, तो इसे स्पष्ट एरर के साथ reject करें। इससे बग्गी क्लाइंट्स द्वारा पुरानी कुंजी के साथ नया payment amount भेजने जैसे आकस्मिक टकराव रुकते हैं।
रीप्ले पर, पहली सफल कोशिश जैसा ही परिणाम लौटाएँ — वही HTTP स्टेटस कोड और वही response body, न कि कोई नया रीड जो बदल भी गया हो सकता है।
PostgreSQL में डेडुप टेबल: एक सरल, भरोसेमंद पैटर्न
एक समर्पित deduplication टेबल idempotency लागू करने का सबसे सरल तरीका है। पहला अनुरोध idempotency कुंजी के लिए एक पंक्ति बनाता है। हर retry उसी पंक्ति को पढ़ता है और स्टोर किया गया परिणाम लौटाता है।
क्या स्टोर करें
टेबल को छोटा और केंद्रित रखें। एक सामान्य संरचना:
key: idempotency key (text)owner: किसका key है (user_id, account_id, या API client ID)request_hash: महत्वपूर्ण request फ़ील्ड्स का हैशresponse: अंतिम response payload (अक्सर JSON) या स्टोर किए गए परिणाम का pointercreated_at: कब कुंजी पहली बार देखी गई
यूनिक कंस्ट्रेन्ट इस पैटर्न का मूल है। (owner, key) पर यूनिकनेस लागू करें ताकि एक ही क्लाइंट डुप्लिकेट ना बना सके और दो अलग क्लाइंट्स टकराएँ नहीं।
request_hash भी स्टोर करें ताकि कुंजी के दुरुपयोग का पता चल सके। अगर retry उसी कुंजी के साथ अलग हैश भेजता है, तो mix करने के बजाय एरर लौटाएँ।
रिटेंशन और इंडेक्सिंग
Dedup पंक्तियाँ हमेशा नहीं रहनी चाहिए। इन्हें यथार्थवादी retry विंडो के लिए रखें, फिर साफ़ करें।
लोड के तहत गति के लिए:
- तेज़ insert या lookup के लिए
(owner, key)पर यूनिक इंडेक्स - क्लीनअप सस्ता करने के लिए
created_atपर वैकल्पिक इंडेक्स
अगर response बड़ा है, तो एक pointer स्टोर करें (उदाहरण के लिए result ID) और पूरा payload कहीं और रखें। इससे टेबल का आकार कम रहेगा जबकि retry व्यवहार एक समान रहेगा।
चरण-दर-चरण: Go में एक retry-safe हैंडलर फ्लो
एक retry-safe हैंडलर को दो चीज़ों की जरूरत होती है: “वही अनुरोध फिर से” पहचानने का एक स्थिर तरीका, और पहला परिणाम टिकाऊ रूप में स्टोर करने की जगह ताकि आप उसे रीप्ले कर सकें।
भुगतान, इंपोर्ट और webhook ingestion के लिए एक व्यावहारिक फ्लो:
-
अनुरोध वैरिफाई करें, फिर तीन मान व्युत्पन्न करें: एक idempotency key (हेडर या क्लाइंट फ़ील्ड से), एक owner (tenant या user ID), और एक request hash (महत्वपूर्ण फ़ील्ड्स का हैश)।
-
एक डेटाबेस ट्रांज़ैक्शन शुरू करें और dedup रिकॉर्ड बनाकर देखें। इसे
(owner, key)पर यूनिक बनाएं।request_hash, status (started,completed) और response के प्लेसहोल्डर स्टोर करें। -
अगर insert conflict होता है, तो मौजूदा पंक्ति लोड करें। अगर वह
completedहै, तो स्टोर किया हुआ response लौटाएँ। अगर वहstartedहै, तो थोड़ी देर प्रतीक्षा करें (सरल polling) या 409/202 लौटाएँ ताकि क्लाइंट बाद में retry करे। -
केवल तभी जब आप सफलतापूर्वक dedup पंक्ति का “own” कर लें, बिज़नेस लॉजिक एक बार चलाएँ। जहाँ संभव हो, एक ही ट्रांज़ैक्शन के अंदर साइड-इफेक्ट्स लिखें। बिज़नेस रिज़ल्ट और HTTP response (status code और body) को टिकाऊ रूप से स्टोर करें।
-
commit करें, और idempotency key व owner के साथ लॉग करें ताकि सपोर्ट डुप्लिकेट्स ट्रेस कर सके।
एक न्यूनतम टेबल पैटर्न:
create table idempotency_keys (
owner_id text not null,
idem_key text not null,
request_hash text not null,
status text not null,
response_code int,
response_body jsonb,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
primary key (owner_id, idem_key)
);
उदाहरण: “Create payout” एन्डपॉइंट चार्ज करने के बाद टाइमआउट हो जाता है। क्लाइंट वही कुंजी भेजकर retry करता है। आपका हैंडलर conflict पाथ पर पहुँचता है, completed रिकॉर्ड देखता है, और मूल payout ID लौटाता है बिना दोबारा चार्ज किए।
Payments: टाइमआउट के बावजूद एक बार ही चार्ज करें
Payments वो जगह हैं जहाँ idempotency वैकल्पिक नहीं रहती। नेटवर्क फेल होते हैं, मोबाइल ऐप्स retry करते हैं, और गेटवे कभी-कभी टाइमआउट होने पर भी चार्ज बना देता है।
एक व्यावहारिक नियम: idempotency key चार्ज निर्माण की रक्षा करता है, और payment provider ID (charge/intent ID) बाद का source of truth बन जाता है। एक बार जब आप provider ID स्टोर कर लेते हैं, तो उसी अनुरोध के लिए नया चार्ज न बनाएं।
एक पैटर्न जो retries और gateway अनिश्चितता को संभालता है:
- idempotency key पढ़ें और validate करें।
- एक डेटाबेस ट्रांज़ैक्शन में,
(merchant_id, idempotency_key)से keyed एक payment row बनाइए या पाएँ। अगर उसमेंprovider_idपहले से है, तो सेव्ड रिज़ल्ट लौटाएँ। - अगर
provider_idनहीं है, तो gateway को PaymentIntent/Charge बनाने के लिए कॉल करें। - अगर gateway सफल हो, तो
provider_idपर्सिस्ट करें और payment को “succeeded” (या “requires_action”) मार्क करें। - अगर gateway टाइमआउट या अनजान रिज़ल्ट दे, तो status “pending” स्टोर करें और एक सुसंगत प्रतिक्रिया लौटाएँ जो बताती हो कि क्लाइंट के लिए retry करना सुरक्षित है।
टाइमआउट का इलाज कैसे करें — यह मुख्य बात है: असफल मानने की बजाय unknown मानें। pending के रूप में मार्क करें, फिर बाद में gateway से query करके या webhook के जरिये provider ID मिलने पर कन्फर्म करें।
Error responses predictable रखें। क्लाइंट्स आपके लौटाए गए पैटर्न के आधार पर retry लॉजिक बनाते हैं, इसलिए status codes और error shapes स्थिर रखें।
Imports और batch endpoints: बिना प्रगति खोए डुप्लिकेट रोकें
Imports वे जगह हैं जहाँ डुप्लिकेट सबसे अधिक हानि पहुँचाते हैं। यूज़र CSV अपलोड करता है, आपका सर्वर 95% पर टाइमआउट हो जाता है, और वे retry करते हैं। बिना योजना के या तो आप डुप्लिकेट रो बना देंगे या उन्हें फिर से शुरू करना पड़ेगा।
बैच वर्क के लिए, दो परतों पर सोचें: import job और उसके अंदर के आइटम्स। job-लेवल idempotency एक ही अनुरोध से कई jobs बनने से रोकता है। आइटम-लेवल idempotency एक ही रो को दो बार लागू होने से रोकता है।
एक job-लेवल पैटर्न यह है कि import अनुरोध के लिए idempotency key की जरूरत करें (या इसे user ID के साथ एक स्थिर request hash से व्युत्पन्न करें)। इसे import_job रिकॉर्ड के साथ स्टोर करें और retries पर वही job ID लौटाएँ। हैंडलर को कहना चाहिए, “मैंने यह जॉब देखा है, यह इसका वर्तमान स्टेट है,” बजाय “फिर से शुरू करो।”
आइटम-लेवल डेडुप के लिए, डेटा में पहले से मौजूद प्राकृतिक कुंजी पर भरोसा करें। उदाहरण के लिए, प्रत्येक पंक्ति में स्रोत सिस्टम से एक external_id हो सकता है, या (account_id, email) जैसा स्थिर संयोजन। PostgreSQL में इसे unique constraint से लागू करें और upsert व्यवहार का उपयोग करें ताकि retries डुप्लिकेट्स न बनाएँ।
शिप करने से पहले तय करें कि जब कोई पंक्ति पहले से मौजूद हो तो replay क्या करेगा। स्पष्ट रहें: skip करें, कुछ फील्ड अपडेट करें, या fail करें। बिना स्पष्ट नियमों के merge से बचें।
partial success सामान्य है। एक बड़े “ok” या “failed” के बजाय, पंक्ति-स्तर के परिणाम जॉब से जुड़ा कर स्टोर करें: row number, natural key, status (created, updated, skipped, error), और error message। retry पर आप सुरक्षित रूप से फिर से चला सकते हैं जबकि पहले समाप्त हुए rows के लिए वही परिणाम रखेंगे।
इम्पोर्ट्स को restartable बनाने के लिए checkpoints जोड़ें। पेजेज़ में प्रोसेस करें (उदाहरण के लिए 500 rows प्रति पेज), हर पेज commit के बाद last processed cursor (row index या source cursor) स्टोर करें। अगर प्रोसेस क्रैश हो जाए, अगला प्रयास आखिरी checkpoint से resumed होगा।
Webhook ingestion: dedup, validate, फिर सुरक्षित रूप से process करें
Webhook भेजने वाले retry करते हैं। वे events को आउट-ऑफ़-ऑर्डर भी भेजते हैं। अगर आपका handler हर डिलीवरी पर स्टेट अपडेट करे तो अंततः आप रिकॉर्ड दो बार बनाएँगे, दो बार ईमेल भेजेंगे, या दो बार चार्ज करेंगे।
सबसे पहले उपयुक्त dedup कुंजी चुनें। अगर provider आपको एक यूनिक event ID देता है, तो उसे यूज़ करें। इसे webhook endpoint के लिए idempotency key मानें। जब कोई event ID न हो, तभी payload के हैश पर fallback करें।
सुरक्षा पहले: signature verify करें उससे पहले कि आप कुछ भी स्वीकार करें। अगर signature fail हो, तो रिक्वेस्ट reject करें और dedup रिकॉर्ड न लिखें। वरना attacker किसी event ID को “reserve” कर सकता है और असली events को बाद में ब्लॉक कर देगा।
Retries के दौरान एक सुरक्षित फ्लो:
- signature और बेसिक shape (जरूरी headers, event ID) verify करें।
- unique constraint के साथ dedup टेबल में event ID insert करें।
- अगर insert duplicate की वजह से fail हो, तो तुरंत 200 लौटाएँ।
- audit और debugging के लिए raw payload (और headers) स्टोर करें जब यह उपयोगी हो।
- processing enqueue करें और जल्दी 200 लौटाएँ।
जल्दी acknowledge करना मायने रखता है क्योंकि कई providers के short timeouts होते हैं। request में सबसे छोटा भरोसेमंद काम करें: verify, dedup, persist। फिर asynchronously process करें (worker, queue, background job)। अगर आप async नहीं कर सकते, तो processing idempotent रखें और internal side effects को उसी event ID पर की-आधारित रखें।
Out-of-order delivery सामान्य है। यह मत मानें कि “created” पहले आएगा और फिर “updated।” external object ID पर upserts पसंद करें और last processed event timestamp या version ट्रैक करें।
raw payloads स्टोर करने से मदद मिलती है जब ग्राहक कहे “हमें अपडेट नहीं मिला।” आप स्टोर किए हुए बॉडी से processing फिर चला सकते हैं जब आपने बग फिक्स कर लिया हो, बिना provider से पुनः भेजने के लिए कहना पड़े।
Concurrency: समानांतर अनुरोधों के बीच सही बने रहें
जब एक ही idempotency key के साथ दो अनुरोध एक ही समय में आते हैं तो retries जटिल हो जाते हैं। अगर दोनों हैंडलर “do work” चरण को चलाएँ और किसी ने भी परिणाम न बचाया हो, तो आप फिर भी double charge, double import, या double enqueue कर सकते हैं।
सरल समन्वय बिंदु डेटाबेस ट्रांज़ैक्शन है। पहली स्टेप को “key क्लेम करना” बनाइए और डेटाबेस तय करे कौन जीतता है। सामान्य विकल्प:
- dedup टेबल में unique insert (डेटाबेस एक विजेता लागू करता है)
- dedup पंक्ति बनाने (या खोजने) के बाद
SELECT ... FOR UPDATE - idempotency key के हैश पर transaction-level advisory locks
- बिज़नेस रिकॉर्ड पर यूनिक कंस्ट्रेन्ट को अंतिम बैकस्टॉप के रूप में रखना
लंबे चलने वाले काम के लिए, बाहरी सिस्टम कॉल करने या मिनटों के इम्पोर्ट के दौरान row lock रखने से बचें। इसके बजाय dedup पंक्ति में एक छोटा state machine स्टोर करें ताकि अन्य अनुरोध तेजी से बाहर निकल सकें।
एक व्यावहारिक सेट ऑफ स्टेट्स:
in_progressसाथ मेंstarted_atcompletedसाथ में cached responsefailedसाथ में error code (वैकल्पिक, आपकी retry नीति के अनुसार)expires_at(सफाई के लिए)
उदाहरण: दो ऐप इंस्टेंसेस को एक ही payment अनुरोध मिलता है। इंस्टेंस A कुंजी insert करता है और in_progress मार्क करता है, फिर provider को कॉल करता है। इंस्टेंस B conflict पाथ पर आता है, dedup पंक्ति पढ़ता है, in_progress देखता है, और एक तेज़ “अब भी प्रोसेस हो रहा है” प्रतिक्रिया देता है (या थोड़ी देर इंतज़ार कर फिर चेक करता है)। जब A खत्म करता है, वह पंक्ति को completed अपडेट करता है और response body स्टोर करता है ताकि बाद के retries वही आउटपुट पाएं।
सामान्य गलतियाँ जो idempotency तोड़ती हैं
ज्यादातर idempotency बग्ज़ fancy locking के बारे में नहीं होते। वे “लगभग सही” फैसलों के कारण होते हैं जो retries, timeouts, या दो यूज़र्स के समान क्रियाओं के दौरान फेल होते हैं।
एक आम जाल idempotency कुंजी को ग्लोबली यूनिक मानना है। अगर आप इसे scope नहीं करते (user, account, या endpoint से), तो दो अलग क्लाइंट्स टकरा सकते हैं और एक दूसरे का परिणाम पा सकते हैं।
एक और समस्या यही है कि उसी कुंजी को अलग request body के साथ स्वीकार कर लेना। अगर पहली कॉल $10 के लिए थी और रीप्ले $100 के लिए है, तो आप चुपचाप पहले का परिणाम लौटाना नहीं चाहिए। request_hash (या प्रमुख फील्ड्स का हैश) स्टोर करें, रीप्ले पर तुलना करें, और स्पष्ट conflict एरर लौटाएँ।
क्लाइंट्स तब भ्रमित होते हैं जब रीप्ले अलग response shape या status code लौटाता है। अगर पहली कॉल ने 201 और JSON बॉडी लौटाया, तो रीप्ले को वही बॉडी और स्थिर स्टेटस कोड लौटाना चाहिए। रीप्ले व्यवहार बदलने से क्लाइंट्स को अंदाज़ा लगाना पड़ता है।
गलतियाँ जो अक्सर डुप्लिकेट बनाती हैं:
- केवल इन-मेमोरी मैप या cache पर निर्भर रहना, और restart पर dedup state खो देना।
- बिना स्कोप के कुंजी का उपयोग (cross-user या cross-endpoint टकराव)।
- उसी कुंजी पर payload mismatches को validate न करना।
- पहले साइड-इफेक्ट करना (charge, insert, publish) और बाद में dedup रिकॉर्ड लिखना।
- हर retry पर नया generated ID लौटाना बजाय मूल परिणाम को रीप्ले करने के।
एक cache पढ़ने को तेज़ कर सकता है, पर सत्य का स्रोत टिकाऊ होना चाहिए (आम तौर पर PostgreSQL)। वरना deploy के बाद retries डुप्लिकेट बना सकते हैं।
साफ़-सुथरी सफाई की योजना भी बनाएं। अगर आप हर कुंजी को हमेशा के लिए स्टोर करते हैं तो टेबल बढ़ेंगी और इंडेक्स धीमे होंगे। रिटेंशन विंडो निर्धारित करें, पुरानी पंक्तियाँ हटाएँ, और यूनिक इंडेक्स छोटा रखें।
त्वरित चेकलिस्ट और अगले कदम
Idempotency को अपने API अनुबंध का हिस्सा मानें। हर एन्डपॉइंट जो क्लाइंट, queue, या gateway द्वारा retry हो सकता है, उसके लिए यह स्पष्ट नियम होना चाहिए कि “एक जैसा अनुरोध” क्या है और “एक जैसा परिणाम” कैसा दिखता है।
शिप करने से पहले चेकलिस्ट:
- हर retryable endpoint के लिए, क्या idempotency स्कोप परिभाषित और लिखित है (per user, per account, per order, per external event)?
- क्या dedup डेटाबेस द्वारा लागू है (idempotency key और स्कोप पर यूनिक कंस्ट्रेन्ट), सिर्फ़ कोड-स्तरीय चेक नहीं?
- रीप्ले पर क्या आप वही status code और response body लौटाते हैं (या एक डॉक्यूमेंटेड स्थिर सबसेट), न कि नया ऑब्जेक्ट या नया timestamp?
- Payments के लिए, क्या आप unknown outcomes (submit के बाद timeout, gateway का “processing”) को सुरक्षित तरीके से हैंडल करते हैं बिना दो बार चार्ज किए?
- क्या लॉग्स और मेट्रिक्स यह स्पष्ट करते हैं कि कोई अनुरोध पहली बार कब देखा गया था बनाम कब replayed था?
अगर कोई आइटम “शायद” है, तो इसे अभी ठीक करें। अधिकतर विफलताएँ तनाव के दौरान दिखती हैं: पैरेलल retries, धीमे नेटवर्क, और आंशिक आउटेज।
यदि आप AppMaster (appmaster.io) पर आंतरिक टूल्स या ग्राहक-सामना करने वाले ऐप्स बना रहे हैं, तो idempotency कुंजियाँ और PostgreSQL dedup टेबल को जल्दी डिजाइन करना मददगार है। इस तरह, जैसे-जैसे प्लेटफ़ॉर्म आवश्यकताओं के बदलने पर Go बैकएंड को regenerate करता है, आपका retry व्यवहार सुसंगत रहता है।
सामान्य प्रश्न
Retries सामान्य हैं क्योंकि नेटवर्क और क्लाइंट सामान्य परिस्थितियों में फेल होते हैं। एक अनुरोध सर्वर पर सफल हो सकता है पर क्लाइंट तक उत्तर न पहुंचे, इसलिए क्लाइंट पुनःप्रयास करता है और जब तक सर्वर मूल परिणाम को पहचानकर उसी तरह वापस नहीं देता, तब तक वही काम दोबार हो सकता है।
उसी क्रिया के हर पुनःप्रयास पर एक ही कुंजी भेजें। यह कुंजी क्लाइंट द्वारा जनरेट की जानी चाहिए — यादृच्छिक और अनुमान नहीं लगाई जा सकने वाली स्ट्रिंग (उदाहरण के लिए UUID)। अलग क्रिया के लिए एक ही कुंजी फिर से उपयोग न करें।
इसे अपने बिज़नेस नियम के अनुसार स्कोप करें — आमतौर पर प्रति एन्डपॉइंट और कॉलर पहचान (user, account, tenant या API token)। इससे दो अलग ग्राहक एक-दूसरे की कुंजी पर टकराएँगे और एक दूसरे के परिणाम नहीं पाएँगे।
पहली सफल कोशिश जैसा ही आउटकम वापस करें। व्यवहार में इसका मतलब है कि वही HTTP स्टेटस कोड और वही response body (या कम से कम वही resource ID और स्टेट) रीप्ले करें, ताकि क्लाइंट सुरक्षित रूप से retry कर सके बिना दूसरी साइड-इफेक्ट पैदा किए।
स्पष्ट conflict-स्टाइल एरर के साथ अस्वीकार करें। महत्वपूर्ण request फील्ड्स का एक हैश रखें और रीप्ले पर तुलना करें — यदि कुंजी समान है पर पेलोड अलग है, तो तुरंत असफल करें ताकि एक ही कुंजी के तहत दो अलग ऑपरेशन्स मिल न जाएँ।
कुंजियों को यथार्थवादी पुनःप्रयास विंडोज़ को कवर करने के लिए रखें, फिर उन्हें हटाएँ। भुगतान के लिए सामान्य डिफ़ॉल्ट 24–72 घंटे है, इंपोर्ट्स के लिए एक हफ्ता अर्थपूर्ण हो सकता है, और वेबहुक के लिए भेजने वाले के retry policy से मेल खाएं।
एक समर्पित dedup टेबल अच्छा काम करती है क्योंकि डेटाबेस एक यूनिक कंस्ट्रेन्ट लागू कर सकता है और रिस्टार्ट्स सहन करता है। owner scope, key, request hash, status, और replay के लिए response स्टोर करें, और (owner, key) को यूनिक बनाएं ताकि सिर्फ़ एक अनुरोध “जीते”।
पहले डेटाबेस ट्रांज़ैक्शन में कुंजी का क्लेम करें, फिर साइड-इफेक्ट तभी करें जब आपने सफलतापूर्वक इसे क्लेम किया हो। अगर पैरलल अनुरोध आते हैं, तो वे यूनिक कंस्ट्रेन्ट पर टकराएँगे, in_progress या completed देखेंगे, और जल्दी से wait/replay प्रतिक्रिया दें ताकि लॉजिक दो बार न चले।
टाइमआउट को “अज्ञात” मानें, “असफल” नहीं। एक pending state रिकॉर्ड करें और अगर आपके पास provider ID है तो उसे source of truth बनाएँ ताकि retries वही भुगतान परिणाम दें बजाय नए चार्ज बनाने के।
दो स्तरों पर dedupe करें: job-level और item-level। retries को वही import job ID लौटाएँ, और पंक्तियों के लिए एक प्राकृतिक कुंजी (external ID या (account_id, email)) पर unique constraint या upsert लागू करें ताकि reprocessing डुप्लिकेट न बनाए।


