നിങ്ങൾ Node.js ഉപയോഗിച്ച് നിർമ്മിക്കുമ്പോൾ, തുടക്കത്തിൽ എറർ ഹാൻഡ്‌ലിംഗ് (error handling) വളരെ എളുപ്പമായി തോന്നും. ഒരു റൂട്ടിൽ try-catch ഉപയോഗിച്ച് ഒരു 500 സ്റ്റാറ്റസ് കോഡ് അയച്ചാൽ മതി, അടുത്തതായി എന്തുചെയ്യണമെന്ന് ക്ലയന്റ് തീരുമാനിക്കും. ഈ രീതി HTTP-ക്ക് അനുയോജ്യമാണ്, എന്നാൽ ബാക്ക്ഗ്രൗണ്ട് ജോബുകളിലേക്ക് (background jobs) മാറുമ്പോൾ ഇത് പരാജയപ്പെടുന്നു. ഒരു ക്യൂ സിസ്റ്റത്തിൽ (queue system), കാത്തുനിൽക്കുന്ന ഒരു ക്ലയന്റില്ല. അവിടെ ഒരു വർക്കറും (worker), ഒരു പേലോഡും (payload), Redis, RabbitMQ അല്ലെങ്കിൽ SQS എന്നിവയിൽ എവിടെയോ വർദ്ധിച്ചുകൊണ്ടിരിക്കുന്ന ഒരു റീട്രൈ കൗണ്ടറും (retry counter) മാത്രമേയുള്ളൂ. വെബ് റിക്വസ്റ്റുകൾ പരാജയപ്പെടുമ്പോൾ നിങ്ങൾ ചെയ്യുന്ന അതേ രീതിയിൽ തന്നെ പരാജയങ്ങളെ കൈകാര്യം ചെയ്താൽ, നിങ്ങൾ ഒരു ട്രാൻസാക്ഷൻ മാത്രം നഷ്ടപ്പെടുത്തുകയല്ല ചെയ്യുന്നത്. പകരം നിങ്ങളുടെ മുഴുവൻ പൈപ്പ്‌ലൈനും (pipeline) തടസ്സപ്പെടുകയോ, കമ്പ്യൂട്ട് റിസോഴ്സുകൾ പാഴാവുകയോ, അല്ലെങ്കിൽ ഒരേ 'poisoned message' കാരണം നിങ്ങളുടെ വർക്കറുകൾ വീണ്ടും വീണ്ടും ക്രാഷ് ആകുകയോ ചെയ്തേക്കാം.

ബാക്ക്ഗ്രൗണ്ട് ജോബുകളിൽ HTTP രീതി പരാജയപ്പെടുന്നു

ഒരു റിക്വസ്റ്റ്-റെസ്പോൺസ് സൈക്കിളിൽ (request-response cycle), ഫീഡ്‌ബാക്ക് ലൂപ്പ് (feedback loop) ഉടനടിയാണ്. ഒരു ഉപയോക്താവ് ഒരു ബട്ടൺ ക്ലിക്ക് ചെയ്യുന്നു, സെർവർ ഒരു എറർ നൽകുന്നു, ഉപയോക്താവിന് ഒരു ഫെയിലർ സ്ക്രീൻ കാണാം. ക്ലീനപ്പ് (Cleanup) ലളിതമാണ്. എന്നാൽ ഒരു ക്യൂ വർക്കർ (queue worker) ഒറ്റപ്പെട്ട നിലയിലാണ് പ്രവർത്തിക്കുന്നത്. അത് ഒരു ജോലി എടുക്കുന്നു, നിമിഷങ്ങളോ മിനിറ്റുകളോ അതിനായി ചിലവഴിക്കുന്നു, തുടർന്ന് വിജയം അറിയിക്കുന്നു (acknowledgment). ഇടയിൽ എന്തെങ്കിലും തെറ്റുപറ്റിയാൽ, എന്തുകൊണ്ടാണ് അത് സംഭവിച്ചതെന്ന് ക്യൂവിന് അറിയില്ല. അക്നോളജ്‌മെന്റ് ലഭിച്ചില്ല എന്ന് മാത്രമേ അതിന് അറിയാവൂ. നിങ്ങളുടെ കോൺഫിഗറേഷൻ അനുസരിച്ച്, അത് വീണ്ടും വീണ്ടും ശ്രമിച്ചുകൊണ്ടിരിക്കും (retry). തെറ്റായ ഒരു പേലോഡ് (malformed payload) നൂറുകണക്കിന് തവണ വർക്കറുകൾക്കിടയിൽ ചുറ്റിക്കറങ്ങുകയും, ശരിക്കും പ്രോസസ്സ് ചെയ്യേണ്ട ജോലികൾക്കിടയിൽ സിപിയു (CPU) പാഴാക്കുകയും ചെയ്തേക്കാം.

