Blog · Infraestrutura · 17 min de leitura

Temporal: Hands-On com Agendamento de Workflows Self-Hosted

Por Fabio Douek

Ir para seção
Explica (TLDR) como se eu fosse...
O que é isso?

Imagine um relogio com lembretinhos pregados nele. Quando o relogio bate nove horas, ele avisa o seu robo ajudante para ir ver se o escorregador do parquinho ainda esta de pe. Se o escorregador estiver quebrado, o robo escreve um bilhete num caderninho para os adultos consertarem depois.

O legal e que o relogio guarda os lembretinhos, nao o robo. Entao se o robo tropeca e cai, o proximo robo pega o mesmo bilhete e termina a tarefa. E por isso que as pessoas escolhem esse relogio em vez do mais simples da cozinha: as tarefas nunca se perdem, mesmo quando algo da errado no meio do caminho.

Trate o Temporal como infraestrutura duravel para tarefas recorrentes. O proprio schedule e um registro do lado do servidor, separado do worker process que executa o trabalho. A responsabilidade muda de "o cron disparou" para "o workflow foi concluido", porque a plataforma rastreia toda tentativa, retry e resultado com trilha de auditoria.

A due diligence em um deployment self-hosted deve cobrir a escolha de persistencia (dev server in-memory versus Postgres), o catchup window padrao de um ano (que pode reexecutar runs perdidos apos longos periodos offline), e o fato de que o binario de dev e explicito sobre nao ser para producao. A licenca Apache 2.0 e permissiva, mas a pegada operacional e relevante e merece dimensionamento.

Pense no Temporal como um tratamento para uma condicao cronica comum: jobs agendados que falham silenciosamente porque o host travou no meio da execucao. O mecanismo e durable execution. Cada passo de um workflow agendado e registrado, entao um worker que falhou pode ser substituido e o trabalho retoma do ultimo passo concluido em vez de recomecar.

Efeitos colaterais a observar incluem overhead operacional de rodar um servidor mais um banco de dados, uma curva de aprendizado em torno das regras de determinismo no codigo do workflow, e um catchup window padrao de um ano que pode causar estouro apos longas indisponibilidades. Bons candidatos sao times que ja operam Postgres com tranquilidade; menos adequado para um cron job pontual em uma unica maquina.

Note o que muda quando o engenheiro de plantao deixa de ser paginado as 3 da manha porque um cron job morreu no meio. O trabalho se recupera sozinho, a tabela de incidentes conta a historia sem precisar de paginacao manual, e a manha de segunda comeca com evidencia em vez de trabalho de detetive.

O novo atrito e mais silencioso e merece ser nomeado. O time troca a legibilidade de um crontab de uma linha por um servidor, uma UI e uma curva de aprendizado. Alguns engenheiros sentem a perda do "consigo ler isso em cinco segundos". O trabalho de adocao e em parte tecnico e em parte sobre dar tempo ao time para confiar no novo formato do schedule antes de depender dele.

Trate Temporal Schedules como um metronomo que mantem o tempo mesmo quando a banda sai do palco. O intervalo bate a cada dois minutos, o calendario dispara as 09:00 nos dias uteis, e o jitter espalha as entradas para que dois musicos nao comecem o mesmo compasso no mesmo instante.

Onde ele se prova util e na recuperacao: se um musico cai no meio de uma frase, o metronomo lembra exatamente onde a parte estava, e o substituto retoma na batida certa. Overlap policies sao os sinais de mao do maestro: pular a proxima entrada, enfileirar uma, cancelar a anterior, ou deixar todos tocarem juntos.

A historia e o upgrade de "o cron disparou" para "o trabalho foi concluido". Faca self-host num laptop em menos de cinco minutos com um unico install via Homebrew, entregue um workflow Python recorrente com intervalos, cron strings, jitter e overlap policies, e evolua para uma stack com Postgres quando o time estiver pronto.

O posicionamento se encaixa em tres audiencias. Times queimados pelo peso operacional do Airflow, times que superaram as responsabilidades de overlap-locking do Celery Beat, e times rodando cron num laptop e se perguntando por que o job parou silenciosamente na terca passada. O antes-e-depois se escreve sozinho: um run perdido, depois um run-retomado-do-passo-quatro, com a trilha de auditoria anexada.

