Why NextWhy Next
सूची पर लौटें
💻 डेवलपमेंट12 मिनट में पढ़ें

staging में बस बंद कर दो? - शोर भरे alert के पीछे छिपी असली गड़बड़

staging में रोज़ आने वाला Google Play 404 alert। source बंद कर दो तो शांति हो जाती है, लेकिन वह और failure mode को ठीक करना, दो अलग चीज़ें निकलीं। असल में 404 एक permanent error है जिसे रीट्राई करने से कोई फ़ायदा नहीं, फिर भी वह 5xx के साथ मिलाकर रीट्राई हो रहा था - यही कहानी है।

मुख्य सारांश

queue पर रीट्राई होने वाले job में जब external API 404 फेंकता है, तो अक्सर वह 404 एक permanent condition होती है जिसे रीट्राई करने से कोई फ़र्क़ नहीं पड़ता। लेकिन client अगर 404 को 5xx·429 के साथ एक ही exception में मिला दे, तो वह रीट्राई बजट जलाता है और alert भर देता है। समाधान यह है कि response code सबसे पहले देखने वाली client boundary पर ही permanent error (4xx) और temporary error (5xx·429·timeout) को अलग कर दिया जाए। और शोर भरे alert के सामने 'source बंद करना' और 'failure mode ठीक करना' दो अलग चीज़ें हैं। पहला सिर्फ़ इस एक alert को मिटाता है, दूसरा उस failure के लौटकर आने के हर मौक़े को मिटा देता है।

इस पेज पर

रोज़ एक ही समय पर एक ही alert आता था। staging में subscription reconcile job fail होने का ERROR था। सुबह 3:17 पर cron चलता, और 3 मिनट बाद alert आ जाता। stack trace हमेशा एक ही लाइन से शुरू होता था: Google Play Voided Purchases API returned 404. तीन बार retry होता, तीनों बार वही 404 आकर मर जाता, job DLQ में गिर जाता, और हमारे Slack में एक और लाल alert जमा हो जाता।

यह कोई incident नहीं था। कुछ भी टूटा नहीं था। बस शोर था। और यह शोर भरा alert कैसे संभालना है, यह तय करते-करते मुझे फिर याद आया कि "बंद करना" और "ठीक करना" दो अलग-अलग सवाल हैं।

404 क्यों आ रहा था

Voided Purchases API एक Google Play endpoint है जो refund या cancel हुई ख़रीद की लिस्ट लौटाता है। हम इसी से रोज़ यह पकड़ते हैं कि किसने चुपके से refund ले लिया और app फिर भी इस्तेमाल कर रहा है। लेकिन यह endpoint 404 फेंके तो इसका मतलब "data नहीं है" नहीं होता। voidedpurchases.list में 404 का मतलब है कि या तो वह application (package) मिल ही नहीं रहा, या service account को उस package तक पहुंचने की permission ही नहीं है।

staging के service account को production package के लिए Play Console access मिला ही नहीं है। मिलना चाहिए भी नहीं। staging वह environment नहीं है जहां असली store refund traffic बहता है। इसलिए staging से यह API बुलाओ तो 404 आएगा ही - आज भी, कल भी, जब तक configuration न बदले तब तक हमेशा। यह कोई थोड़ी देर के लिए टूटकर जुड़ने वाला temporary incident नहीं, बल्कि environment का बनाया permanent condition है।

यहीं पर असली फ़र्क़ आता है। temporary incident हो तो retry सही जवाब है। network थोड़ी देर टिमटिमाए या दूसरी तरफ़ का server थोड़ी देर के लिए 5xx दे दे, तो कुछ सेकंड बाद फिर बुला लो, काम बन जाएगा। लेकिन permanent condition retry से नहीं सुधरती। staging के service account में जो permission है ही नहीं, वह 3 सेकंड बाद अचानक आ नहीं जाएगी। इसलिए इस 404 को तीन बार retry करना शुद्ध बर्बादी थी, और उसके बाद आने वाला ERROR alert शुद्ध noise था।

code retry क्यों कर रहा था

वजह client के response classify करने के तरीक़े में थी। listVoidedPurchases 200 के अलावा हर response को एक साथ मिलाकर throw कर रहा था।

if (!res.ok) {
  throw AppHttpException.serviceUnavailable(
    `Google Play Voided Purchases API returned ${res.status}`,
  );
}

serviceUnavailable वह error है जिसे हमारी queue "retry के लायक़" मानकर पढ़ती है। इसलिए 404 हो, 500 हो या 429, यहां से निकलने वाला हर error तीन बार retry होकर हर बार alert पर ख़त्म होता है। दिक़्क़त यह है कि इन तीनों status का मतलब बिल्कुल अलग है। 500 और 429 कहते हैं "अभी नहीं हो रहा, पर थोड़ी देर में हो सकता है", जबकि 404 कहता है "इस condition में आगे भी नहीं होगा"। जिस पल यह सब एक ही exception में मिल जाता है, retry करने पर भी कोई फ़ायदा न देने वाला error retry pipeline पर चढ़ जाता है।