രണ്ട് തരം പരാജയങ്ങൾ

സ്ഥിരതയുള്ള ക്യൂകൾ (resilient queues) ഉണ്ടാക്കുന്നതിനുള്ള ആദ്യ നിയമം, എല്ലാ എററുകളെയും ഒരേപോലെ കാണുന്നത് നിർത്തുക എന്നതാണ്. പരാജയങ്ങൾ സംഭവിക്കുന്ന നിമിഷം തന്നെ അവയെ രണ്ട് ഗ്രൂപ്പുകളായി തിരിക്കേണ്ടതുണ്ട്.

റീട്രൈ ചെയ്യാവുന്ന പരാജയങ്ങൾ (Retryable failures) താൽക്കാലികമാണ് (transient). നെറ്റ്‌വർക്ക് ടൈമൗട്ടുകൾ (network timeouts), ഒരു തേർഡ് പാർട്ടി API-ൽ നിന്നുള്ള റേറ്റ് ലിമിറ്റുകൾ (rate limits), അല്ലെങ്കിൽ ഡാറ്റാബേസ് കണക്ഷൻ താൽക്കാലികമായി വിച്ഛേദിക്കപ്പെടുന്നത് എന്നിവ ഇതിന് ഉദാഹരണമാണ്. സമ്മർദ്ദത്തിലായ ഒരു സിസ്റ്റത്തിന്റെ ലക്ഷണങ്ങളാണിവ. രണ്ട് മിനിറ്റ് കഴിഞ്ഞ് അടുത്ത ശ്രമത്തിൽ ഇവ വിജയിച്ചേക്കാം.

സ്ഥിരമായ പരാജയങ്ങൾ (Permanent failures) 'poison pills' ആണ്. തെറ്റായ പേലോഡുകൾ, സ്കീമ ValidationError (schema validation errors), അല്ലെങ്കിൽ ഒരു അപ്‌സ്ട്രീം സർവീസ് (upstream service) മാറ്റം വരുത്തിയത് കാരണം ആവശ്യമായ ഒരു ഫീൽഡ് ഇല്ലാതിരിക്കുക എന്നിവ ഇതിൽ ഉൾപ്പെടുന്നു. ഇവ വീണ്ടും ശ്രമിക്കുന്നത് വെറുതെ സമയം കളയലാണ്. നൂറാം തവണയും ഇവ ഒരേപോലെ പരാജയപ്പെടും.

നിങ്ങളുടെ catch ബ്ലോക്കിന് ഈ രണ്ട് വ്യത്യാസങ്ങളും തിരിച്ചറിയാൻ കഴിയുന്നില്ലെങ്കിൽ, നിങ്ങളുടെ ക്യൂ നിയന്ത്രണമില്ലാതെയാണ് പ്രവർത്തിക്കുന്നത്.

പാറ്റേൺ 1: Catch ബ്ലോക്കിൽ എററുകളെ തരംതിരിക്കുക