Temporal: Hands-On com Agendamento de Workflows Self-Hosted
GitHub Veja o codigo fonte aqui: temporal-scheduling-quickstart

Visao Geral

Eu venho escrevendo cron jobs ha muitos anos e me virando bem. A maioria dos cron jobs e curta, idempotente, e o host e confiavel o suficiente para que “dispara a cada cinco minutos, funciona na maior parte das vezes” seja aceitavel. A historia desmorona no momento em que um job leva mais de um minuto ou conversa com um servico externo instavel. Quando voce precisa de um state file e logica de retry, ja esta dentro do espaco de problema de uma workflow engine.

Temporal e uma das varias workflow engines que valem o seu tempo, e a que estou cobrindo hoje. Outras virao em posts futuros. O argumento principal do Temporal e durable execution: voce escreve codigo, a plataforma registra cada passo, e se o seu worker process travar, o trabalho retoma de onde parou em vez de recomecar. Esse e o caso de uso de destaque, e a maior parte do conteudo sobre Temporal e sobre durabilidade de workflows.

Este post limita deliberadamente o escopo a agendamento. O Temporal expoe um objeto Schedule de primeira classe com intervalos, calendar specs, cron strings, timezone, jitter, overlap policy, backfill, pause e trigger ad-hoc, nada disso voce precisa amarrar manualmente. Durable execution ainda se aplica por baixo (cada run agendado e um workflow), mas o foco aqui e na superficie de agendamento.

Arquitetura

Antes de qualquer codigo, a topologia. O Temporal nao e uma library que voce importa no seu worker; e um servidor com o qual voce conversa via gRPC. O schedule vive no servidor. Seu codigo vive em um worker process que faz polling de uma task queue.

Arquitetura do Temporal: servidor com servicos de frontend, history, matching e internal worker, mais um Schedule do lado do servidor, conversando com um worker Python via gRPC na porta 7233. Persistencia e SQLite no modo start-dev ou Postgres no modo compose; Web UI na porta 8233 em dev ou 8080 em compose.

Como um run agendado flui pelo diagrama:

  1. O schedule dispara do lado do servidor. E um registro no Temporal Server, nao um while True: sleep no seu codigo. Ele bate independentemente do seu worker estar vivo ou nao.
  2. O servidor inicia um Workflow Execution e enfileira a primeira task na task queue nomeada (neste post, health-check-queue).
  3. Seu worker Python faz polling daquela task queue via gRPC na :7233, pega a task, e executa o workflow e as activities que voce definiu.
  4. Cada passo e registrado na persistencia (SQLite para temporal server start-dev, Postgres na stack docker-compose). Mate o worker no meio do run, suba um novo, e o trabalho retoma do ultimo passo concluido.
  5. A Web UI le do servidor: :8233 em dev, :8080 em compose. Nada no seu codigo Python muda entre os dois.
⚠️ A Web UI do Temporal Server roda em uma porta diferente entre temporal server start-dev e a stack docker-compose. As duas stacks sao drop-in replacements em todo o resto, mas a URL da UI muda. Eu ja perdi uma quantidade vergonhosa de tempo com isso.

Setup

Estou rodando macOS em Apple Silicon. O post foi escrito para macOS, mas tudo funciona em Linux ou Windows com pequenas trocas de comando: principalmente as linhas brew install, que viram apt/dnf/pacman no Linux ou o script instalador da Temporal CLI no Windows. O codigo Python, os targets do Makefile e a stack docker-compose sao identicos entre plataformas.

Passo 1: Instalar a Temporal CLI e iniciar o dev server

A CLI e um install do Homebrew. O dev server e o mesmo binario, em um modo que sobe uma stack Temporal completa com a Web UI ja embutida:

brew install temporal
temporal server start-dev \
  --db-filename ./temporal.db \
  --ui-port 8233

Apos o Passo 2 (clone), make dev roda o mesmo comando de dentro do diretorio do projeto.

Essa e a instalacao local inteira. --db-filename escreve o estado em um arquivo SQLite para que ele sobreviva a restarts; sem isso, voce fica com um banco in-memory e seus schedules somem no Ctrl-C. O endpoint gRPC do frontend escuta em localhost:7233, a Web UI em http://localhost:8233, e o namespace default ja e criado para voce.

