Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds
Powrót do kolekcji
Przewodnik12 min czytania

Temporal, procesy odporne na awarie

Temporal wykonuje procesy odpornie na awarie: proces wznawia się od miejsca przerwania. Przepływy, aktywności, determinizm, ceny i porównanie z kolejkami.

Temporal, czyli proces, który przeżyje restart serwera

Proces składający się z siedmiu kroków, gdzie każdy może zawieść, jest w zwykłym kodzie koszmarem. Trzeba zapisywać stan po każdym kroku, obsłużyć ponowienia, przewidzieć, co zrobić, gdy serwer padnie między krokiem czwartym a piątym, i pilnować, żeby ponowienie nie wykonało płatności dwa razy.

Temporal odwraca ten problem. Proces piszesz jako zwykłą funkcję z pętlami i warunkami, a platforma zapisuje historię wykonania i odtwarza ją po awarii. Funkcja przerwana w połowie wznawia się w tym samym miejscu, ze wszystkimi zmiennymi na swoich wartościach.

Na czym polega trwałe wykonanie

Kluczem jest sposób odtwarzania. Platforma nie zapisuje stanu zmiennych, tylko historię zdarzeń: co zostało wywołane i co zwróciło. Po awarii uruchamia funkcję od początku, ale zamiast wykonywać kroki ponownie, podstawia wyniki z historii, aż dojdzie do miejsca przerwania.

Ta konstrukcja ma jedną konsekwencję, którą trzeba zrozumieć, bo z niej wynikają wszystkie ograniczenia. Kod przepływu musi być deterministyczny: przy tych samych zdarzeniach musi wykonać te same kroki w tej samej kolejności. Inaczej odtworzenie rozjedzie się z historią.

W praktyce znaczy to, że w przepływie nie wolno odczytać bieżącego czasu, wylosować liczby ani wywołać zewnętrznego interfejsu bezpośrednio. Te rzeczy robisz w aktywnościach, czyli funkcjach wywoływanych z przepływu, których wyniki trafiają do historii.

Podział na przepływ i aktywność jest więc podstawowym pojęciem. Przepływ to logika: co po czym, pod jakim warunkiem, ile razy ponowić. Aktywność to działanie mające skutek na zewnątrz: wywołanie interfejsu, zapis do bazy, wysłanie wiadomości.

Pierwszy przepływ

Code
TypeScript
import { proxyActivities, sleep } from '@temporalio/workflow'
import type * as aktywnosci from './aktywnosci'

const { pobierzOplate, wyslijPotwierdzenie, zarezerwujTowar } =
  proxyActivities<typeof aktywnosci>({
    startToCloseTimeout: '1 minute',
    retry: { maximumAttempts: 5 }
  })

export async function realizujZamowienie(zamowienieId: string) {
  await zarezerwujTowar(zamowienieId)
  const platnosc = await pobierzOplate(zamowienieId)

  await sleep('2 hours')
  await wyslijPotwierdzenie(zamowienieId, platnosc.id)

  return { status: 'zrealizowane' }
}

Wywołanie usypiające na dwie godziny nie blokuje żadnego procesu. Platforma zapisuje moment wznowienia, zwalnia zasoby i uruchamia funkcję ponownie za dwie godziny, odtwarzając stan z historii. Ta sama konstrukcja obsługuje uśpienie na trzydzieści dni.

Konfiguracja ponowień siedzi przy deklaracji aktywności, więc nie pisze się pętli z odczekiwaniem. Aktywność, która zawiodła, zostanie powtórzona zgodnie z zasadą, a przepływ o tym nie wie.

To jest właśnie główna oszczędność. Kod wygląda jak opis procesu, a wszystko, co zwykle zajmuje trzy czwarte implementacji, dzieje się poza nim.

Kompensacja zamiast transakcji

Proces dotykający kilku systemów nie ma transakcji rozproszonej. Gdy krok trzeci zawiedzie, trzeba cofnąć skutki kroków pierwszego i drugiego, a to jest praca, którą trzeba zaprojektować.