നിങ്ങളുടെ വർക്കറിലെ catch ബ്ലോക്ക് ആണ് ഫയലിലെ ഏറ്റവും ശ്രദ്ധാപൂർവ്വം എഴുതേണ്ട കോഡ്. ഒരു എറർ ഉണ്ടാകുമ്പോൾ ഉടൻ തന്നെ അത് പരിശോധിക്കുക. എറർ കോഡ് ECONNRESET ആണോ അതോ ടൈമൗട്ട് ആണോ? എങ്കിൽ അത് റീട്രൈ ചെയ്യാൻ ക്യൂ ചെയ്യുക. അതോ അത് ഒരു SyntaxError, ഒരു Joi validation റിജക്ഷൻ, അല്ലെങ്കിൽ ഒരു മിസ്സിംഗ് ഫോറിൻ കീ കൺസ്ട്രയിന്റ് (missing foreign key constraint) ആണോ? എങ്കിൽ അതിനെ നേരിട്ട് ഒരു ഡെഡ്-ലെറ്റർ ക്യൂവിലേക്ക് (dead-letter queue, DLQ) മാറ്റുക, അത് നിങ്ങളുടെ റീട്രൈ ലിമിറ്റിൽ ഉൾപ്പെടുത്തരുത്.

BullMQ, Bee Queue എന്നിവയുൾപ്പെടെയുള്ള മിക്ക Node.js ക്യൂ ലൈബ്രറികളും നിങ്ങൾക്ക് കസ്റ്റം ബാക്കോഫ് സ്ട്രാറ്റജികളും (custom backoff strategies) എറർ ഹുക്കുകളും (error hooks) നിർവചിക്കാൻ അനുവദിക്കുന്നു. അവ ഉപയോഗിക്കുക. ഒരു സ്ഥിരമായ എറർ ഡിഫോൾട്ട് ആയി മൂന്ന് തവണ റീട്രൈ ചെയ്യാൻ പാടില്ല. ബാക്കിയുള്ള ജോലികൾ സുഗമമായി നടക്കാൻ വേണ്ടി അത് പ്രധാന ക്യൂവിൽ നിന്ന് നീക്കം ചെയ്യണം. DLQ കൃത്യമായ പേലോഡും എറർ കോൺടെക്സ്റ്റും (error context) സംരക്ഷിക്കുന്നു, ഇത് ബഗ്ഗുകൾ പരിഹരിച്ചതിന് ശേഷം പിന്നീട് ആ ജോലി വീണ്ടും ചെയ്യാൻ നിങ്ങളെ സഹായിക്കുന്നു.

പാറ്റേൺ 2: ജിറ്ററിനൊപ്പമുള്ള എക്സ്പോണൻഷ്യൽ ബാക്കോഫ് (Exponential Backoff with Jitter)

ഉടനടി റീട്രൈ ചെയ്യുന്നത് അമിതമാണ്. ഒരു ഡൗൺസ്ട്രീം ഡാറ്റാബേസ് നിലവിൽ വലിയ ലോഡ altındaയാണെങ്കിൽ, അമ്പത് വർക്കറുകളിൽ നിന്ന് ഓരോ രണ്ട് സെക്കൻഡിലും അതിലേക്ക് റിക്വസ്റ്റ് അയക്കുന്നത് അതിനെ പൂർണ്ണമായും തകർത്തേക്കാം. സിസ്റ്റത്തിന് വീണ്ടെടുക്കാൻ സമയം നൽകേണ്ടതുണ്ട്.

