.NET 8-ലെ TimeProvider-ഉം ഒരു ചെറിയ SynchronizationContext മാറ്റവും ഉപയോഗിച്ച്, റീട്രൈ ലോജിക് (retry-logic) യൂണിറ്റ് ടെസ്റ്റുകളുടെ സമയം വലിയ തോതിൽ കുറയ്ക്കാൻ സാധിക്കും. യഥാർത്ഥത്തിലുള്ള 'sleep' സമയങ്ങൾ ഒഴിവാക്കുന്നതിലൂടെ ഇത് സാധ്യമാകുന്നു. ഏഴ് സെക്കൻഡ് എടുത്തിരുന്ന ഒരു ടെസ്റ്റ് ഇപ്പോൾ ഏതാനും മില്ലിസെക്കൻഡുകൾക്കുള്ളിൽ പൂർത്തിയാകും. കൂടാതെ, പ്രൊഡക്ഷനിൽ ഇത് പ്രവർത്തിപ്പിക്കുമ്പോൾ അധികമായ യാതൊരു ചിലവും ഉണ്ടാകില്ല.
എന്തുകൊണ്ടാണ് ഈ താമസം പ്രശ്നമാകുന്നത്
മിക്ക റീട്രൈ ഹെൽപ്പറുകളും (retry helpers) exponential backoff രീതിയാണ് ഉപയോഗിക്കുന്നത്: അതായത് ആദ്യം 1 സെക്കൻഡ്, പിന്നെ 2 സെക്കൻഡ്, പിന്നെ 4 സെക്കൻഡ് എന്നിങ്ങനെ കാത്തിരിക്കുന്നു. പ്രൊഡക്ഷനിൽ, പരാജയപ്പെടുന്ന ഒരു എൻഡ്പോയിന്റിലേക്ക് (endpoint) തുടർച്ചയായി റിക്വസ്റ്റുകൾ അയക്കുന്നത് തടയാൻ ഇത് സഹായിക്കുന്നു. എന്നാൽ ഒരു ടെസ്റ്റ് സ്യൂട്ടിൽ, ഇതേ കോഡ് യഥാർത്ഥ ക്ലോക്കിൽ Task.Delay വിളിക്കുന്നു, അതിനാൽ ടെസ്റ്റ് ത്രെഡ് (test thread) യഥാർത്ഥത്തിൽ ഉറങ്ങുകയാണ് (sleep). ഡസൻ കണക്കിന് ടെസ്റ്റുകൾ ഇത്തരത്തിൽ ചെയ്യുമ്പോൾ, പ്രവർത്തനപരമായ ഗുണമൊന്നുമില്ലാതെ തന്നെ continuous-integration (CI) പൈപ്പ്ലൈൻ മിനിറ്റുകൾ നീണ്ടുപോകുന്നു. ഈ "sleep tax" വെറും സമയനഷ്ടം മാത്രമാണ്.
TimeProvider എങ്ങനെ പ്രവർത്തിക്കുന്നു
സിസ്റ്റം ക്ലോക്കിന് മുകളിൽ ഒരു അബ്സ്ട്രാക്ഷനായി (abstraction) ആണ് .NET 8 TimeProvider അവതരിപ്പിച്ചത്. നിലവിലെ സമയം അറിയേണ്ടതോ അല്ലെങ്കിൽ ഡിലേ (delay) ആവശ്യമായതോ ആയ ക്ലാസുകൾക്ക് DateTime.UtcNow അല്ലെങ്കിൽ Task.Delay നേരിട്ട് വിളിക്കുന്നതിന് പകരം ഒരു TimeProvider ഇൻസ്റ്റൻസ് സ്വീകരിക്കാവുന്നതാണ്. പ്രൊഡക്ഷനിൽ നിങ്ങൾ TimeProvider.System നൽകുന്നു, ഇത് യഥാർത്ഥ ക്ലോക്കിലേക്ക് റിക്വസ്റ്റ് കൈമാറുന്നു. എന്നാൽ ഒരു ടെസ്റ്റിൽ നിങ്ങൾ ഒരു FakeTimeProvider നൽകുന്നു. Advance(TimeSpan) ഉപയോഗിച്ച് സമയം മാനുവലായി മുന്നോട്ട് കൊണ്ടുപോകാൻ ഈ ഫേക്ക് പ്രൊവൈഡർ നിങ്ങളെ അനുവദിക്കുന്നു. ആന്തരികമായി Task.Delay ഈ പ്രൊവൈഡറിൽ നിന്നാണ് സമയം വായിക്കുന്നത്, അതിനാൽ ഫേക്ക് ക്ലോക്ക് മുന്നോട്ട് നീക്കുമ്പോൾ നിലവിലുള്ള ഡിലേ ഉടൻ തന്നെ പൂർത്തിയാകുന്നു.
ഇതിന്റെ ആശയം ലളിതമാണ്: യഥാർത്ഥ ക്ലോക്കിന് പകരം നിയന്ത്രിക്കാൻ കഴിയുന്ന ഒരു ക്ലോക്ക് ഉപയോഗിക്കുക, തുടർന്ന് ടെസ്റ്റ് ചെയ്യുന്ന കോഡ് വീണ്ടും പ്രവർത്തിക്കേണ്ട സമയത്തേക്ക് നേരിട്ട് ചാടുക.
xUnit SynchronizationContext കെണി
ഈ രീതി ഒരു കൺസോൾ ആപ്പിൽ (console app) നന്നായി പ്രവർത്തിക്കും, എന്നാൽ xUnit-ൽ ഇത് ഒരു ഡെഡ്ലോക്കിന് (deadlock) കാരണമായി. റീട്രൈ ഹെൽപ്പർ ഒരു ഡിലേക്കായി കാത്തിരിക്കുമ്പോൾ ടെസ്റ്റ് Advance() വിളിച്ചു. xUnit അതിന്റെ തന്നെ SynchronizationContext ഇൻസ്റ്റാൾ ചെയ്യുന്നു, ഇത് continuations ക്യാപ്ചർ ചെയ്യുകയും അവ ടെസ്റ്റ് ത്രെഡിൽ പ്രവർത്തിപ്പിക്കുകയും ചെയ്യുന്നു. Advance() ക്ലോക്ക് മുന്നോട്ട് നീക്കിയപ്പോൾ, continuation ടെസ്റ്റ് ത്രെഡിന് പകരം ത്രെഡ് പൂളിലേക്ക് (thread pool) ക്യൂ ചെയ്യപ്പെട്ടു. ടെസ്റ്റ് ത്രെഡ് സമയം മുന്നോട്ട് നീക്കിക്കൊണ്ടിരുന്നു, ഇത് സിമുലേറ്റഡ് ക്ലോക്കിനെ അടുത്ത റീട്രൈ നടക്കേണ്ട സമയത്തിന് അപ്പുറത്തേക്ക് എത്തിച്ചു. ക്ലോക്ക് വീണ്ടും നീക്കാൻ ഒരു ത്രെഡ് പോലും അവശേഷിക്കാത്തതിനാൽ, പുതിയ ടൈമർ സെറ്റ് ചെയ്ത ഭാവിയിലെ സമയത്ത് ഒരിക്കലും എത്തിച്ചേരാൻ കഴിയില്ല. അങ്ങനെ ടെസ്റ്റ് അനന്തമായി ഹാങ്ങ് ആയിപ്പോയി.
ഒറ്റ വരിയിലുള്ള പരിഹാരം
ടെസ്റ്റിന്റെ തുടക്കത്തിൽ ഒരു വരി മാത്രം ചേർക്കുന്നത് പ്രതീക്ഷിച്ച രീതിയിൽ പ്രവർത്തിക്കാൻ സഹായിക്കുന്നു:
SynchronizationContext.SetSynchronizationContext(null);
കസ്റ്റം കോൺടെക്സ്റ്റ് (custom context) ക്ലിയർ ചെയ്യുന്നത് വഴി await continuations ത്രെഡ് പൂളിൽ പ്രവർത്തിക്കാൻ നിർബന്ധിക്കുന്നു, അവിടെ ഫേക്ക് ടൈമറുടെ കാൾബാക്കുകൾ (callbacks) പ്രവർത്തിക്കാൻ സാധിക്കും. ഈ മാറ്റത്തിലൂടെ അതേ ടെസ്റ്റിന്റെ സമയം ഏഴ് സെക്കൻഡിൽ നിന്ന് ഏകദേശം 36 മില്ലിസെക്കൻഡായി കുറയുന്നു.
കൂടുതൽ ഗുണങ്ങൾ
വെറുതെ സമയം കളയുന്ന 'sleep' ഒഴിവാക്കുന്നതിലുപരി, ടൈം സോൺ (time-zone) സംബന്ധമായ ലോജിക്കുകൾ ടെസ്റ്റ് ചെയ്യാൻ ഒരു ഫേക്ക് ക്ലോക്ക് എളുപ്പമാക്കുന്നു. യഥാർത്ഥ ക്ലോക്ക് പന്ത്രണ്ട് മണി അടിക്കുന്നത് വരെ കാത്തുനിൽക്കാതെ തന്നെ, ഒരു ഡെയ്ലി ക്വാട്ട (daily quota) കൃത്യമായ പ്രാദേശിക അർദ്ധരാത്രിയിൽ റീസെറ്റ് ആകുന്നുണ്ടോ എന്ന് നിങ്ങൾക്ക് പരിശോധിക്കാം. നിലവിലെ സമയത്തെ അടിസ്ഥാനമാക്കി പ്രവർത്തിക്കുന്ന ഏത് കോഡിനും ഇതേ രീതി ഉപയോഗിക്കാം: പ്രൊവൈഡർ ഇൻജക്റ്റ് ചെയ്യുക, Thread.Sleep അല്ലെങ്കിൽ Task.Delay പോലുള്ള ഒളിഞ്ഞിരിക്കുന്ന കോളുകൾ ഒഴിവാക്കുക, അങ്ങനെ നിങ്ങൾക്ക് വേഗതയേറിയതും കൃത്യവുമായ ടെസ്റ്റുകൾ ലഭിക്കും.
ശ്രദ്ധിക്കേണ്ട കാര്യങ്ങൾ
- എല്ലാ ഡിലേകളും ഇൻജക്റ്റ് ചെയ്ത
TimeProvider-ലൂടെ തന്നെയായിരിക്കണം നടക്കേണ്ടത്. അറിയാതെ ഉപയോഗിക്കുന്ന ഒരുThread.Sleep-ഓ അല്ലെങ്കിൽ നേരിട്ടുള്ളTask.Delay-ഓ ഉണ്ടെങ്കിൽ അത് യഥാർത്ഥ ക്ലോക്കിനെ തന്നെ ഉപയോഗിക്കും, ഇത് വീണ്ടും താമസം ഉണ്ടാക്കും. - നിങ്ങൾ ഉപയോഗിക്കുന്ന ഒരു ലൈബ്രറി ഉള്ളിൽ
Task.Delayഉപയോഗിക്കുന്നുണ്ടെങ്കിലും പ്രൊവൈഡർ നൽകുന്നില്ലെങ്കിൽ, കൂടുതൽ സങ്കീർണ്ണമായ രീതികൾ ഇല്ലാതെ അതിന്റെ സമയം നിയന്ത്രിക്കാൻ കഴിയില്ല. അത്തരം സാഹചര്യങ്ങളിൽ ഇതിന്റെ ഗുണം പരിമിതമായിരിക്കും. - ഒളിഞ്ഞിരിക്കുന്ന കാത്തിരിപ്പുകൾ കണ്ടെത്താൻ ടെസ്റ്റ് ഫോൾഡറിൽ
Task.Delay,Thread.Sleepഎന്നിവ തിരയുക (grep). ഫേക്ക് ക്ലോക്ക് ഫലപ്രദമായി നിലനിർത്താൻ ഈ കോളുകൾ പ്രൊവൈഡർ ഉപയോഗിക്കുന്ന രീതിയിലേക്ക് മാറ്റുക (refactor) എന്നതാണ് ഏക വഴി.
ചുരുക്കത്തിൽ
സിസ്റ്റം ക്ലോക്കിന് പകരം TimeProvider ഉപയോഗിക്കുന്നതും xUnit-ന്റെ SynchronizationContext പ്രവർത്തനരഹിതമാക്കുന്നതും സാവധാനത്തിൽ നടക്കുന്ന റീട്രൈ ടെസ്റ്റുകളെ നിമിഷങ്ങൾക്കുള്ളിൽ പൂർത്തിയാകുന്ന പരിശോധനകളാക്കി മാറ്റുന്നു. ഇത് CI റിസോഴ്സുകൾ ലാഭിക്കാനും സമയത്തെ ആശ്രയിച്ചുള്ള കോഡുകൾ എളുപ്പത്തിൽ പരിശോധിക്കാനും സഹായിക്കുന്നു. ഇതിനായി വേണ്ടത് ഏതാനും വരി കോഡുകൾ മാത്രമാണ്; എന്നാൽ ഇതിലൂടെ ഓരോ ടെസ്റ്റ് റണ്ണിലും ലഭിക്കുന്ന ലാഭം സെക്കൻഡുകളിൽ കണക്കാക്കാവുന്നതാണ്.
