Workern dog. Synkroniseringen gjorde det inte.

Döda workern mitt i en batch och följ en kontakt: vilken cursor som sparades, varför den upprepade skrivningen inte landar två gånger och vem som äger synkroniseringen medan den startar om. Sedan de två incidenterna som visade att återhämtning bara var halva jobbet.

återställning vid synkroniseringsfel

Vad händer med en pågående synkronisering när workern dör mitt i en batch?

Cursorn flyttas först i slutet av en felfri batch. En worker som dödas mitt i en batch lämnar den alltså vid början av den batchen, och nästa körning läser samma sida. Varje posts leveransnyckel härleds från sidans cursor och postens position, och en ledger över slutförda skrivningar i sju dagar hindrar upprepningen från att landa två gånger. En lease per synkronisering hindrar två körningar från att arbeta samtidigt. Det var den enkla halvan. En synkronisering fortsatte att misslyckas, körning efter körning, innan något pausade den, och en arbetsyta låg tyst med fel och utan en enda lyckad körning innan något märkte det. Därför finns nu en breaker som pausar efter fem misslyckanden i rad och ett larm varje timme vid tjugofyra timmar utan lyckad körning.

Läs mer
På denna sida

I ett föredrag som AI Engineer publicerade den 3 oktober 2026 startade en developer advocate från Crusoe ett PyTorch-träningsjobb på två A100-noder. Sedan berättade talaren för demons eget övervakningssystem att en GPU hade kastat ett XID 79-fel. Noden markerades som nere och jobbet avbröts. Processen fick en SIGTERM och ”upp till två minuter på sig att hantera den SIGTERM:en och antingen spara sina checkpoints eller skriva ut sina loggar”. En reservnod tog över, och ”slurm-jobbet startar om. Det laddar checkpointen och fortsätter träningen där det slutade”. Strax under femton minuter från början till slut. Ingen blev larmad.

En synkronisering är ett mycket mindre djur än en träningskörning. Frågan är densamma. Vad behöver en körning för att det ska kosta minuter och inga dubbletter när workern dödas?

Det här är svaret för Tajos egen synkroniseringsrunner, läst ur koden och inte ur en broschyr. Reverse ETL beskriver jobbets form. Det här är vad som händer när jobbet dör.

Följ en kontakt

En körning läser källan en sida i taget, från en sparad cursor. Kalla sidans cursor c3 och vår kontakt den 212:e posten på den sidan. Tre saker gäller för den kontakten innan något skrivs.

För det första har den en leveransnyckel. Nyckeln är en hash av synkroniseringens id, källan, sidans cursor, positionen på sidan och själva posten. Cursorn och posten går in i hashen och bäddas aldrig in i nyckeln, så nyckeln innehåller ingen e-postadress.

För det andra går skrivningen via en ledger och inte direkt till destinationen. Innan destinationen anropas reserveras nyckeln som väntande. När skrivningen lyckas markeras raden som slutförd och sparas i sju dagar.

För det tredje har cursorn inte flyttats. Den flyttas bara i slutet av en batch, bara om varje post i den batchen har landat och bara efter att körningen har bevisat att den fortfarande äger synkroniseringen.

Döda nu workern mellan skrivningen och checkpointen.

Vid en SIGTERM avslutar varje köworker sitt arbete inom en gemensam deadline på femton sekunder, och processen avslutas efter tjugofem sekunder oavsett vad som fortfarande körs; systemd ger den fyrtiofem innan den dödar den. En körning som inte blev klar inom det fönstret är fortfarande markerad som pågående, och nästa worker plockar upp den via köns återställning av fastnade jobb. Vid ett hårt kill avslutas ingenting alls. Hur som helst gäller samma två fakta. Destinationen har vår kontakt. Vår cursor står fortfarande på c3.

Upprepningen

Nästa körning börjar vid c3, läser samma sida, härleder samma nyckel för den 212:e posten och frågar ledgern. Ledgern säger slutförd. Destinationen anropas inte; det sparade resultatet spelas upp och körningen går vidare. På bulkvägen, där Brevos import-endpoint tar emot en hel sida i ett anrop, körs samma kontroll per nyckel innan förfrågan byggs. En sida där varje nyckel redan är slutförd skickar ingenting.

Det är hela tricket, och det är värt att säga varför det fungerar. Nyckeln härleds från sidans cursor i början av batchen, inte från den körning som råkade vara igång. En krasch, ett nytt försök sextio sekunder senare och en schemalagd körning i morgon härleder alla samma nyckel för samma post. Ledgern bryr sig inte om vem av dem som frågar.

