ஒரு பயனர் ஒரு பொத்தானைக் கிளிக் செய்கிறார். கோரிக்கை (request) முடங்குகிறது. பத்து வினாடிகள் அமைதி நிலவுகிறது. அவர்கள் மாற்றுப் பொத்தானை (fallback button) கிளிக் செய்கிறார்கள். இப்போது ஒரே நோக்கத்திற்காக இரண்டு வேலைகள் (jobs) ஓடிக்கொண்டிருக்கின்றன. இதன் விளைவாகத் திரும்பத் திரும்பச் Sid-effects (duplicate side effects), இரட்டைப் கட்டணங்கள் மற்றும் உங்கள் மதிய நேரத்தையே முழுவதையும் வீணடிக்கும் தரவுச் சிக்கல்கள் (data mess) உருவாகின்றன.

இது ஒரு frontend பிழை அல்ல. React-இல் ஒரு disabled button அல்லது debounce timer உங்களைக் காப்பாற்றாது. முதல் கோரிக்கை ஏற்கனவே செயல்பாட்டில் (in flight) இருந்தது. நெட்வொர்க் அந்தப் பதிலைத் தின்றுவிட்டது (swallowed the response). உங்கள் backend ஒவ்வொரு புதிய கோரிக்கையையும் ஒரு புதிய அறிவுறுத்தலாகக் கருதினால், மீண்டும் முயற்சி செய்தல் (retries) என்பது ஒரு சுமையாகிவிடும். இதை உங்கள் API வடிவமைப்பு மற்றும் தரவுத்தளத் திட்டத்தில் (database schema) சரிசெய்ய வேண்டும்.

இதற்கான தீர்வு ஒரு எளிய கட்டமைப்புப் பிரிவிலிருந்து தொடங்குகிறது.

Jobs மற்றும் Attempts-ஐப் பிரியுங்கள்

ஒரு 'job'-ஐ பயனர் என்ன விரும்புகிறார் என்பதன் நிலையான பதிவாகக் கருதவும். இது உரிமையாளர் (owner), அளவுருக்கள் (parameters), இலக்கு வழங்குநர் (target provider) மற்றும் துல்லியமான நோக்கம் (intent) ஆகியவற்றைச் சேகரிக்கிறது. ஒரு 'attempt' என்பது அந்த நோக்கத்தை நிறைவேற்ற மேற்கொள்ளப்படும் ஒரு குறிப்பிட்ட முயற்சியாகும்.

ஒரு அச்சுக்கலையை (print shop) கற்பனை செய்து பாருங்கள். நீங்கள் ஒரு கோப்பை ஒப்படைக்கிறீர்கள், அவர்கள் உங்களுக்கு டிக்கெட் #45-ஐ வழங்குகிறார்கள். அந்த டிக்கெட் தான் 'job'. கடை முதலில் இங்க்ஜெட் பிரிண்டரை (inkjet printer) முயற்சிக்கிறது. அது சிக்கிக்கொள்கிறது (jams). அதுதான் முதல் முயற்சி (attempt one). அவர்கள் கோப்பை லேசர் பிரிண்டருக்கு (laser printer) மாற்றுகிறார்கள். அது இரண்டாவது முயற்சி (attempt two). இந்தச் செயல்முறை முழுவதும், டிக்கெட் #45 மாறாது. ஒவ்வொரு பிரிண்டருக்கும் அவர்கள் ஒரு புதிய டிக்கெட்டை வழங்கியிருந்தால், நீங்கள் மூன்று முறை பணம் செலுத்த வேண்டியிருக்கும் மற்றும் மூன்று தேவையற்ற நகல்களைப் பெற வேண்டியிருக்கும்.

உங்கள் தரவுத்தளம் இதைப் பிரதிபலிக்க வேண்டும். ஒரு அட்டவணை 'jobs'-ஐக் கொண்டிருக்கும். மற்றொரு அட்டவணை 'attempts'-ஐக் கொண்டிருக்கும். 'attempts'கள் அதிகரித்துக் கொண்டே இருந்தாலும், 'job' வரி (row) மாறாமல் இருக்கும்.

இந்தத் தனிமைப்படுத்துதல் உங்களுக்குக் கட்டுப்பாட்டைத் தருகிறது. மேலும், நெட்வொர்க் தடங்கல்களிலும் அழியாத ஒரு idempotency key-ஐ இணைப்பதற்கான இடத்தையும் இது வழங்குகிறது.

ஒவ்வொரு Job-க்கும் ஒரு Idempotency Key-ஐக் கட்டாயமாக்குங்கள்