എക്സ്പോണൻഷ്യൽ ബാക്കോഫ് (exponential backoff) ഉപയോഗിക്കുക. ആദ്യ പരാജയത്തിൽ ഒരു സെക്കൻഡ് കാത്തിരിക്കുക. രണ്ടാമത്തേതിൽ രണ്ട് സെക്കൻഡ്. പിന്നീട് നാല്, എട്ട് എന്നിങ്ങനെ അഞ്ച് മിനിറ്റ് വരെയുള്ള ഒരു പരിധി വരെ കാത്തിരിക്കുക. എന്നാൽ സമയം മാത്രം പോരാ. പരാജയപ്പെട്ട എല്ലാ ജോലികളും ഒരേ ഇടവേളയാണ് ഉപയോഗിക്കുന്നതെങ്കിൽ, ബാക്കോഫ് സമയം കഴിയുമ്പോൾ അവയെല്ലാം ഒരേസമയം टकराക്കും. 'thundering herd' എന്ന് വിളിക്കപ്പെടുന്ന ഈ ഒരേസമയം വരുന്ന തിരക്ക്, വീണ്ടെടുക്ക ശ്രമിക്കുന്ന ഒരു സർവീസിനെ തകർക്കാൻ സാധ്യതയുണ്ട്.

ജിറ്റർ (jitter) ചേർക്കുക. നിങ്ങൾ കണക്കാക്കിയ ഡിലേയിൽ (delay) പത്ത് മുതൽ ഇരുപത് ശതമാനം വരെ ഒരു റാൻഡം (random) മാറ്റം വരുത്തുക. നാല് സെക്കൻഡ് എന്നത് 4.2 അല്ലെങ്കിൽ 4.7 സെക്കൻഡ് ആകാം. ഈ ലളിതമായ ക്രമക്കേട് റീട്രൈ സ്പൈക്കുകളെ (retry spikes) കുറയ്ക്കുകയും നിങ്ങളുടെ ഇൻഫ്രാസ്ട്രക്ചറിനെ തകരാതിരിക്കാൻ സഹായിക്കുകയും ചെയ്യുന്നു.

പാറ്റേൺ 3: ഐഡെംപൊട്ടൻസിക്ക് (Idempotency) വേണ്ടി രൂപകൽപ്പന ചെയ്യുക

ഇവിടെയാണ് ക്യൂ ഇൻഫ്രാസ്ട്രക്ചർ ബിസിനസ് ലോജിക്കുമായി (business logic) സംഗമിക്കുന്നത്. ഒരു പേയ്‌മെന്റ് പ്രൊവൈഡർ വഴി ഉപഭോക്താവിനെ ചാർജ് ചെയ്യുന്ന ഒരു ജോലിയെക്കുറിച്ച് സങ്കൽപ്പിക്കുക. വർക്കർ വിജയകരമായി ചാർജ് ചെയ്യുന്നു, എന്നാൽ ആ വിജയം നിങ്ങളുടെ ഡാറ്റാബേസിൽ രേഖപ്പെടുത്തുന്നതിനോ ജോലിയെ അക്നോളജ് ചെയ്യുന്നതിനോ മുമ്പ് കണക്ഷൻ വിച്ഛേദിക്കപ്പെടുന്നു. ക്യൂ ഒരു പരാജയം കാണുന്നു. അത് വീണ്ടും ശ്രമിക്കുന്നു. ഉപഭോക്താവ് രണ്ട് തവണ ചാർജ് ചെയ്യപ്പെടുന്നു.

Node.js-ൽ, ഓരോ side effect-ഉം idempotent ആക്കുന്നതിലൂടെ ഇത് തടയാം. Job ID അല്ലെങ്കിൽ ബിസിനസ്സ് ആവശ്യങ്ങൾക്കനുസരിച്ചുള്ള ഒരു identifier ഉപയോഗിച്ച് ഒരു idempotency key നിർമ്മിക്കുക. ഒരു charge ചെയ്യുന്നതിനോ, email അയക്കുന്നതിനോ, അല്ലെങ്കിൽ inventory ക്രമീകരിക്കുന്നതിനോ മുമ്പ്, ആ ജോലി നേരത്തെ ചെയ്തോ എന്ന് പരിശോധിക്കുക. ആ key നിങ്ങളുടെ database-ലേക്കും അത് സ്വീകരിക്കുന്ന ഏതെങ്കിലും third-party API-കളിലേക്കും കൈമാറുക. ഒരേ payload പത്ത് തവണ പ്രവർത്തിപ്പിച്ചാലും ഒരു തവണ പ്രവർത്തിപ്പിക്കുന്നത് പോലെ തന്നെ ഫലം ലഭിക്കുന്ന രീതിയിൽ നിങ്ങളുടെ jobs ക്രമീകരിക്കുക. ഈ ഒരു ശീലം സാമ്പത്തികവും ഡാറ്റാ-ഇന്റഗ്രിറ്റി സംബന്ധവുമായ ബഗുകളുടെ ഒരു വലിയ വിഭാഗത്തെ തന്നെ ഇല്ലാതാക്കും.