Verifique com a mesma CLI:

temporal operator namespace list
# default
temporal workflow list
# (empty)

Passo 2: Clonar o repositorio companheiro

O repositorio companheiro do my2cents.ai contem o projeto completo. Clone, entre no diretorio do projeto e instale as dependencias:

# 1. Clone the samples repository
git clone https://github.com/fabiodouek/my2centsai-blog-samples.git

# 2. Change into the project directory
cd my2centsai-blog-samples/temporal-scheduling-quickstart

# 3. Install uv if you do not already have it
brew install uv

# 4. Install Python 3.12 and the project's pinned dependencies
uv python install 3.12
uv sync

O que voce vai encontrar dentro do projeto:

temporal-scheduling-quickstart/
├── pyproject.toml          # Python 3.12, three deps, hatchling build
├── Makefile                # one-command dev / worker / schedule / clean
├── docker-compose.yml      # Postgres-backed stack for the second track
├── README.md
├── .env.example            # TEMPORAL_ADDRESS, TASK_QUEUE, DB_PATH
└── src/
    ├── config.py           # env-driven config
    ├── targets.py          # the httpbin.org targets to probe
    ├── storage.py          # SQLite schema and insert helper
    ├── activities.py       # probe_endpoint + record_incident
    ├── workflows.py        # HealthCheckWorkflow
    ├── worker.py           # registers workflow + activities
    └── schedules/
        ├── create_interval.py   # every 2 min, jitter, overlap=SKIP
        ├── create_cron.py       # weekday 09:00 America/New_York
        ├── pause_trigger.py     # pause / unpause / trigger
        ├── backfill.py          # backfill the last 30 minutes
        ├── list_describe.py     # list / describe schedules
        └── delete_all.py        # drop both schedules
Se voce so quer ver rodando, o Makefile encapsula tudo: make dev em um terminal (sobe o Temporal dev server), make worker em outro (roda o worker Python), make schedule em um terceiro (cria o schedule de 2 em 2 minutos). Depois abra http://localhost:8233. O passo a passo abaixo explica o que cada arquivo faz.

Passo 3: Percorrer o codigo

O workflow sonda uma lista de targets HTTP. Cada sondagem e uma activity (entao retries e timeouts sao a nivel de framework), e uma sondagem que falha registra uma linha de incidente no SQLite (tambem via activity, porque codigo de workflow nao pode fazer I/O diretamente). Os quatro arquivos que importam ficam em src/.

Abra src/targets.py, a lista a sondar:

from dataclasses import dataclass


@dataclass
class Target:
    name: str
    url: str
    expected_status: int
    timeout_seconds: float = 5.0


TARGETS: list[Target] = [
    Target(
        name="httpbin-ok",
        url="https://httpbin.org/status/200",
        expected_status=200,
    ),
    Target(
        name="httpbin-flaky",
        url="https://httpbin.org/status/500",
        expected_status=200,
    ),
    Target(
        name="httpbin-slow",
        url="https://httpbin.org/delay/2",
        expected_status=200,
        timeout_seconds=4.0,
    ),
]

https://httpbin.org/status/500 e o target deliberadamente quebrado. E o que o schedule vai capturar e registrar como incidente. httpbin-slow existe para exercitar o start_to_close_timeout das activities.

Abra src/activities.py, as duas activities:

import time
from dataclasses import dataclass

import httpx
from temporalio import activity
from temporalio.exceptions import ApplicationError

from .storage import init_db, insert_incident
from .targets import Target


@dataclass
class ProbeResult:
    target_name: str
    target_url: str
    status_code: int
    elapsed_ms: int


@activity.defn
async def probe_endpoint(target: Target) -> ProbeResult:
    start = time.perf_counter()
    async with httpx.AsyncClient(timeout=target.timeout_seconds) as http:
        resp = await http.get(target.url)
    elapsed_ms = int((time.perf_counter() - start) * 1000)
    if resp.status_code != target.expected_status:
        raise ApplicationError(
            f"unexpected status {resp.status_code} from {target.url}",
            type="UnexpectedStatus",
        )
    return ProbeResult(target.name, target.url, resp.status_code, elapsed_ms)