Nu kostnaden. Ledgern glömmer efter sju dagar. Kodens egen kommentar säger att sju dagar täcker ett fördröjt nytt försök och ett kort avbrott, och mer än så påstår den inte. En upprepning från en cursor som är äldre än så skriver igen. Och en körning som avbryts hårt kan få vänta. Den döda körningens lease håller i upp till femton minuter, och i en schemalagd synkronisering gäller att en körning vars lease är ledig städas bort och schemaläggs om först när den startade för mer än en timme sedan. Crusoes demo är tillbaka på under femton minuter. Vår är det inte alltid.

Vem äger synkroniseringen medan den startar om

En körning i taget äger en synkronisering. Körningen tar en lease i Redis med en egen token, en livslängd på femton minuter och en förnyelse var femte minut. Före varje checkpoint kontrollerar den att den fortfarande har den leasen. Om inte, stoppar den. Körningen registreras med den enkla mening som personen kommer att läsa: ”The run lost its slot part-way through and stopped to avoid writing twice.” (Körningen förlorade sin plats halvvägs och stoppade för att inte skriva två gånger.) En andra körning som krockar med en aktiv ägare försöks inte igen. Den avslutas med ”Another run of this sync was already in progress” (En annan körning av den här synkroniseringen pågick redan), och nästa schemalagda körning startar som vanligt.

Vem som äger åtgärden är också nedskrivet. Varje felkod pekar på en mening om vad som hände, en om vad som ska göras härnäst och en ägare: du eller Tajo. En destination som avvisade varje post för att mappningen är fel är din. En förlorad lease är vår. Körningens sida visar nästa steg; guiden för att återställa synkroniseringar visar vilket.

Hittills är detta den enkla halvan. En krasch kostar en upprepning och ingenting landar två gånger. Två incidenter lärde oss den andra halvan.

Misslyckande efter misslyckande

En synkronisering misslyckades om och om igen. Inte i en tät loop, utan en gång per schemaläggarintervall, körning efter körning, med samma meddelande varje gång: att ett anpassat id inte får innehålla ett kolon. Körningen nådde aldrig destinationen. Den dog medan den köades, innan någon runner såg den, så ett larm som var kopplat till runnern hade aldrig något att reagera på.

Ingenting höll koll på serien. Det är hela slutsatsen.

Lösningen är en breaker. Efter varje registrerat misslyckande läser den synkroniseringens fem senaste avslutade körningar sedan synkroniseringen senast ändrades. Om alla fem är fel pausar den synkroniseringen, skriver en granskningsrad med orsaken circuit_breaker_tripped och utlöser ett kritiskt larm. Serien härleds från de sparade körningarna, inte från en räknare i workern. Ett misslyckande räknas oavsett vilken väg det registrerades, inklusive köningsfelet som orsakade incidenten. Enligt kodens kommentar skulle den ha stoppat den incidenten vid körning fem. En lyckad körning nollställer serien. När pausen hävs aktiveras breakern igen i stället för att utlösas på nytt av de gamla raderna. Guiden för Shopify-connectorn säger samma sak på en rad: efter fem misslyckade körningar i rad pausar synkroniseringen sig själv och dess sida visar det senaste felet.

Vad breakern inte gör är att laga något. Crusoe byter ut noden och startar om jobbet utan att fråga. Tajo stannar och frågar: en pausad synkronisering förblir pausad tills en person åtgärdar orsaken och publicerar den igen. Det försiktiga standardvalet är medvetet: ett automatiskt nytt försök för en synkronisering som skriver till kundposter är i sig en skrivning.

Tystnaden som ingen såg

Invändningen är rimlig och lyder så här: ”En breaker som pausar efter fem misslyckanden hade fångat den andra incidenten också. Varför bygga en andra vakt?” För att den andra incidenten inte såg ut som ett misslyckande. Under en lång period hade en arbetsyta en aktiv synkronisering, en växande hög felrader och inte en enda lyckad körning, någonsin, och ingenting larmade. Varje signal som fanns var kopplad till aktivitet: en körning misslyckades, en webhook kom in, en budget överskreds. Noll lyckade körningar är ingen händelse. Det är frånvaron av en, och en breaker körs bara när ett misslyckande registreras. En schemaläggare som är död, en kö som har fastnat, en worker som aldrig startar: de registrerar ingenting, och en breaker vaknar aldrig.

