Pipelines e orquestração
DAG, dependências, retry e carga incremental — sem servidor, sem agendador, sem banco de metadados.
Airflow, Prefect e Dagster resolvem orquestração com um servidor, um banco de metadados e um agendador. Arcane.Pipeline resolve com uma estrutura de dados e um laço — o que cabe num processo só, que é onde a maioria dos pipelines de verdade vive.
adopt Arcane.Pipeline as P
fluxo := P.fluxo("vendas")
P.etapa(fluxo, "extrair", extrair)
P.etapa(fluxo, "limpar", limpar, ["extrair"])
P.etapa(fluxo, "conferir", conferir, ["limpar"])
P.etapa(fluxo, "carregar", carregar, ["conferir"])
relatorio := P.rodar(fluxo)A ordem sai das dependências, não da ordem em que você declarou — é a ordenação topológica que todo DAG faz. P.grafico(fluxo) mostra o resultado antes de rodar.
As três garantias#
1. Um ciclo é erro, não aviso#
P.etapa(fluxo, "x", acao_x, ["y"])
P.etapa(fluxo, "y", acao_y, ["x"])
erro: as etapas x, y dependem umas das outras.
nota: um ciclo não tem ordem possível
dica: quebre o ciclo, ou junte as etapas numa sóNão há ordem que satisfaça as duas. Escolher uma arbitrariamente produziria um resultado que ninguém consegue explicar — e que muda entre execuções.
2. Falhou? quem depende é PULADO#
ok extrair
falhou limpar banco fora do ar
pulada conferir depende de limpar
pulada carregar depende de conferir
ok notificar3. Retry para a falha passageira#
// 3 tentativas, esperando 2s, 4s entre elas
P.etapa(fluxo, "carregar", carregar, ["limpar"], 3, 2)A espera cresce a cada tentativa. Se o banco está ocupado, insistir no mesmo ritmo mantém ele ocupado. E o relatório diz em qual tentativa passou — uma etapa que sempre precisa de três é um problema que a média esconde.
Carga incremental#
Reprocessar tudo a cada execução é o que faz um pipeline de 10 minutos virar um de 6 horas em dois anos.
fluxo := P.fluxo("vendas", "estado/vendas.json")
action extrair(ctx):
desde := P.marca(fluxo, "ate") ?? "1970-01-01"
novas := Banco.consultar(db,
"SELECT * FROM vendas WHERE atualizado_em > ?", [desde])
given len(novas) bigger 0:
P.marcar(fluxo, "ate", maior(novas, "atualizado_em"))
yield novasRodar só um pedaço#
P.rodar_ate(fluxo, "limpar") // roda 'extrair' e 'limpar', e paraEle resolve as dependências sozinho — roda o que limpar precisa, e nada além. É como se depura um pipeline longo sem esperar a carga.
O relatório#
{
"fluxo": "vendas",
"ok": yes,
"duracao": 12.4,
"resumo": {"total": 4, "ok": 4, "falhou": 0, "pulada": 0, "saltada": 0},
"etapas": [
{"etapa": "extrair", "estado": "ok", "duracao": 8.1, "tentativas": 1},
…
],
"resultados": {"extrair": [ … ], "limpar": [ … ]}
}O relatório é o produto. Sem ele, saber o que aconteceu exige ler log — e log de pipeline é o que ninguém lê até quebrar.
Condição: o mesmo fluxo em modos diferentes#
P.etapa(fluxo, "carga_completa", completa, ["limpar"],
1, 0, lambda ctx: MODO is "cheio")A etapa é saltada quando a condição dá falso — e saltar não é falhar: o fluxo segue verde. É como se roda o mesmo pipeline em modo cheio e incremental sem duplicá-lo.