Code
TypeScript
export async function realizujZamowienie(zamowienieId: string) {
  const cofnij: Array<() => Promise<void>> = []

  try {
    await zarezerwujTowar(zamowienieId)
    cofnij.push(() => zwolnijTowar(zamowienieId))

    const platnosc = await pobierzOplate(zamowienieId)
    cofnij.push(() => zwrocPlatnosc(platnosc.id))

    await nadajPrzesylke(zamowienieId)
  } catch (blad) {
    for (const krok of cofnij.reverse()) await krok()
    throw blad
  }
}

Ten wzorzec to jedyny sensowny sposób obsługi procesów obejmujących wiele systemów. Platforma nie robi tego za Ciebie, ale sprawia, że da się go napisać czytelnie, bo kod kompensacji leży obok kodu głównego, a nie w osobnym mechanizmie odzyskiwania.

Warto zaprojektować kroki tak, żeby dało się je powtórzyć bez skutków ubocznych. Aktywność wywołana dwa razy z tym samym identyfikatorem powinna wykonać operację raz, bo ponowienia są tu normą, a nie wyjątkiem.

Sygnały i zapytania

Proces trwający tygodniami musi przyjmować informacje z zewnątrz i odpowiadać na pytania o swój stan.

Code
TypeScript
import { defineSignal, defineQuery, setHandler, condition } from '@temporalio/workflow'

const zatwierdz = defineSignal<[string]>('zatwierdz')
const stan = defineQuery<string>('stan')

export async function procesAkceptacji(wniosekId: string) {
  let decyzja: string | null = null
  setHandler(zatwierdz, (kto) => { decyzja = kto })
  setHandler(stan, () => decyzja ? 'zatwierdzony' : 'oczekuje')

  const doczekano = await condition(() => decyzja !== null, '7 days')
  if (!doczekano) return { status: 'wygasl' }

  await wykonajWniosek(wniosekId)
  return { status: 'wykonany', zatwierdzil: decyzja }
}

Sygnał wnosi informację do trwającego procesu, a zapytanie odczytuje stan bez wpływania na niego. To mechanizm, który zastępuje odpytywanie bazy w pętli i pozwala budować procesy z krokiem czekającym na decyzję człowieka.

Ten sam wzorzec obsługuje agentów z modelem językowym, gdzie działanie nieodwracalne wymaga zatwierdzenia. Proces zatrzymuje się, człowiek zatwierdza, proces idzie dalej, a wszystko to bez utrzymywania stanu w bazie po Twojej stronie.

Wersjonowanie przepływów

To zagadnienie, które przy pierwszym wdrożeniu wygląda na teoretyczne, a przy trzecim staje się najważniejsze.

Problem wynika wprost z odtwarzania. Proces uruchomiony tydzień temu, wznawiany dziś, odtworzy się na kodzie, który jest w repozytorium teraz. Jeśli w międzyczasie ktoś dodał krok w środku, odtworzenie napotka w historii zdarzenia, których nowy kod się nie spodziewa.

Code
TypeScript
import { patched } from '@temporalio/workflow'

export async function realizujZamowienie(zamowienieId: string) {
  await zarezerwujTowar(zamowienieId)

  if (patched('weryfikacja-adresu')) {
    await sprawdzAdres(zamowienieId)
  }

  await pobierzOplate(zamowienieId)
}

Znacznik pozwala nowym procesom wykonać dodatkowy krok, a starym go pominąć, więc obie wersje odtwarzają się poprawnie. Po zakończeniu wszystkich starych procesów znacznik da się usunąć.

Alternatywą jest wersjonowanie po nazwie: nowa wersja przepływu dostaje nową nazwę, a stara pracuje, aż wygasną uruchomione procesy. To prostsze przy dużych zmianach, kosztem dwóch wersji kodu w repozytorium przez pewien czas.

Praktyczna zasada: przy procesach trwających godziny zmiany są mało ryzykowne, przy procesach trwających tygodnie każda zmiana wymaga decyzji, jak obsłużyć to, co już biegnie. Warto tę różnicę uwzględnić przy projektowaniu, bo proces trzydziestodniowy oznacza trzydzieści dni zgodności wstecznej.

Widoczność i diagnozowanie