Ett loks dödmansgrepp fungerar tvärtom. Föraren måste fortsätta att trycka på det, och tystnad stoppar tåget. Synkroniseringsversionen körs en gång i timmen, tretton minuter över jämnt. För varje arbetsyta med en aktiv synkronisering ställer den en enda fråga: har någon körning lyckats de senaste tjugofyra timmarna? En arbetsyta vars äldsta aktiva synkronisering aktiverades för mindre än ett dygn sedan lämnas i fred; den har inte haft ett dygn på sig att lyckas än. En som är äldre och tyst får ett kritiskt larm. Larmet säger vilken tystnad det är: körningar registrerade men ingen som lyckas, eller inga körningar registrerade alls. Det utlöses en gång per incident. En markör i Redis håller det stängt tills den första lyckade körningen och rensas sedan, så att nästa incident larmar igen.

Båda vakterna kom i en och samma commit, i efterhand. Breakerns kommentar säger att den skulle ha stoppat den första incidenten vid körning fem. Dödmanslarmets kommentar säger att en synkronisering som aktiverades för länge sedan och har noll körningar måste larma. Ingen av dem fanns när den behövdes. Vi skrev dem efteråt, med båda incidenterna noterade i filhuvudet så att ingen glömmer varför.

Vad som är byggt, och vad som inte är det

Byggt i skrivande stund:

  • Cursorn sparas i slutet av varje felfri batch, och igen vid en krasch.
  • Varje posts leveransnyckel härleds från sidans cursor och postens position och kontrolleras mot en ledger på sju dagar före varje anrop till destinationen.
  • En lease per synkronisering, med en livslängd på femton minuter.
  • Fem försök per körning, med backoff från sextio sekunder.
  • En breaker som pausar synkroniseringen efter fem misslyckanden i rad och registrerar varför.
  • Ett larm varje timme vid tjugofyra timmar utan lyckad körning.

Inte byggt, och värt att känna till innan du förlitar dig på det:

  • Dödmanslarmet bevakar arbetsytan, inte synkroniseringen. En frisk synkronisering döljer ett tyst syskon i samma arbetsyta.
  • Dess larm når Tajos driftpersonal: alltid en loggrad, och Sentry, en webhook eller ett mejl där det är konfigurerat. Det syns inte på synkroniseringens sida. Det gör breakerns paus.
  • Ingenting startar om en pausad synkronisering. Det gör en person.
  • Återhämtning efter ett hårt kill går inte snabbt. Leasen kan hålla i femton minuter och städningen väntar en timme.
  • Båda tröskelvärdena, fem misslyckanden och tjugofyra timmar, är inställningar för driftsättningen. Du kan inte ställa in dem per synkronisering.

En lång kedja av nästan felfria steg misslyckas ändå; vägen ut är att definiera sluttillståndet och kontrollera det. Den här artikeln är den operativa halvan av det. Definiera vad en död körning lämnar efter sig, och kontrollera för det ingenting som tystnad lämnar efter sig.

Crusoes avslutande rad var att ”fel är oundvikliga”. Vår går ett steg längre. Kraschen var den enkla halvan, eftersom den lämnade rader efter sig. Tystnaden var halvan vi var tvungna att leta efter.

Vanliga Frågor

Vad händer med en pågående synkronisering när workern dör mitt i en batch?

Cursorn pekar fortfarande på början av den avbrutna batchen, eftersom Tajo flyttar den först när en batch har landat felfritt. Nästa körning läser samma sida igen. Varje post har en leveransnyckel som härleds från sidans cursor och postens position, och en ledger sparar varje slutförd skrivning i sju dagar. Poster som redan har skrivits spelas därför upp från ledgern i stället för att skickas till destinationen en andra gång.

Varför skriver en upprepad synkronisering inte samma kontakt två gånger?

Före varje anrop till destinationen kontrollerar körningen postens leveransnyckel mot en ledger. Nyckeln är en hash av synkroniseringen, källan, sidans cursor, positionen på sidan och själva posten. En krasch, ett nytt försök en minut senare och en schemalagd körning nästa dag härleder alltså samma nyckel för samma post. En nyckel som redan är markerad som slutförd inom sjudagarsfönstret skickas inte igen.

Vad är ett dödmanslarm för en synkronisering?

En kontroll som utlöses av frånvaro i stället för av fel. En gång i timmen tittar Tajo på varje arbetsyta med en aktiv synkronisering och larmar när ingen körning har lyckats de senaste tjugofyra timmarna, även när ingen körning alls har registrerats. Larmet utlöses en gång per incident och aktiveras igen efter den första lyckade körningen. Det finns eftersom ett fellarm bara kan se fel som har registrerats.

Ansök om tidig åtkomst

Ange ditt förnamn och din e-postadress. Vi skickar en länk för att bekräfta din begäran om åtkomst till Tajo.

Skaffa Brevo

Den här sidan som Markdown: worker-died-sync-didnt.md

Läser in Markdown …
Öppna i Claude Öppna i ChatGPT