मज़ेदार बात यह है कि सही जवाब पहले से उसी फ़ाइल में मौजूद था। उसी client के getSubscriptionPurchase में 404 को पहले से ही अलग handle किया जा रहा था।

if (res.status === 404) {
  throw new GooglePlayApiError(404, "purchase not found");
}

एक method 404 के मतलब की इज़्ज़त कर रहा था, दूसरा उसे मिटा रहा था। example कोड में ही मौजूद था, बस नया method उसे follow नहीं कर रहा था। ऐसी असमानता आमतौर पर तब बनती है जब हर method अलग दिन, अलग concern के साथ लिखा जाता है।

ठीक करना: boundary पर अलग करना

fix दो layer में हुआ, और दोनों ने पहले से मौजूद pattern को ही follow किया।

सबसे पहले client boundary पर 404 को अलग किया। 404 को "नहीं मिला" मतलब वाले GooglePlayApiError(404) से throw किया, और बाक़ी 5xx·429·timeout को वैसे ही serviceUnavailable में रहने दिया। यानी सिर्फ़ वही रखा retry की list में, जिसे सच में retry चाहिए।

if (res.status === 404) {
  throw new GooglePlayApiError(404, "application not found");
}
if (!res.ok) {
  throw AppHttpException.serviceUnavailable(/* 5xx·429·timeout = असली retry के लायक़ */);
}

उसके बाद तय किया कि reconcile service इस 404 को कैसे स्वीकार करे। service में पहले से ही "service account key न हो तो चुपचाप skip कर दो" वाला flow (isConfigured === false) मौजूद था। 404 भी मूल रूप से वही स्थिति है - इस environment में यह API बुलाई ही नहीं जा सकती। इसलिए इसे भी उसी जगह बिठा दिया।

try {
  const voided = await client.listVoidedPurchases();
  // ... reconcile जारी
} catch (e) {
  if (e instanceof GooglePlayApiError && e.status === 404) {
    this.logger.warn("voided purchases 404 - इस environment में access नहीं, skip किया");
    return { skippedAppNotFound: true };
  }
  throw e; // 404 न हो तो वैसे ही फिर throw करके सामान्य retry
}

catch में सिर्फ़ 404 को निगला जाता है, warn log छोड़कर skip का statistic लौटा दिया जाता है। दोबारा throw न होने से न retry होता है, न DLQ, न ERROR alert। इसके उलट, 404 के अलावा कोई भी error उसी तरह फिर throw होता है, जिससे पुराना retry logic सामान्य रूप से काम करता रहता है। सिर्फ़ शोर वाला हिस्सा शांत होता है, असली दिक़्क़त अब भी उतनी ही ज़ोर से शोर मचाती रहती है।

इस पर test भी जोड़े। क्या client 404 पर GooglePlayApiError throw करता है, क्या reconcile 404 को skip करता है, और क्या 404 के अलावा error अब भी फिर throw होता है। आख़िरी case सबसे ज़रूरी है। 404 को चुप कराने की कोशिश में अगर ग़लती से 500 भी निगल लिया जाए, तो असली incident दिख ही नहीं पाएगा।

"बस staging में बंद कर दो न?"

PR भेजने के बाद एक अच्छा सवाल आया। staging में यह job शुरू से चलाई ही न जाए तो? यह intuition सही है। staging में असली refund traffic है ही नहीं, तो API बुलाने की वजह ही नहीं बनती। तो जाकर देखा।

बंद करना सोचे जितना आसान नहीं निकला। staging में इस feature को बंद करने का सीधा switch service account key की मौजूदगी (isConfigured) है, लेकिन वह key सिर्फ़ इसी job के लिए नहीं थी। app payment receipt verification, Google real-time notification (RTDN) webhook processing, subscription reconcile processor - पूरी subscription verification उसी key पर टिकी थी। staging में payment और subscription test करने के लिए वह key चाहिए ही होगी। key हटाकर job बंद करो तो जो असल में test करना है, वह भी साथ में बंद हो जाता है।

तो फिर सिर्फ़ इसी job को बंद करने वाला अलग toggle बना दें? ऐसा कोई toggle था ही नहीं। मतलब GOOGLE_VOIDED_RECONCILE_ENABLED जैसा नया switch डालना पड़ेगा। यानी "सिर्फ़ staging बंद करना" भी आख़िर में code और environment variable छूने वाला काम ही है। मुफ़्त में बंद करने का कोई रास्ता शुरू से था ही नहीं।