ஒரு job-ஐ உருவாக்கும் ஒவ்வொரு POST கோரிக்கையும் ஒரு தனித்துவமான idempotency key-ஐக் கொண்டிருக்க வேண்டும். இந்தத் திறவுகோல் (key) பயனருக்குச் சொந்தமானது, session-க்கு அல்ல. உரிமையாளர் ID (owner ID) மற்றும் key ஆகிய இரண்டையும் இணைத்து, அந்த இரண்டு நெடுவரிசைகளுக்கும் (columns) இடையே ஒரு தனித்துவமான தரவுத்தளக் கட்டுப்பாட்டை (unique database constraint) அமல்படுத்துங்கள்.

ஏன் தரவுத்தளக் கட்டுப்பாடு? ஏனெனில், தரவைச் சேர்ப்பதற்கு முன் பயன்பாட்டு நிரலில் (application code) அது ஏற்கனவே உள்ளதா என்று சரிபார்ப்பது ஒரு 'race condition' சிக்கலை உருவாக்கும். இரண்டு ஒரே மாதிரியான கோரிக்கைகள் ஒரே மைக்ரோ வினாடி இடைவெளியில் உள்ளே நுழையக்கூடும். தரவுத்தளத்தையே இதற்குக் காவலராக இருக்க விடுங்கள். ஒரு பயனர் ஒரே owner ID மற்றும் key-ஐ இருமுறை அனுப்பினால், இரண்டாவது கோரிக்கை அந்தத் தனித்துவ மீறலைக் (unique violation) கண்டறியும், அப்போது நீங்கள் ஏற்கனவே உள்ள job-ஐத் திருப்பி அனுப்பலாம். இரண்டு கோரிக்கைகளுக்கும் ஒரே job ID கிடைக்கும். எந்தத் தேவையற்ற வேலையும் தொடங்காது.

அதன் எல்லை (scope) குறித்துத் தெளிவாக இருங்கள். யாராவது அதே key-ஐப் பயன்படுத்தி உள்ளீட்டுத் தரவை (input payload) மாற்றினால், ஒரு முரண்பாட்டை (conflict) தெரிவிக்கவும். Idempotency key என்பது பயனருக்கு மட்டுமல்லாமல், துல்லியமான நோக்கத்துடன் (exact intention) பிணைக்கப்பட்டிருக்க வேண்டும். ஒரே key-யுடன் வேறுபட்ட உள்ளீடு என்பது கிளையண்ட் குழப்பத்தில் இருப்பதைக் குறிக்கிறது; உங்கள் அமைப்பு அதை ஊகிப்பதற்குப் பதிலாக நிராகரிக்க வேண்டும்.

நிலை மாற்றங்களைப் (State Transitions) பாதுகாக்கவும்

ஒரு attempt என்பது ஒரு நிலை மாற்றம் (state transition), புதிய job அல்ல. முந்தைய attempt இன்னும் தொடக்க நிலையில் அல்லது அறியப்படாத நிலையில் (unknown state) இருந்தால், ஒரு புதிய attempt-ஐ உருவாக்க உங்கள் API மறுக்க வேண்டும்.

இதற்குக் காரணம் 'timeouts' தான். ஒரு வழங்குநர் கோரிக்கை (provider request) காலாவதியானால் (timeout), கிளையண்ட் தோல்வியைக் காண்பான், ஆனால் சர்வர் பக்கச் செயல்முறை இன்னும் இயங்கிக் கொண்டிருக்கலாம். GPU கிளஸ்டர் இன்னும் உங்கள் inference கோரிக்கையைச் செயல்படுத்திக் கொண்டிருக்கலாம். கன்டெய்னர் (container) இன்னும் blob storage-இல் தரவை எழுதிக் கொண்டிருக்கலாம். காலாவதியான attempt-ஐத் தோல்வியடைந்ததாகக் குறித்துவிட்டு, உடனடியாக இரண்டாவது attempt-ஐத் தொடங்கினால், நீங்கள் மீண்டும் மீண்டும் நிகழும் பக்கவிளைவுகளுடன் (duplicate side effects) சூதாடுகிறீர்கள் என்று அர்த்தம்.

ஒரு timeout-ஐத் தோல்வியடைந்த நிலையாகக் கருதாமல், ஒரு அறியப்படாத நிலையாகக் கருதவும். முந்தைய attempt ஒரு முடிவு நிலைக்கு (terminal state) வரும் வரை அல்லது ஒரு வெளிப்படையான செயல்முறை மூலம் ரத்து செய்யப்படும் வரை புதிய attempt-களைத் தடுக்கவும். இந்தத் தாமதம் சற்று அசௌகரியமாக இருக்கலாம், இது பயனரைத் காத்திருக்கத் தூண்டும். ஆனால், இது இரண்டு பணியாளர்கள் (workers) ஒரே வளங்களை (resources) மாற்றுவதால் ஏற்படும் குழப்பத்தைத் தடுக்கிறது.