Jedną z mniej oczywistych zalet jest to, że stan każdego procesu jest widoczny bez dopisywania własnego logowania.

Interfejs pokazuje historię wykonania: który krok się wykonał, co zwrócił, ile razy był ponawiany i gdzie proces stoi teraz. Przy zgłoszeniu, że zamówienie utknęło, odpowiedź na pytanie „na czym stoi" zajmuje kilkanaście sekund zamiast przeszukiwania logów.

Drugą rzeczą jest możliwość ręcznej ingerencji. Proces da się zatrzymać, zakończyć, wznowić od wybranego zdarzenia albo wysłać mu sygnał. To bywa jedynym rozsądnym wyjściem, gdy zewnętrzna usługa zwróciła coś, czego nikt nie przewidział.

Trzecią jest to, że historia jest kompletna z definicji. Nie ma sytuacji, w której zapomniano zalogować krok, bo zapis powstaje automatycznie i to on jest podstawą działania, a nie dodatkiem do niego. Przy procesach dotyczących pieniędzy ta właściwość bywa argumentem samym w sobie, bo daje ślad audytowy bez osobnej pracy.

Warto natomiast pamiętać o retencji. Historia zajmuje miejsce i jest rozliczana, więc przy dużym wolumenie trzeba ustalić, jak długo procesy zakończone mają być przechowywane. W wersji zarządzanej okres przechowywania sięga dziewięćdziesięciu dni, a dłuższe trzymanie wymaga wyeksportowania historii poza platformę, przy czym sam eksport również generuje działania i wchodzi na rachunek.

Ceny i wdrożenie

WariantKosztCo obejmuje cena
Wersja otwarta u siebie0 USDSerwer, baza i utrzymanie po Twojej stronie
Cloud Essentialsod 100 USD miesięcznie1 mln działań, 1 GB pamięci aktywnej, 40 GB archiwalnej
Cloud Businessod 500 USD miesięcznie2,5 mln działań, 2,5 GB i 100 GB, zniżki za zobowiązanie
Cloud Enterprise i Mission Criticalwycena indywidualna, umowa rocznaPo 10 mln działań, 10 GB i 400 GB, mocniejsze gwarancje wsparcia

Rozliczenie w wersji zarządzanej łączy opłatę planu z zużyciem liczonym w działaniach. Działanie to konkretna operacja rozliczana przez usługę: uruchomienie przepływu, wykonanie albo ponowienie aktywności, uruchomienie licznika czasu, sygnał, aktualizacja oraz zapytanie. To nie jest to samo co każde zdarzenie w historii, i ta różnica działa na Twoją korzyść: odtworzenie przepływu po awarii nie generuje działań w ogóle, a wszystkie aktywności lokalne w obrębie jednego zadania przepływu liczą się jako jedno. Przepływ z dziesięcioma aktywnościami i pięcioma ponowieniami generuje ich mimo to więcej, niż wynikałoby z liczby kroków, bo każde ponowienie jest osobnym działaniem.

Sama opłata planu też nie jest zwykłym abonamentem doliczanym do zużycia, i tu najłatwiej pomylić się w budżecie. Płacisz większą z dwóch kwot: minimum planu albo procent od rachunku za zużycie, pięć procent na Essentials i dziesięć na Business. Wliczoną pulę działań i miejsca dostajesz w tej kwocie, a osobno płacisz dopiero za nadwyżkę, po 50 dolarów za pierwszy dodatkowy milion działań, z rabatami schodzącymi do 25 dolarów przy dużych wolumenach.

Wersja otwarta jest darmowa, ale wymaga serwera, bazy danych i utrzymania. Przy jednym zespole ten koszt zwykle przewyższa abonament, przy większej organizacji proporcja się odwraca. Ostatnie zmiany dołożyły też możliwość uruchamiania procesów wykonawczych w środowisku bezserwerowym, co zmienia rachunek przy obciążeniu nierównomiernym. Warto jednak sprawdzić etap: wariant dla funkcji AWS Lambda jest w publicznym podglądzie od sierpnia 2026 roku, a wariant dla Cloud Run od Google w jeszcze wcześniejszej fazie przedpremierowej.