@activity.defn
async def record_incident(
    target_name, target_url, status_code, error, elapsed_ms,
) -> None:
    await init_db()
    await insert_incident(
        target_name, target_url, status_code, error, elapsed_ms,
    )

Abra src/workflows.py. O workflow itera sobre os targets, faz retry de cada sondagem com backoff exponencial, e registra incidentes para falhas terminais. O proprio workflow nunca lanca excecao, entao o schedule continua disparando mesmo quando um target esta permanentemente fora do ar:

from datetime import timedelta

from temporalio import workflow
from temporalio.common import RetryPolicy

with workflow.unsafe.imports_passed_through():
    from .activities import probe_endpoint, record_incident
    from .targets import TARGETS


@workflow.defn
class HealthCheckWorkflow:
    @workflow.run
    async def run(self) -> dict[str, str]:
        retry_policy = RetryPolicy(
            initial_interval=timedelta(seconds=1),
            backoff_coefficient=2.0,
            maximum_interval=timedelta(seconds=10),
            maximum_attempts=3,
            non_retryable_error_types=["UnexpectedStatus"],
        )
        results: dict[str, str] = {}
        for target in TARGETS:
            try:
                outcome = await workflow.execute_activity(
                    probe_endpoint,
                    target,
                    start_to_close_timeout=timedelta(seconds=15),
                    retry_policy=retry_policy,
                )
                results[target.name] = f"ok status={outcome.status_code}"
            except Exception as e:
                workflow.logger.warning(f"{target.name} failed: {e}")
                await workflow.execute_activity(
                    record_incident,
                    args=[target.name, target.url, None, str(e), 0],
                    start_to_close_timeout=timedelta(seconds=10),
                )
                results[target.name] = f"incident: {e}"
        return results

Uma nota sobre non_retryable_error_types=["UnexpectedStatus"]: o Temporal vai retentar uma activity ate atingir maximum_attempts, exceto se o type do ApplicationError lancado bater com uma string nao-retentavel. Nao queremos retentar um 500 tres vezes: o target esta quebrado, registre e siga em frente. Erros de rede (timeouts, falhas de DNS, connection resets) lancam tipos de erro diferentes e sao retentados.

Abra src/worker.py, e chato e voce quer que seja:

import asyncio
from temporalio.client import Client
from temporalio.worker import Worker

from .activities import probe_endpoint, record_incident
from .config import TASK_QUEUE, TEMPORAL_ADDRESS, TEMPORAL_NAMESPACE
from .workflows import HealthCheckWorkflow


async def main() -> None:
    client = await Client.connect(TEMPORAL_ADDRESS, namespace=TEMPORAL_NAMESPACE)
    worker = Worker(
        client,
        task_queue=TASK_QUEUE,
        workflows=[HealthCheckWorkflow],
        activities=[probe_endpoint, record_incident],
    )
    await worker.run()


if __name__ == "__main__":
    asyncio.run(main())

Inicie o worker em um segundo terminal (de dentro de temporal-scheduling-quickstart/):

uv run python -m src.worker
# or: make worker

Voce agora tem um Temporal server, um worker Python e zero schedules. Hora de adicionar alguns.

Testes

Eu testei quatro coisas, nesta ordem: um schedule por intervalo com overlap policy e jitter; um schedule por calendario com timezone; pause / trigger / backfill / list contra um schedule ativo; e a evolucao para a stack docker-compose com Postgres.

Teste 1: Schedule por intervalo com overlap policy e jitter

O schedule por intervalo e o cavalo de batalha entediante e duravel que a maioria dos times quer. A cada dois minutos, com dez segundos de jitter para que schedules concorrentes no mesmo cluster nao disparem todos no mesmo tick de relogio. Abra src/schedules/create_interval.py:

# src/schedules/create_interval.py
import asyncio
from datetime import timedelta

from temporalio.client import (
    Client,
    Schedule,
    ScheduleActionStartWorkflow,
    ScheduleIntervalSpec,
    ScheduleOverlapPolicy,
    SchedulePolicy,
    ScheduleSpec,
    ScheduleState,
)

from ..config import (
    INTERVAL_SCHEDULE_ID, TASK_QUEUE, TEMPORAL_ADDRESS,
    TEMPORAL_NAMESPACE, WORKFLOW_ID_PREFIX,
)
from ..workflows import HealthCheckWorkflow