Pattern 4: നിങ്ങളുടെ Dead-Letter Queue-നെ ഒരു Dashboard പോലെ പരിഗണിക്കുക

ഒരു DLQ എന്നത് പരാജയപ്പെട്ട ജോലികൾ മറവിയിലേക്ക് പോകുന്ന ഒരു ശ്മശാനമല്ല. അതൊരു ഓപ്പറേഷണൽ ടൂൾ ആണ്, കൂടാതെ നിങ്ങൾ ഏറ്റവും കൂടുതൽ നിരീക്ഷിക്കേണ്ട കാര്യങ്ങളിൽ ഒന്നായിരിക്കണം അത്.

DLQ-യുടെ ആഴം (depth) കൂടുമ്പോൾ പ്രവർത്തിക്കുന്ന തരത്തിലുള്ള അലേർട്ടുകൾ (alerts) സജ്ജീകരിക്കുക. DLQ-യിൽ ഒരു മെസ്സേജ് പോലും ഉണ്ടെങ്കിൽ, അത് നിങ്ങളുടെ validation logic തകരാറിലായതുകൊണ്ടോ, ഒരു upstream schema മാറിയതുകൊണ്ടോ, അല്ലെങ്കിൽ ഒരു downstream service നിങ്ങൾക്ക് തിരിച്ചറിയാൻ കഴിയാത്ത തെറ്റായ വിവരങ്ങൾ (garbage) അയക്കുന്നത് കൊണ്ടോ ആകാം. ഉപഭോക്താക്കൾ പരാതിപ്പെടാൻ തുടങ്ങുന്നതിന് മുമ്പ് നിങ്ങൾ കണ്ടെത്തേണ്ട സൂചനകളാണിവ. Raw payload, stack trace, timestamp എന്നിവ പരിശോധിക്കാൻ കഴിയുന്ന ഒരു dashboard നിർമ്മിക്കുക. ഒരു runbook തയ്യാറാക്കി വെക്കുക: പരാജയത്തിന്റെ കാരണം പരിശോധിക്കുക, കോഡ് ശരിയാക്കുക (patch), തുടർന്ന് ശരിയായ ക്രമത്തിൽ മെസ്സേജുകൾ വീണ്ടും പ്രവർത്തിപ്പിക്കുക (replay). നിങ്ങളുടെ queue implementation പിന്തുണയ്ക്കുന്നുണ്ടെങ്കിൽ, മെസ്സേജുകളുടെ എണ്ണം കൂടുന്നതിനൊപ്പം അവയുടെ വളർച്ചാ നിരക്ക് (growth rate) അടിസ്ഥാനത്തിലും അലേർട്ടുകൾ നൽകുക; കാരണം ഒരു തെറ്റായ deploy മിനിറ്റുകൾക്കുള്ളിൽ DLQ നിറയാൻ കാരണമായേക്കാം.

Pattern 5: സംശയമുണ്ടെങ്കിൽ, പ്രോസസ്സ് ക്രാഷ് ചെയ്യുക (Crash the Process)

Node.js ഒരു V8 isolate-നുള്ളിൽ ഒരു സിംഗിൾ ഇവന്റ് ലൂപ്പിലാണ് (single event loop) പ്രവർത്തിക്കുന്നത്. കൈകാര്യം ചെയ്യാത്ത ഒരു promise rejection അല്ലെങ്കിൽ ഒരു