Warto sprawdzić aktualne progi przed budżetowaniem, bo struktura planów bywa zmieniana.

Temporal kontra alternatywy

RozwiązanieMocna stronaSłabośćKiedy wybrać
TemporalTrwałe wykonanie, procesy tygodniowe, widoczność stanuWymóg determinizmu, koszt wdrożeniaProcesy wielokrokowe dotykające pieniędzy
Kolejka zadańProsta, znana, taniaStan i ponowienia piszesz samPojedyncze zadania w tle
n8n albo MakeWizualne, szybkie wdrożenieMniej kontroli, słabsze przy złożonej logiceAutomatyzacje łączące usługi
Własny mechanizm stanuPełna kontrolaUtrzymujesz to, co platforma daje gotoweProsty proces o dwóch krokach
AirflowHarmonogram, graf zależności, dojrzałe integracjePotoki cykliczne, a nie procesy na żądaniePrzetwarzanie danych o ustalonych porach

Wybór między pierwszymi dwoma wierszami zależy od tego, ile kroków ma proces i co się stanie, gdy zawiedzie w połowie. Wysłanie wiadomości w tle to zadanie dla kolejki. Proces obejmujący rezerwację, płatność, wysyłkę i powiadomienie, gdzie każdy krok może zawieść, a częściowe wykonanie oznacza problem z pieniędzmi, to sytuacja, w której platforma zwraca się szybko.

Trzeci wiersz to inna kategoria. Narzędzia automatyzacji sprawdzają się przy łączeniu gotowych usług, a nie przy procesach z rozbudowaną logiką i wymogiem dokładnie jednokrotnego wykonania.

Warto też uczciwie policzyć koszt wejścia. Platforma wprowadza pojęcia, których zespół musi się nauczyć: podział na przepływy i aktywności, determinizm, wersjonowanie, procesy wykonawcze. To tydzień do dwóch, zanim pierwszy proces trafi na produkcję, i kolejny miesiąc, zanim zespół przestanie się potykać o determinizm.

Ten koszt zwraca się przy trzecim procesie, a nie przy pierwszym. Jeśli w planach jest jeden przepływ i nic więcej, prostsze rozwiązanie zwykle wystarczy. Jeśli zaplecze będzie miało kilkanaście procesów dotykających płatności, zamówień i integracji zewnętrznych, wspólna platforma oszczędza pisania tego samego mechanizmu kilkanaście razy.

Ostatni wiersz trafił do tabeli nie dlatego, że konkuruje, tylko dlatego, że bywa mylony. Orkiestrator potoków danych planuje powtarzalne przetwarzanie o ustalonych porach albo po zdarzeniu i pokazuje, co przeszło. Trwałe wykonanie obsługuje proces uruchamiany raz na zamówienie, który potrafi czekać tydzień na płatność. Nakładanie się jest niewielkie, a pomyłka w kategorii kosztuje przepisanie całości, bo obejście jednego narzędzia drugim daje się zrobić i nigdy nie daje się utrzymać.

Zastosowanie przy agentach

Zadania agentowe okazały się jednym z naturalniejszych zastosowań tej platformy i warto rozumieć dlaczego.

Agent wykonujący dziesięć wywołań modelu i narzędzi jest procesem wielokrokowym, w którym każdy krok może zawieść, a powtórzenie całości od początku kosztuje tokeny. Wznowienie od siódmego kroku zamiast od pierwszego to oszczędność wprost proporcjonalna do tego, jak daleko proces zaszedł.

Druga rzecz to czas. Zadanie agentowe potrafi trwać kwadrans, a przy narzędziach czekających na odpowiedź człowieka nawet dni. Utrzymywanie tego w pamięci procesu serwera jest kruche, a w tym modelu naturalne.

Code
Python
@workflow.defn
class AgentBadawczy:
    @workflow.run
    async def run(self, pytanie: str) -> str:
        plan = await workflow.execute_activity(
            zaplanuj_kroki, pytanie, start_to_close_timeout=timedelta(minutes=2)
        )

        wyniki = []
        for krok in plan.kroki:
            wyniki.append(await workflow.execute_activity(
                wykonaj_krok, krok, start_to_close_timeout=timedelta(minutes=5)
            ))

        return await workflow.execute_activity(zloz_odpowiedz, wyniki)