async def main() -> None:
    client = await Client.connect(TEMPORAL_ADDRESS, namespace=TEMPORAL_NAMESPACE)
    schedule = Schedule(
        action=ScheduleActionStartWorkflow(
            HealthCheckWorkflow.run,
            id=f"{WORKFLOW_ID_PREFIX}-interval",
            task_queue=TASK_QUEUE,
        ),
        spec=ScheduleSpec(
            intervals=[ScheduleIntervalSpec(every=timedelta(minutes=2))],
            jitter=timedelta(seconds=10),
        ),
        policy=SchedulePolicy(
            overlap=ScheduleOverlapPolicy.SKIP,
            catchup_window=timedelta(minutes=10),
        ),
        state=ScheduleState(note="Health check every 2 minutes with 10s jitter."),
    )
    await client.create_schedule(INTERVAL_SCHEDULE_ID, schedule)


if __name__ == "__main__":
    asyncio.run(main())

Rode (o arquivo ja existe no repositorio clonado):

uv run python -m src.schedules.create_interval
# or: make schedule
# created schedule: health-check-interval

Abra http://localhost:8233/namespaces/default/schedules e o schedule aparece com seu proximo horario de execucao:

Web UI do Temporal mostrando o schedule health-check-interval recem-criado com seu proximo horario de execucao.

Em quatro minutos os dois primeiros runs aparecem. Cada run aparece como um workflow execution com o mesmo prefixo de workflow ID. Clique em View All Runs na pagina de detalhe do schedule para ver o historico completo. A lista esta ordenada do mais novo para o mais antigo, entao os dois primeiros runs ficam embaixo:

Lista de workflows filtrada por TemporalScheduledById igual a health-check-interval, mostrando todos os 12 runs concluidos ordenados do mais novo para o mais antigo; os dois primeiros runs ficam no final da lista.

Olhando dentro de um run, o resultado do workflow mostra o desfecho por target: os dois targets saudaveis retornam ok, e httpbin-flaky registra um incidente depois que o Temporal esgota o orcamento de retry da activity:

Detalhe de run de workflow na Web UI do Temporal mostrando o JSON de resultado: httpbin-flaky retornou um incidente, httpbin-ok e httpbin-slow retornaram ok com status 200.

Aquela linha "httpbin-flaky": "incident: Activity task failed" corresponde a uma linha gravada em incidents.db pela activity record_incident.

Os dois botoes deste snippet que importam:

  • SchedulePolicy.overlap=ScheduleOverlapPolicy.SKIP: se o run anterior ainda esta rodando no proximo tick, pule o proximo tick. Seis valores estao disponiveis: SKIP, BUFFER_ONE, BUFFER_ALL, CANCEL_OTHER, TERMINATE_OTHER, ALLOW_ALL. SKIP e o default certo para “seria bom rodar a cada dois minutos mas nunca dois ao mesmo tempo”.
  • catchup_window=timedelta(minutes=10): se o servidor ficou inacessivel por dez minutos e perdeu cinco runs, dispare apenas o mais recente quando voltar. O default se voce nao setar e um ano, o que significa que uma longa indisponibilidade do servidor pode causar estouro de milhares de replays na hora da recuperacao. Defina isso explicitamente. Sempre.

Teste 2: Schedule por calendario com timezone

O schedule por intervalo e certo para “a cada dois minutos”. O schedule por calendario e certo para “todo dia util as 09:00 horario de Nova York”. Calendar specs usam ScheduleRange(start, end, step) para cada campo do calendario. Abra src/schedules/create_cron.py:

# src/schedules/create_cron.py (excerpt)
from temporalio.client import (
    ScheduleCalendarSpec, ScheduleRange, ScheduleSpec, SchedulePolicy,
    ScheduleOverlapPolicy,
)

weekday_9am = ScheduleCalendarSpec(
    hour=(ScheduleRange(start=9),),
    minute=(ScheduleRange(start=0),),
    day_of_week=(ScheduleRange(start=1, end=5),),  # Mon..Fri
    comment="Weekdays 09:00 America/New_York",
)