बंद करना और ठीक करना दो अलग सवाल हैं

लेकिन असली मुद्दा मुश्किल होना नहीं था। toggle बनाना आसान भी होता, तो भी वह 404 वाले fix की जगह नहीं ले सकता था। क्योंकि दोनों approach अलग-अलग सवाल हल करते हैं।

  • 404 को retry की list से हटाना एक जड़ से किया गया defense है। कोई भी environment हो - staging हो, configuration बिगड़ा हुआ production हो, या भविष्य में कोई toggle भूल जाए - retry करने पर बेकार साबित होने वाला 404 alert की बाढ़ में तब्दील नहीं होगा।
  • staging में job बंद करना एक operational optimization है। "इस environment में बुलाने की वजह ही नहीं, तो call ही skip कर दो" - यह ऊपर से जोड़ने लायक़ अच्छा फ़ैसला है।

क्रम मायने रखता है। 404 का defense रखते हुए toggle जोड़ना ठीक है। लेकिन सिर्फ़ toggle डालकर 404 का defense हटा दो, तो किसी दिन production में configuration बिगड़ने पर 404 आते ही alert फिर फट पड़ेगा। और उस वक़्त "staging है इसलिए ignore करो" कहकर टाला भी नहीं जा सकेगा। source बंद करना अभी दिख रहे एक alert को मिटाता है, failure mode ठीक करना उस alert के दोबारा आने के हर मौक़े को मिटा देता है।

शोर भरा alert दिखते ही हाथ पहले source की तरफ़ जाता है। यह job बंद कर दें, यह alert rule हटा दें, इस environment से निकाल दें। यह एक स्वाभाविक impulse है, और कभी-कभी वही सही भी होता है। लेकिन बंद करने से पहले एक बार पूछना ज़रूरी है - मैं अभी alert बंद कर रहा हूं, या alert ने जो failure mode बताया था उसे ठीक कर रहा हूं। पहला सिर्फ़ इस alert को चुप करा देता है, दूसरा इस failure के किसी और रूप में लौटने को रोक देता है।

boundary पर जो सवाल पूछना ज़रूरी है

यह कहानी सिर्फ़ Google Play API की नहीं है। बाहर call करके उसकी failure को queue पर retry कराने वाला हर code इसी दोराहे पर खड़ा होता है। retry policy को queue configuration में बदलने से पहले, client boundary पर ही यह सवाल पूछना चाहिए।

क्या यह error retry से सुधरता है? 5xx·429·timeout में ज़्यादातर हां - अभी नहीं हुआ तो थोड़ी देर में हो सकता है। 4xx में ज़्यादातर नहीं। 404, 403, 400 तब तक वही जवाब देते रहेंगे जब तक condition ख़ुद न बदले। इन दोनों को एक ही exception में मिला दो, तो retry करने पर बेकार साबित होने वाली failure retry budget जलाकर alert भर देती है। अलग करने की जगह queue नहीं, response code सबसे पहले देखने वाली client boundary है।

और ठीक करने से पहले एक बार codebase पर नज़र दौड़ाना मत भूलना। हमारे मामले में तो उसी फ़ाइल के पड़ोसी method में 404 पहले से ही सही तरीक़े से handle हो रहा था। नया कुछ ईजाद करने की ज़रूरत ही नहीं थी। जवाब पहले से code में मौजूद हो, बस नया code उसे follow न कर रहा हो - ऐसा मामला सोचे से कहीं ज़्यादा बार होता है।

अक्सर पूछे जाने वाले सवाल

background job 404 पर बार-बार रीट्राई कर रहा है और failure alert दोहराया जा रहा है। क्या करें?

सबसे पहले यह देखना होगा कि वह 404 temporary है या permanent। अगर resource उस environment में शुरू से ही मौजूद नहीं है, या access permission ही नहीं है, तो रीट्राई करने पर भी वही result मिलेगा। ऐसे में सही तरीक़ा यही है कि client जहां पहली बार response code देखता है, वहीं 404 को अलग error type में भेज दिया जाए, ताकि job उसे दोबारा throw न करे और warn log के साथ graceful skip हो जाए। तभी retry, DLQ और ERROR alert रुकेंगे।

temporary error और permanent error में फ़र्क़ कैसे करें?

आमतौर पर 5xx·429·timeout temporary होते हैं इसलिए retry के लायक़ होते हैं - अभी नहीं हुआ तो थोड़ी देर बाद हो सकता है। दूसरी तरफ़ 404·403·400 जैसे 4xx तब तक वही जवाब देते रहेंगे जब तक request या condition ख़ुद न बदले, इसलिए इन्हें retry करना बेकार है। यह फ़र्क़ queue की retry policy में नहीं, बल्कि response code सबसे पहले देखने वाली client boundary पर करना बेहतर है।

संबंधित लेख