Wywołania modelu umieszczasz w aktywnościach, bo są niedeterministyczne. Pętla i warunki zostają w przepływie, więc logika procesu jest czytelna, a odporność przychodzi za darmo.

Biblioteki agentowe pokroju PydanticAI i LangGraph mają gotowe integracje z tą platformą, więc nie trzeba pisać tego opakowania samodzielnie. Warto to sprawdzić przed budowaniem własnego rozwiązania.

Typowe błędy

Pierwszy to niedeterministyczny kod w przepływie. Odczyt bieżącego czasu, losowanie albo wywołanie interfejsu bezpośrednio z przepływu psuje odtwarzanie, a błąd ujawnia się dopiero po awarii, czyli w najgorszym momencie.

Drugi to aktywności bez zabezpieczenia przed powtórzeniem. Ponowienia są tu normalne, więc aktywność pobierająca opłatę bez klucza idempotencji obciąży klienta dwa razy.

Trzeci to zmiana kodu przepływu przy działających procesach. Proces trwający tydzień odtworzy się na nowej wersji kodu, więc zmiana kolejności kroków rozjedzie się z historią. Do tego służy wersjonowanie przepływów i trzeba je stosować od początku, a nie po pierwszym incydencie.

Czwarty to zbyt drobne aktywności. Każda generuje zdarzenia w historii, więc przepływ z tysiącem drobnych wywołań kosztuje więcej i wolniej się odtwarza niż przepływ z dziesięcioma sensownymi.

Piąty to brak limitów czasu. Aktywność bez ograniczenia potrafi wisieć w nieskończoność, blokując proces i nie zgłaszając niczego.

Szósty to sięganie po platformę do zadań, które są zwykłą kolejką. Koszt wdrożenia i utrzymania ma sens przy procesach złożonych, a przy jednym kroku w tle jest nieproporcjonalny.

FAQ

Do czego dokładnie służy Temporal?

Do wykonywania procesów wielokrokowych tak, żeby przeżyły awarie: restart serwera, błąd sieci, niedostępność zewnętrznej usługi. Proces przerwany wznawia się od miejsca zatrzymania ze stanem zmiennych, więc nie trzeba zapisywać go ręcznie po każdym kroku.

Czy Temporal jest darmowy?

Wersja otwarta tak i uruchamiasz ją u siebie, płacąc kosztem serwera, bazy i utrzymania. Wersja zarządzana zaczyna się od stu dolarów miesięcznie na planie Essentials, z wliczonym milionem działań i pulą miejsca, a dopłacasz dopiero za nadwyżkę. Nowe konto dostaje tysiąc dolarów w kredytach ważnych dziewięćdziesiąt dni.

Dlaczego przepływ musi być deterministyczny?

Bo odtwarzanie po awarii polega na ponownym uruchomieniu funkcji z podstawianiem wyników z historii. Kod, który przy tych samych zdarzeniach wykonałby inne kroki, rozjedzie się z zapisem. Rzeczy niedeterministyczne, w tym czas i wywołania zewnętrzne, umieszczasz w aktywnościach.

Czy nadaje się do agentów z modelem językowym?

Tak i to jedno z rosnących zastosowań. Zadanie agentowe składa się z wielu wywołań, każde może zawieść, a powtarzanie całości od zera kosztuje tokeny. Wznowienie od miejsca przerwania oszczędza pieniądze, a mechanizm sygnałów obsługuje zatrzymanie na decyzję człowieka przed działaniem nieodwracalnym.

Jakie języki są obsługiwane?

TypeScript, Go, Java, Python, PHP, .NET, Ruby oraz Rust, ten ostatni w publicznym podglądzie, z tym samym modelem pojęciowym w każdym. Procesy napisane w różnych językach mogą się wzajemnie wywoływać, co bywa przydatne przy zapleczu złożonym z kilku usług.

Dokumentacja stoi na stronie projektu, a szczegóły rozliczenia opisuje strona cennika chmury.