એક યુઝર બટન પર ક્લિક કરે છે. રિક્વેસ્ટ અટકી જાય છે. દસ સેકન્ડ સુધી શાંતિ રહે છે. તેઓ ફોલબેક (fallback) બટન પર ક્લિક કરે છે. હવે એક જ ઈરાદા (intention) માટે બે જોબ્સ ચાલી રહ્યા છે. પરિણામે ડુપ્લીકેટ સાઈડ ઈફેક્ટ્સ, ડબલ ચાર્જ અને ડેટાની એવી ગડબડ થાય છે જે તમારો આખો બપોર બગાડી શકે છે.
આ કોઈ ફ્રન્ટએન્ડ બગ નથી. React માં ડિસેબલ કરેલું બટન અથવા ડિબાઉન્સ ટાઈમર (debounce timer) તમને બચાવી શકશે નહીં. પહેલી રિક્વેસ્ટ પહેલેથી જ ચાલુ હતી. નેટવર્કે ફક્ત રિસ્પોન્સને ગળી લીધો (swallowed). જો તમારું બેકએન્ડ દરેક આવતી રિક્વેસ્ટને એકદમ નવી સૂચના તરીકે ગણે છે, તો રિટ્રાય્સ (retries) જોખમી બની જાય છે. તમારે આ સમસ્યા તમારા API ડિઝાઈન અને ડેટાબેઝ સ્કીમામાં સુધારવાની જરૂર છે.
આનો ઉકેલ એક સરળ સ્ટ્રક્ચરલ સ્પ્લિટ (structural split) થી શરૂ થાય છે.
Jobs ને Attempts થી અલગ કરો
જોબને યુઝર શું ઈચ્છે છે તેના કાયમી રેકોર્ડ તરીકે વિચારો. તે ઓનર (owner), પેરામીટર્સ, ટાર્ગેટ પ્રોવાઈડર અને ચોક્કસ ઈરાદાને કેપ્ચર કરે છે. 'એટેમ્પ્ટ' (attempt) એ તે ઈરાદાને પૂર્ણ કરવાનો એક ચોક્કસ પ્રયાસ છે.
એક પ્રિન્ટ શોપની કલ્પના કરો. તમે એક ફાઇલ આપો છો અને તેઓ તમને ટિકિટ #45 આપે છે. તે ટિકિટ એ 'જોબ' છે. શોપ ઇન્કજેટ પ્રિન્ટરનો પ્રયાસ કરે છે. તે જામ થઈ જાય છે. તે પહેલો પ્રયાસ (attempt one) છે. તેઓ ફાઇલને લેસર પ્રિન્ટર પર લઈ જાય છે. તે બીજો પ્રયાસ (attempt two) છે. આખી પ્રક્રિયા દરમિયાન, ટિકિટ #45 ક્યારેય બદલાતી નથી. જો શોપ દરેક પ્રિન્ટર માટે નવી ટિકિટ ઇશ્યૂ કરે, તો તમારે ત્રણ વાર ચૂકવણી કરવી પડે અને ત્રણ બિનજરૂરી નકલો મળે.
તમારો ડેટાબેઝ પણ આવું જ હોવો જોઈએ. એક ટેબલ જોબ્સ રાખે છે. બીજું ટેબલ એટેમ્પ્ટ્સ રાખે છે. જોબ રો (row) સ્થિર રહે છે જ્યારે એટેમ્પ્ટ્સ તેની નીચે એકઠા થતા જાય છે.
આ અલગ પાડવાથી તમને નિયંત્રણ મળે છે. તે તમને એક આઈડેમપોટન્સી કી (idempotency key) જોડવા માટે જગ્યા પણ આપે છે જે નેટવર્કની સમસ્યાઓ (network blips) વચ્ચે પણ ટકી રહે છે.
દરેક જોબ માટે Idempotency Key ફરજિયાત કરો
દરેક POST રિક્વેસ્ટ જે જોબ બનાવે છે તેમાં એક યુનિક આઈડેમપોટન્સી કી હોવી જોઈએ. આ કી યુઝરની છે, સેશનની નહીં. ઓનર ID અને કીને ભેગા કરો, અને પછી તે બે કોલમ પર યુનિક ડેટાબેઝ કન્સ્ટ્રેઈન્ટ (unique database constraint) લાગુ કરો.
ડેટાબેઝ કન્સ્ટ્રેઈન્ટ કેમ? કારણ કે ઇન્સર્ટ કરતા પહેલા એપ્લિકેશન કોડમાં તેની હાજરી તપાસવી એ 'રેસ કન્ડિશન' (race condition) ઊભી કરી શકે છે. બે સમાન રિક્વેસ્ટ એક જ માઈક્રોસેકન્ડના ગેપમાંથી નીકળી શકે છે. ડેટાબેઝને જ અમલીકરણકર્તા (enforcer) બનવા દો. જો યુઝર એક જ ઓનર ID અને કી બે વાર મોકલે છે, તો બીજી રિક્વેસ્ટ યુનિક વાયોલેશન પકડી લેશે અને તમે હાલની જોબ પરત કરશો. બંને રિક્વેસ્ટને એક જ જોબ ID મળશે. કોઈ ડુપ્લીકેટ કામ શરૂ થશે નહીં.
સ્કોપ (scope) બાબતે કડક રહો. જો કોઈ કીનો ફરીથી ઉપયોગ કરે પરંતુ ઇનપુટ પેલોડ (input payload) બદલે, તો 'conflict' રિટર્ન કરો. આઈડેમપોટન્સી કી માત્ર યુઝર સાથે જ નહીં, પણ ચોક્કસ ઈરાદા સાથે જોડાયેલી હોવી જોઈએ. અલગ ઇનપુટ સાથે સમાન કીનો અર્થ એ છે કે ક્લાયન્ટ મૂંઝવણમાં છે, અને તમારા સિસ્ટમે અનુમાન લગાવવાને બદલે તેને નકારી દેવી જોઈએ.
સ્ટેટ ટ્રાન્ઝિશન (State Transitions) ને સુરક્ષિત કરો
એટેમ્પ્ટ એ સ્ટેટ ટ્રાન્ઝિશન છે, નવી જોબ નથી. જો અગાઉનો એટેમ્પ્ટ હજુ પણ 'સ્ટાર્ટિંગ' અથવા 'અનનોન' સ્ટેટમાં અટકેલો હોય, તો તમારા API એ નવો એટેમ્પ્ટ શરૂ કરવાનો ઇનકાર કરવો જોઈએ.
ટાઈમઆઉટ (Timeouts) એ તેનું કારણ છે. જ્યારે પ્રોવાઈડર રિક્વેસ્ટ ટાઈમઆઉટ થાય છે, ત્યારે ક્લાયન્ટને નિષ્ફળતા દેખાય છે, પરંતુ સર્વર-સાઇડ પ્રોસેસ હજુ પણ ચાલુ હોઈ શકે છે. GPU ક્લસ્ટર હજુ પણ તમારી ઇન્ફરન્સ રિક્વેસ્ટ પર કામ કરી રહ્યું હોઈ શકે છે. કન્ટેનર હજુ પણ બ્લોબ સ્ટોરેજમાં લખી રહ્યું હોઈ શકે છે. જો તમે ટાઈમઆઉટ થયેલા એટેમ્પ્ટને 'ફેઈલ' તરીકે માર્ક કરો છો અને તરત જ બીજો એટેમ્પ્ટ શરૂ કરો છો, તો તમે ડુપ્લીકેટ સાઈડ ઈફેક્ટ્સનું જોખમ લઈ રહ્યા છો.
ટાઈમઆઉટને 'ફેઈલ' સ્ટેટ તરીકે નહીં, પણ 'અનનોન' (unknown) સ્ટેટ તરીકે ગણો. જ્યાં સુધી અગાઉનો એટેમ્પ્ટ 'ટર્મિનલ સ્ટેટ' (terminal state) પર ન પહોંચે અથવા આઉટ-ઓફ-બેન્ડ પ્રોસેસ દ્વારા સ્પષ્ટપણે રદ ન કરવામાં આવે ત્યાં સુધી નવા એટેમ્પ્ટ્સને બ્લોક કરો. આ વિરામ અસુવિધાજનક છે. તે યુઝરને રાહ જોવાની ફરજ પાડે છે. તે બે વર્કર્સ દ્વારા એક જ ડાઉનસ્ટ્રીમ રિસોર્સમાં ફેરફાર કરવાથી થતી ગડબડને પણ અટકાવે છે.
Compare-and-Swap સાથે રેસ કન્ડિશન ઉકેલો
સૌથી મુશ્કેલ સમસ્યાઓ ત્યારે સામે આવે છે જ્યારે એકસાથે અનેક એટેમ્પ્ટ્સ પૂર્ણ થાય છે. કદાચ તમારી સિસ્ટમે પ્રાઇમરી પ્રોવાઈડર સામે પહેલો એટેમ્પ્ટ મોકલ્યો હોય. દસ સેકન્ડની શાંતિ પછી, તેણે ફોલબેક સામે બીજો એટેમ્પ્ટ મોકલ્યો. હવે બંને એટેમ્પ્ટ્સ પૂર્ણ થઈ ગયા છે. તમે બંનેને એક જ જોબ રોમાં તેમના પરિણામો લખવા દેવા જોઈએ નહીં.
Compare-and-swap લોજિકનો ઉપયોગ કરો. જોબ રોમાં વર્ઝન નંબર ઉમેરો. જ્યારે કોઈ એટેમ્પ્ટ પૂર્ણ થાય, ત્યારે તે શરતો સાથે અપડેટ રન કરે છે:
- વર્તમાન વર્ઝન એ હોવું જોઈએ જે એટેમ્પ્ટે શરૂઆતમાં વાંચ્યું હતું.
- અન્ય કોઈ એટેમ્પ્ટે રિઝલ્ટ સ્લોટ પહેલેથી જ ક્લેમ ન કર્યો હોવો જોઈએ.
- જો બંને શરતો પાસ થાય, તો પરિણામ લખો અને વર્ઝન વધારો.
SQL શબ્દોમાં, તે WHERE id = $1 AND version = $2 AND completed_by IS NULL સાથેના અપડેટ સ્ટેટમેન્ટ જેવું દેખાશે. જો અપડેટ શૂન્ય રો (rows) રિટર્ન કરે છે, તો તેનો અર્થ છે કે બીજો એટેમ્પ્ટ પહેલેથી જ જીતી ગયો છે. મોડા આવેલા રિક્વેસ્ટને અવગણવી જોઈએ. તેનું પરિણામ કાઢી નાખો. મર્જ (merge) ન કરો. એપેન્ડ (append) ન કરો. તે કામને ફેંકી દો. અગાઉના વિજેતા પર ઓવરરાઈટ કરતું મોડું પરિણામ ડેટા કરપ્શન છે, અને એકમાત્ર સુરક્ષિત પગલું તેને રદ કરવાનું છે.
આ રિવર્સ-ઓર્ડર ફિનિશને ચોખ્ખી રીતે હેન્ડલ કરે છે. પ્રયાસ A પહેલા નીકળે છે પરંતુ ત્રીસ સેકન્ડ પછી પાછો ફરે છે. પ્રયાસ B બીજા ક્રમે નીકળે છે પરંતુ પાંચ સેકન્ડ પછી પાછો ફરે છે. પ્રયાસ B 'compare-and-swap' જીતે છે. પ્રયાસ A નું અપડેટ શૂન્ય રો (rows) ને સ્પર્શે છે. તમારું સિસ્ટમ આ રેસને લોગ કરે છે, જૂના (stale) પેલોડને અવગણે છે, અને આગળ વધે છે.
બ્રેકપોઈન્ટ્સનું પરીક્ષણ કરો
તમે આ બગ્સને 'happy-path' ટેસ્ટિંગમાં પકડી શકશો નહીં. તમારા ટેસ્ટિંગ સ્યુટને ખામીઓ (fractures) ને લક્ષ્ય બનાવવાની જરૂર છે.
- ડબલ-ક્લિકનું અનુકરણ કરો. સમાન idempotency key સાથેના બે એકસાથે આવતા POST રિક્વેસ્ટ્સ સમાન job IDs આપવા જોઈએ.
- ખોટા ઇનપુટ સાથે સમાન કી મોકલો. સંઘર્ષ (conflict) પ્રતિસાદની અપેક્ષા રાખો. જો પેરામીટર્સ અલગ હોય, તો સિસ્ટમે ચુપચાપ હાલનું જોબ (job) પરત ન કરવું જોઈએ.
- ટાઈમઆઉટ (timeout) પેદા કરો. ચકાસો કે જોબ 'failed state' માં નહીં પણ 'unknown state' માં જાય છે, અને જ્યાં સુધી અસ્પષ્ટતા દૂર ન થાય ત્યાં સુધી સિસ્ટમ આગળના પ્રયાસોને બ્લોક કરે છે.
- બે પ્રયાસોને ઉલટા ક્રમમાં પૂર્ણ કરવા માટે મજબૂર કરો. ખાતરી કરો કે જે બીજો પાછો ફરે છે તે હારે છે, ભલે પહેલો નીકળનાર સત્તાવાર પ્રાથમિક પ્રોવાઈડર હોય.
આ ટેસ્ટ એ માત્ર 'edge-case' લક્ઝરી નથી. તે તમારા API દ્વારા બાકીની સિસ્ટમ સાથે કરવામાં આવેલો કરાર છે.
ફેલ-ઓવર (Fail Over) કરતા પહેલા પ્રોવાઈડરના ઈરાદાને પ્રમાણિત કરો
જો તમે મલ્ટી-પ્રોવાઈડર સેટઅપ ચલાવો છો, તો તમે વિવિધ AI મોડલ્સને પરસ્પર બદલી શકાય તેવા સ્લોટ્સ તરીકે ગણવાનું વિચારી શકો છો. તેઓ સમાન કોડ પાથ, સમાન HTTP ક્લાયન્ટ અને સમાન JSON સ્કીમા શેર કરે છે. તેનો અર્થ એ નથી કે તેઓ એકસરખું વર્તન કરે છે.
એક મોડલ ટોપ-લેવલ કીનું 'hallucinate' (ખોટું અનુમાન) કરી શકે છે. બીજું તમારા સિસ્ટમ પ્રોમ્પ્ટ ફોર્મેટિંગને અવગણી શકે છે. સ્કીમા વેલિડેશન સિન્ટેક્સ ભૂલો પકડી લે છે, પરંતુ તે એવો પ્રતિસાદ પસાર કરી દેશે જેને તમારું બિઝનેસ લોજિક સમજી શકશે નહીં. પ્રોવાઈડર માન્ય JSON પરત કરી શકે છે જે તમારા પ્રોમ્પ્ટ ટેમ્પલેટ સાથે ખોટી રીતે કામ કરે છે.
ઓટોમેટિક મોડલ સ્વિચિંગની મંજૂરી આપતા પહેલા પ્રોવાઈડર-વિશિષ્ટ ટેસ્ટ ચલાવો. ખાતરી કરો કે ફોલબેક (fallback) મોડલ લો ટેમ્પરેચર (low temperature) પર ખરેખર તમારા આઉટપુટ સ્ટ્રક્ચરનું પાલન કરે છે. ચકાસો કે તમારો પ્રોમ્પ્ટ તે પ્રોવાઈડરના ટોકનાઈઝર (tokenizer) દ્વારા યોગ્ય રીતે રેન્ડર થાય છે. વાસ્તવિક ઇનપુટ્સ સાથે સંપૂર્ણ રાઉન્ડ ટ્રિપ ટેસ્ટ કરો. ઓટોમેટિક ફેલઓવર ત્યારે જ સુરક્ષિત છે જ્યારે તમે સાબિત કરી દીધું હોય કે ફોલબેક સમાન ઓપરેશનલ કોન્ટ્રાક્ટ શેર કરે છે.
દરેક ઈરાદા (Intent) માટે એક જ જોબ રાખો
ફોલબેક પાથ સારા છે. અનિયંત્રિત ફોલબેક ગુણાકાર એ એક બગ છે. તમારા સ્ટેક (stack) ના દરેક લેયરને એ ચકાસવાની જરૂર છે કે તેણે તે જ ચોક્કસ કાર્ય પહેલેથી જોયું છે કે નહીં. લોડ બેલેન્સર, API હેન્ડલર, ડેટાબેઝ અને વર્કર - આ તમામ એ જ ઓળખનું પાલન કરવા જોઈએ.
તમારી સિસ્ટમ એવી રીતે બનાવો કે જેથી રીટ્રાય્સ (retries) અને ફોલબેક્સ એક સ્થિર જોબ હેઠળ નવા પ્રયાસો તરીકે દેખાય. ડેટાબેઝ-બેક કરેલી idempotency key સાથે જોબને લોક કરો. ટ્રાન્ઝિશનનું રક્ષણ કરો. પ્રયાસો વચ્ચે સ્પર્ધા થવા દો. બરાબર એક જ વિજેતા બને તે જુઓ. આ રીતે તમે યુઝરના એક ક્લિકને વીકએન્ડના ડેટા ક્લીનઅપમાં બદલાતા અટકાવી શકો છો.