schedule = Schedule(
    action=ScheduleActionStartWorkflow(
        HealthCheckWorkflow.run,
        id="health-check-cron",
        task_queue=TASK_QUEUE,
    ),
    spec=ScheduleSpec(
        calendars=[weekday_9am],
        time_zone_name="America/New_York",
    ),
    policy=SchedulePolicy(overlap=ScheduleOverlapPolicy.BUFFER_ONE),
    state=ScheduleState(note="Weekday morning health check."),
)

Rode da mesma forma que o Teste 1:

uv run python -m src.schedules.create_cron
# or: make cron
# created schedule: health-check-cron

Abra a pagina de detalhe do schedule health-check-cron. O painel Upcoming Runs prova que o calendar spec e o timezone estao conectados: cada entrada esta as 09:00 America/New_York, traduzida pela Web UI para o seu timezone local, e a lista pula da sexta 8 de maio direto para segunda 11 de maio, ignorando o fim de semana:

Pagina de detalhe do schedule health-check-cron na Web UI do Temporal mostrando cinco runs futuros as 09:00 America/New_York em dias uteis consecutivos, com o fim de semana de 9 e 10 de maio ignorado.

Se voce preferir uma string crontab familiar, troque calendars=[weekday_9am] por cron_expressions=["0 9 * * MON-FRI"]. Ambos funcionam. O formato calendar e mais legivel para expressoes nao triviais e o formato cron e mais curto para as faceis.

⚠️ Horario de verao e correspondencia literal. Um schedule para 02:30 dispara na maioria dos dias mas pula o dia em que os relogios adiantam (02:30 nao existe) e dispara duas vezes no dia em que atrasam. Escolha 03:00 se voce nao quiser pensar nisso.

Teste 3: Pause, trigger, backfill, list

Esse foi o teste que me virou de “Temporal e interessante” para “estou substituindo meu cron”. Nenhuma dessas operacoes existe no cron. Todas elas sao uma chamada Python. Abra src/schedules/pause_trigger.py:

# src/schedules/pause_trigger.py (excerpt)
handle = client.get_schedule_handle("health-check-interval")
await handle.pause(note="Maintenance window")
await handle.unpause(note="Resuming")
await handle.trigger(overlap=ScheduleOverlapPolicy.ALLOW_ALL)

Backfill e a feature matadora. “O schedule ficou pausado por uma hora, por favor rode todos os runs que perdemos.” Abra src/schedules/backfill.py:

# src/schedules/backfill.py
from datetime import datetime, timedelta, timezone
from temporalio.client import ScheduleBackfill, ScheduleOverlapPolicy

handle = client.get_schedule_handle("health-check-interval")
now = datetime.now(timezone.utc)
await handle.backfill(
    ScheduleBackfill(
        start_at=now - timedelta(minutes=30),
        end_at=now - timedelta(minutes=1),
        overlap=ScheduleOverlapPolicy.ALLOW_ALL,
    ),
)

overlap=ALLOW_ALL aqui significa “dispare todos os runs perdidos concorrentemente”. Escolha BUFFER_ALL se voce quer que sejam sequenciais.

Comece pausando o schedule e descrevendo:

uv run python -m src.schedules.pause_trigger pause
# or: make pause
uv run python -m src.schedules.list_describe describe
# or: make describe
# id:          health-check-interval
# note:        Paused via pause_trigger.py
# paused:      True
# num_actions:                19
# num_actions_skipped_overlap:0
# running_actions:            0
# last_action_started_at:     2026-05-05 23:50:07.151203+00:00
# last_action_scheduled_at:   2026-05-05 23:50:07.127000+00:00
# next_action_at:             2026-05-05 23:54:02.613000+00:00

A linha paused: True mais o next_action_at no futuro sao a prova: o schedule ainda tem um relogio de tick ativo, mas esta bloqueado pelo pause. Despausar destrava o gate sem resetar nada.

A pagina de lista de Schedules tambem mostra o estado de pause: health-check-interval carrega um badge amarelo de Paused enquanto health-check-cron mantem Running:

Lista de Schedules na Web UI do Temporal mostrando dois schedules: health-check-cron com status Running e health-check-interval com um badge amarelo Paused.

Agora exercite o resto do ciclo de vida. Despause para destravar o gate, faca backfill para disparar os runs que voce perdeu, e dispare um run extra ad-hoc:

