På denne siden
I et foredrag som AI Engineer publiserte 3. oktober 2026, startet en developer advocate fra Crusoe en PyTorch-treningsjobb på to A100-noder. Så fortalte foredragsholderen demoens eget overvåkingssystem at én GPU hadde kastet en XID 79-feil. Noden ble merket som nede, og jobben ble avbrutt. Prosessen fikk en SIGTERM og «opptil to minutter på å håndtere den SIGTERM-en og enten lagre checkpointene dine eller skrive ut loggene dine». En reservenode tok over, og «slurm-jobben starter på nytt. Den laster inn checkpointen og fortsetter treningen der den slapp». Litt under femten minutter fra start til slutt. Ingen ble varslet.
En synkronisering er et mye mindre dyr enn en treningskjøring. Spørsmålet er det samme. Hva trenger en kjøring for at det skal koste minutter og ingen duplikater når workeren blir drept?
Dette er svaret for Tajos egen synkroniseringsrunner, lest ut av koden og ikke ut av en brosjyre. Reverse ETL beskriver formen på jobben. Dette er hva som skjer når jobben dør.
Følg én kontakt
En kjøring leser kilden én side om gangen, fra en lagret cursor. Kall sidens cursor c3 og kontakten vår den 212. posten på den siden. Tre ting gjelder for den kontakten før noe blir skrevet.
For det første har den en leveringsnøkkel. Nøkkelen er en hash av synkroniseringens id, kilden, sidens cursor, posisjonen på siden og selve posten. Cursoren og posten går inn i hashen og bygges aldri inn i nøkkelen, så nøkkelen inneholder ingen e-postadresse.
For det andre går skrivingen gjennom en ledger og ikke rett til destinasjonen. Før destinasjonen kalles, reserveres nøkkelen som ventende. Når skrivingen lykkes, merkes raden som fullført og tas vare på i sju dager.
For det tredje har cursoren ikke flyttet seg. Den flytter seg bare på slutten av en batch, bare hvis hver post i den batchen har landet, og bare etter at kjøringen har bevist at den fortsatt eier synkroniseringen.
Drep nå workeren mellom skrivingen og checkpointen.
Ved en SIGTERM avslutter hver køworker arbeidet sitt innenfor én felles frist på femten sekunder, og prosessen avslutter etter tjuefem sekunder uansett hva som fortsatt kjører; systemd gir den førtifem før den dreper den. En kjøring som ikke ble ferdig innenfor det vinduet, er fortsatt merket som pågående, og neste worker plukker den opp gjennom køens gjenoppretting av jobber som har hengt seg. Ved et hardt kill blir ingenting avsluttet i det hele tatt. Uansett gjelder de samme to faktaene. Destinasjonen har kontakten vår. Cursoren vår står fortsatt på c3.
Gjentakelsen
Neste kjøring starter på c3, leser den samme siden, avleder den samme nøkkelen for den 212. posten og spør ledgeren. Ledgeren sier fullført. Destinasjonen kalles ikke; det lagrede resultatet spilles av, og kjøringen går videre. På bulkveien, der Brevos import-endepunkt tar imot en hel side i ett kall, kjøres den samme sjekken per nøkkel før forespørselen bygges. En side der hver nøkkel allerede er fullført, sender ingenting.
Det er hele trikset, og det er verdt å si hvorfor det virker. Nøkkelen avledes av sidens cursor ved starten av batchen, ikke av kjøringen som tilfeldigvis var i gang. Et krasj, et nytt forsøk seksti sekunder senere og en planlagt kjøring i morgen avleder alle den samme nøkkelen for den samme posten. Ledgeren bryr seg ikke om hvem av dem som spør.
Så kostnaden. Ledgeren glemmer etter sju dager. Kodens egen kommentar sier at sju dager dekker et forsinket nytt forsøk og et kort avbrudd, og mer enn det hevder den ikke. En gjentakelse fra en cursor som er eldre enn det, skriver på nytt. Og en kjøring som blir avbrutt hardt, kan bli stående og vente. Den døde kjøringens lease holder i opptil femten minutter, og på en planlagt synkronisering blir en kjøring med ledig lease ryddet bort og planlagt på nytt først når den startet for over en time siden. Crusoes demo er tilbake på under femten minutter. Vår er ikke alltid det.
Hvem eier synkroniseringen mens den starter på nytt
Én kjøring eier en synkronisering om gangen. Kjøringen tar en lease i Redis med sitt eget token, en levetid på femten minutter og en fornyelse hvert femte minutt. Før hver checkpoint bekrefter den at den fortsatt har den leasen. Hvis ikke, stopper den. Kjøringen registreres med den enkle setningen som personen kommer til å lese: «The run lost its slot part-way through and stopped to avoid writing twice.» (Kjøringen mistet plassen sin underveis og stoppet for å unngå å skrive to ganger.) En ny kjøring som kolliderer med en aktiv eier, prøves ikke på nytt. Den avsluttes med «Another run of this sync was already in progress» (En annen kjøring av denne synkroniseringen pågikk allerede), og neste planlagte kjøring starter som normalt.
Hvem som eier rettingen, er også skrevet ned. Hver feilkode peker til én setning om hva som skjedde, én om hva som skal gjøres videre, og en eier: deg eller Tajo. En destinasjon som avviste hver post fordi mappingen er feil, er din. En tapt lease er vår. Kjøringens side viser neste steg; veiledningen for gjenoppretting av synkronisering viser hvilken.
Så langt er dette den enkle halvdelen. Et krasj koster én gjentakelse, og ingenting lander to ganger. To hendelser lærte oss den andre halvdelen.
Feil etter feil
Én synkronisering feilet igjen og igjen. Ikke i en tett løkke, men én gang per planleggerintervall, kjøring etter kjøring, med den samme meldingen hver gang: at en egendefinert id ikke kan inneholde et kolon. Kjøringen nådde aldri destinasjonen. Den døde mens den ble lagt i kø, før noen runner så den, så et varsel som var knyttet til runneren, aldri hadde noe å reagere på.
Ingenting fulgte med på feilrekken. Det er hele funnet.
Løsningen er en breaker. Etter hver registrerte feil leser den synkroniseringens fem siste avsluttede kjøringer siden synkroniseringen sist ble endret. Hvis alle fem er feil, setter den synkroniseringen på pause, skriver én revisjonsrad med årsaken circuit_breaker_tripped og utløser ett kritisk varsel. Feilrekken avledes av de lagrede kjøringene, ikke av en teller i workeren. En feil teller uansett hvilken vei den ble registrert, også køfeilen som forårsaket hendelsen. Ifølge kodens kommentar ville den ha stoppet den hendelsen ved kjøring fem. Én vellykket kjøring nullstiller rekken. Når pausen oppheves, aktiveres breakeren igjen i stedet for å utløses på nytt av de gamle radene. Veiledningen for Shopify-connectoren sier det samme på én linje: etter fem mislykkede kjøringer på rad setter synkroniseringen seg selv på pause, og siden dens viser den siste feilen.
Det breakeren ikke gjør, er å reparere noe. Crusoe bytter ut noden og starter jobben på nytt uten å spørre. Tajo stopper og spør: en synkronisering på pause blir stående på pause til en person retter årsaken og publiserer den på nytt. Den forsiktige standarden er bevisst: et automatisk nytt forsøk på en synkronisering som skriver til kundeposter, er i seg selv en skriving.
Stillheten ingen så
Innvendingen er rimelig, og den lyder slik: «En breaker som setter på pause etter fem feil, ville ha fanget den andre hendelsen også. Hvorfor bygge en vakt til?» Fordi den andre hendelsen ikke så ut som en feil. I lang tid hadde et arbeidsområde en aktiv synkronisering, en voksende haug med feilrader og ikke én eneste vellykket kjøring, noensinne, og ingenting varslet. Alle signalene som fantes, var knyttet til aktivitet: en kjøring feilet, en webhook kom inn, et budsjett ble overskredet. Null vellykkede kjøringer er ikke en hendelse. Det er fraværet av en, og en breaker kjører bare når en feil blir registrert. En planlegger som er død, en kø som har låst seg, en worker som aldri starter opp: de registrerer ingenting, og en breaker våkner aldri.
Et lokomotivs dødmannsknapp virker motsatt. Føreren må fortsette å trykke på den, og stillhet stopper toget. Synkroniseringsversjonen kjører én gang i timen, tretten minutter over hel. For hvert arbeidsområde med en aktiv synkronisering stiller den ett spørsmål: har noen kjøring lyktes de siste tjuefire timene? Et arbeidsområde der den eldste aktive synkroniseringen ble slått på for mindre enn et døgn siden, får være i fred; det har ikke hatt et døgn på å lykkes ennå. Et som er eldre og stille, får ett kritisk varsel. Varselet sier hvilken stillhet det er: kjøringer registrert, men ingen som lykkes, eller ingen kjøringer registrert i det hele tatt. Det utløses én gang per hendelse. En markør i Redis holder det lukket til den første vellykkede kjøringen og fjernes deretter, slik at neste hendelse varsler igjen.
Begge vaktene kom i én commit, i etterkant. Breakerens kommentar sier at den ville ha stoppet den første hendelsen ved kjøring fem. Dødmannsvarselets kommentar sier at en synkronisering som ble slått på for lenge siden og har null kjøringer, må varsle. Ingen av dem fantes da de trengtes. Vi skrev dem etterpå, med begge hendelsene notert i filhodet så ingen glemmer hvorfor.
Hva som er bygget, og hva som ikke er det
Bygget når dette skrives:
- Cursoren lagres på slutten av hver feilfri batch, og igjen ved et krasj.
- Hver posts leveringsnøkkel avledes av sidens cursor og postens posisjon og sjekkes mot en ledger på sju dager før hvert kall til destinasjonen.
- Én lease per synkronisering, med en levetid på femten minutter.
- Fem forsøk per kjøring, med backoff fra seksti sekunder.
- En breaker som setter synkroniseringen på pause etter fem feil på rad og registrerer hvorfor.
- Et varsel hver time ved tjuefire timer uten en vellykket kjøring.
Ikke bygget, og verdt å vite før du stoler på det:
- Dødmannsvarselet overvåker arbeidsområdet, ikke synkroniseringen. Én frisk synkronisering skjuler et stille søsken i samme arbeidsområde.
- Varselet når Tajos driftspersonell: alltid en logglinje, og Sentry, en webhook eller en e-post der det er konfigurert. Det vises ikke på synkroniseringens side. Det gjør breakerens pause.
- Ingenting starter en synkronisering på pause på nytt. Det gjør en person.
- Gjenoppretting etter et hardt kill går ikke raskt. Leasen kan holde i femten minutter, og oppryddingen venter en time.
- Begge tersklene, fem feil og tjuefire timer, er innstillinger for driftsmiljøet. Du kan ikke sette dem per synkronisering.
En lang kjede av nesten perfekte steg feiler likevel; veien ut er å definere sluttilstanden og sjekke for den. Denne artikkelen er den operative halvdelen av det. Definer hva en død kjøring etterlater seg, og sjekk for det ingenting som stillhet etterlater seg.
Crusoes avsluttende linje var at «feil er uunngåelige». Vår går ett steg videre. Krasjet var den enkle halvdelen, fordi det etterlot rader. Stillheten var halvdelen vi måtte lete etter.
Ofte Stilte Spørsmål
Hva skjer med en kjørende synkronisering når workeren dør midt i en batch?
Cursoren peker fortsatt på starten av den avbrutte batchen, fordi Tajo flytter den først når en batch har landet feilfritt. Neste kjøring leser den samme siden på nytt. Hver post har en leveringsnøkkel som er avledet av sidens cursor og postens posisjon, og en ledger tar vare på hver fullførte skriving i sju dager. Poster som allerede er skrevet, spilles derfor av fra ledgeren i stedet for å bli sendt til destinasjonen en gang til.
Hvorfor skriver ikke en gjentatt synkronisering den samme kontakten to ganger?
Før hvert kall til destinasjonen sjekker kjøringen postens leveringsnøkkel mot en ledger. Nøkkelen er en hash av synkroniseringen, kilden, sidens cursor, posisjonen på siden og selve posten. Et krasj, et nytt forsøk et minutt senere og en planlagt kjøring dagen etter avleder dermed den samme nøkkelen for den samme posten. En nøkkel som allerede er merket som fullført innenfor sjudagersvinduet, sendes ikke på nytt.
Hva er et dødmannsvarsel for en synkronisering?
En sjekk som utløses av fravær i stedet for av feil. Én gang i timen ser Tajo på hvert arbeidsområde med en aktiv synkronisering og varsler når ingen kjøring har lyktes de siste tjuefire timene, også når ingen kjøring er registrert i det hele tatt. Varselet utløses én gang per hendelse og aktiveres igjen etter den første vellykkede kjøringen. Det finnes fordi et feilvarsel bare kan se feil som er blitt registrert.