Compare-and-Swap மூலம் Race சிக்கல்களைத் தீர்க்கவும்

பல attempts முடிவடையும் போதுதான் கடினமான சிக்கல்கள் எழுகின்றன. ஒருவேளை உங்கள் அமைப்பு முதன்மை வழங்குநருக்கு (primary provider) முதல் attempt-ஐ அனுப்பியிருக்கலாம். பத்து வினாடிகள் அமைதிக்குப் பிறகு, அது மாற்று வழங்குநருக்கு (fallback) இரண்டாவது attempt-ஐ அனுப்பியிருக்கலாம். இப்போது இரண்டு attempt-களும் முடிந்துவிட்டன. இரண்டுமே தங்கள் முடிவுகளை ஒரே job வரிசையில் எழுத அனுமதிக்க முடியாது.

'compare-and-swap' தர்க்கத்தைப் (logic) பயன்படுத்தவும். Job வரிசையில் ஒரு பதிப்பு எண்ணைச் (version number) சேர்க்கவும். ஒரு attempt முடிவடையும் போது, பின்வரும் நிபந்தனைகளுடன் ஒரு update-ஐ இயக்கவும்:

  • தற்போதைய பதிப்பு (current version), அந்த attempt தொடக்கத்தில் படித்த பதிப்பைப் போலவே இருக்க வேண்டும்.
  • வேறு எந்த attempt-உம் ஏற்கனவே முடிவு இடத்தைப் (result slot) பிடிしていிருக்கக் கூடாது.
  • இவை இரண்டும் சரியாக இருந்தால், முடிவை எழுதி பதிப்பு எண்ணை அதிகரிக்கவும்.

SQL ரீதியாகப் பார்த்தால், அது WHERE id = $1 AND version = $2 AND completed_by IS NULL கொண்ட ஒரு update கூற்றைப் (statement) போல இருக்கும். அந்த update பூஜ்ஜிய வரிசைகளைத் திருப்பிக் கொடுத்தால், மற்றொரு attempt ஏற்கனவே வெற்றி பெற்றுவிட்டது என்று அர்த்தம். தாமதமாக வந்த கோரிக்கையைப் புறக்கணிக்க வேண்டும். அதன் முடிவைத் தூக்கி எறியுங்கள். அதை இணைக்கவோ (merge) அல்லது சேர்க்கவோ (append) வேண்டாம். ஒரு முந்தைய வெற்றியாளரைத் தற்போதைய முடிவு மாற்றியமைப்பது தரவுச் சிதைவுக்கு (data corruption) வழிவகுக்கும், எனவே அதைத் தவிர்ப்பதே பாதுகாப்பானது.

This handles the reverse-order finish cleanly. Attempt A leaves first but returns after thirty seconds. Attempt B leaves second but returns after five seconds. Attempt B wins the compare-and-swap. Attempt A’s update touches zero rows. Your system logs the race, ignores the stale payload, and moves on.

Test the Breakpoints

You will not catch these bugs in happy-path testing. Your suite needs to target the fractures.

  • Simulate a double-click. Two simultaneous POST requests with the same idempotency key must return identical job IDs.
  • Send the same key with mismatched input. Expect a conflict response. The system must not silently return the existing job if the parameters differ.
  • Provoke a timeout. Verify the job lands in an unknown state, not a failed state, and that the system blocks further attempts until the ambiguity clears.
  • Force two attempts to finish in reverse order. Confirm that the second one to return loses, even if the first one to leave was the official primary provider.

These tests are not edge-case luxuries. They are the contract your API makes with the rest of the system.

Validate Provider Intent Before You Fail Over

If you run a multi-provider setup, you might be tempted to treat different AI models as interchangeable slots. They share the same code path, the same HTTP client, and the same JSON schema. That does not mean they behave the same.

One model might hallucinate a top-level key. Another might ignore your system prompt formatting. Schema validation catches syntax errors, but it will pass a response that your business logic cannot interpret. A provider might return valid JSON that simply does the wrong thing with your prompt template.

Run provider-specific tests before you allow automatic model switching. Confirm that the fallback model actually respects your output structure at low temperature. Verify that your prompt renders correctly through that provider’s tokenizer. Test the full round trip with real inputs. Automatic failover is only safe when you have proven that the fallback shares the same operational contract.

Keep One Job Per Intent

Fallback paths are good. Uncontrolled fallback multiplication is a bug. Every layer of your stack needs to evaluate whether it has already seen the exact task. The load balancer, the API handler, the database, and the worker must all respect the same identity.

Build your system so that retries and fallbacks surface as new attempts under one stable job. Lock the job down with a database-backed idempotency key. Guard the transitions. Race the attempts. Let exactly one win. That is how you keep a single user click from turning into a weekend of data cleanup.