uv run python -m src.schedules.pause_trigger unpause
# or: make unpause
uv run python -m src.schedules.backfill
# or: make backfill
uv run python -m src.schedules.pause_trigger trigger
# or: make trigger

Cada comando leva menos de um segundo. A Web UI reflete cada mudanca em tempo real. Cron, em contraste, nao tem nocao de nada disso.

Teste 4: Evoluir para docker-compose

O dev server e otimo para prototipagem mas e um processo so e SQLite. Eventualmente voce vai querer Postgres e um servidor com restart separado. O caminho e a stack docker-compose.

O repositorio companheiro do my2cents.ai inclui um docker-compose.yml enxuto:

services:
  postgresql:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: temporal
      POSTGRES_USER: temporal
    ports: ["5432:5432"]
    volumes: ["postgres-data:/var/lib/postgresql/data"]

  temporal:
    image: temporalio/auto-setup:1.27
    depends_on: [postgresql]
    environment:
      DB: postgres12
      DB_PORT: 5432
      POSTGRES_USER: temporal
      POSTGRES_PWD: temporal
      POSTGRES_SEEDS: postgresql
    ports: ["7233:7233"]

  temporal-ui:
    image: temporalio/ui:2.40.1
    depends_on: [temporal]
    environment:
      TEMPORAL_ADDRESS: temporal:7233
    ports: ["8080:8080"]

volumes:
  postgres-data:

Pare o dev server (Ctrl-C), suba a stack, e o mesmo worker Python, mesmo endereco localhost:7233, conecta sem mudancas:

make compose-up
[+] up 60/60
 ✔ Image postgres:16                                      Pulled     7.0s
 ✔ Image temporalio/auto-setup:1.27                       Pulled     6.8s
 ✔ Image temporalio/ui:2.40.1                             Pulled     6.0s
 ✔ Network temporal-scheduling-quickstart_default         Created    0.0s
 ✔ Volume temporal-scheduling-quickstart_postgres-data    Created    0.0s
 ✔ Container temporal-scheduling-quickstart-postgresql-1  Started    0.6s
 ✔ Container temporal-scheduling-quickstart-temporal-1    Started    0.3s
 ✔ Container temporal-scheduling-quickstart-temporal-ui-1 Started    0.3s

Depois aponte um worker para ela e recrie o schedule:

make worker      # in another terminal
make schedule
open http://localhost:8080   # UI is on 8080 here, not 8233

O schedule, o codigo do workflow, o codigo das activities e o codigo do worker sao byte-a-byte identicos ao que rodou contra o dev server. Esse e o ganho arquitetural: dev e producao self-hosted sao o mesmo sistema, apenas persistidos de forma diferente.

Self-hosting de nivel producao e uma conversa maior: TLS, namespaces, advanced visibility via Elasticsearch, archival, replicacao multi-cluster. O self-hosted guide e a proxima leitura certa. Para uma demo de um laptop so, os tres servicos do compose acima sao suficientes.

Limpeza

Pare o dev server com Ctrl-C no terminal rodando make dev. Se voce subiu a stack docker-compose, derrube e remova o volume do Postgres para nao deixar um banco velho montado:

make delete       # drops the schedules from the cluster
make compose-down # stops compose and deletes the postgres volume
make clean        # removes incidents.db and temporal.db

Schedules sao registros do lado do servidor. Se voce nao deleta antes de deletar o banco, eles somem com o banco. Se voce mantem o banco e sobe um worker novo, eles voltam a disparar imediatamente. Seja deliberado sobre qual dos dois voce quer.

Veredito

Uma engine de durable execution prova seu peso especificamente em workloads de AI. Chamadas a LLM sao lentas, caras e rate-limited; fluxos agenticos encadeiam dez ou vinte delas; jobs de inferencia em batch podem rodar por horas. Um scheduler estilo cron que perde um run em andamento forca o proximo tick a refazer o gasto, e logica ad-hoc de retry agrava a conta toda vez que erra um pouquinho. O Temporal move “o trabalho foi concluido, com quais passos ja pagos” para dentro da plataforma: cada tool call, cada retry, cada sleep longo e um checkpoint, e um worker que travou retoma do ultimo em vez de recomecar a cadeia. Esse e um formato de garantia diferente de “o cron disparou as 03:00”.

Comments