Como implementar e debugar uma cadeia acíclica na prática
Você já tentou rodar um workflow inteiro e ele travou porque um dos nós pendia em loop infinito. Isso acontece quando o grafo dirigido tem um ciclo e o orquestrador não consegue determinar a ordem de execução correta. Vou explicar como isso funciona, como detectar o problema e como estruturar seu pipeline de verdade, sem teoria desnecessária.
O que é uma cadeia acíclica e por que ela existe
Uma cadeia acíclica, ou DAG (Directed Acyclic Graph), é simplesmente um conjunto de tarefas conectadas por arestas direcionadas onde não é possível partir de um nó e retornar a ele mesmo seguindo a direção das arestas. Se existir qualquer caminho que volte ao ponto de partida, seu grafo não é mais acíclico e o sistema de orquestração quebra. A utilidade disso é prática: orquestradores como Airflow, Prefect e Dagster dependem dessa propriedade para calcular uma ordenação topológica válida. Sem ela, não há como saber qual tarefa deve rodar antes da outra. Um DAG válido garante que cada nó seja executado exatamente uma vez, na ordem correta, sem loops infinitos.
Comece pelo algoritmo de detecção, não pela definição
O jeito mais comum de validar se um grafo é acíclico é usar DFS com marcação de estados. Cada nó recebe uma cor: branco (não visitado), cinza (na pilha de recursão atual) ou preto (completamente processado). Se durante o traversal você encontrar uma aresta que aponta para um nó cinza, encontrou um ciclo. Esse é o algoritmo padrão e leva tempo linear em relação ao número de vértices mais arestas, O(V + E). Na prática, muitos pipelines usam uma variação chamada topological sort com Kahn's algorithm, que conta os graus de entrada de cada nó e vai removendo nós com grau zero iterativamente. Se ao final restarem nós com grau maior que zero, o grafo tem ciclo. Esse método é mais fácil de implementar de forma iterativa e evita estourar a pilha de recursão em grafos grandes.
Eu recomendo começar testando com Kahn porque a saída dele já te dá a ordem de execução. Se o grafo for realmente acíclico, o resultado é uma lista ordenada pronta para ser usada pelo seu scheduler. Se sobrar nó, você tem seu ciclo e ainda pode isolar o subgrafo problemático. Aqui vai uma implementação direta em Python que eu uso como ponto de partida:
from collections import deque, defaultdict def is_dag(graph):
in_degree = defaultdict(int) for u in graph:
for v in graph[u]: in_degree[v] += 1
queue = deque(n for n in graph if in_degree[n] == 0) visited = 0
while queue: node = queue.popleft()
visited += 1 for neighbor in graph[node]:
👉 Clique no botão abaixo para saber mais sobre o assunto!
in_degree[neighbor] -= 1 if in_degree[neighbor] == 0:
queue.append(neighbor) return visited == len(graph)
Se a função retornar False, os nós que nunca entraram na fila são os que participam de um ou mais ciclos. Você pode fazer um segundo passo para extrair apenas esse subgrafo e identificar exatamente quais arestas estão problemáticas.
Um problema real que eu encontrei
Num projeto de engenharia de dados, tínhamos um DAG com cerca de 80 tarefas no Airflow. O pipeline rodava normalmente por semanas e de repente começou a falhar com um erro genérico de dependência circular. O problema era que um dos desenvolvedores tinha adicionado uma tarefa de limpeza que puxava dados de uma task que, por sua vez, dependia indiretamente daquela mesma task de limpeza através de três níveis de encadeamento. O ciclo era indireto e invisível de olho nu. A solução que eu apliquei foi simples mas eficiente: exportei o grafo inteiro para JSON, rodei um script de detecção de ciclos que retornava o caminho exato (não só a presença do ciclo), e aí consegui ver a aresta problemática em segundos. O script que usei fez uma varredura com DFS recursivo mantendo o caminho atual em uma lista, e quando encontrava uma aresta para um nó já no caminho, retornava o ciclo completo.
O workaround imediato foi quebrar a dependência transformando a task de limpeza em uma sensor ou trigger rule separada, sem aresta direta de retorno. Na versão seguinte do DAG, passei a validar o grafo automaticamente no CI antes de aceitar o merge, usando uma função parecida com a do Kahn acima. Desde então, esse tipo de bug nunca mais apareceu.
Pegadinhas que ninguém conta
A primeira: dependências implícitas não são detectadas por algoritmos de ciclo. Se sua tarefa B depende do resultado da tarefa A mas você esqueceu de declarar a aresta A B no grafo, o DAG é tecnicamente acíclico mas a execução vai falhar porque B vai rodar sem o dado que precisa. Isso é mais comum do que parece em equipes grandes onde o grafo é construído dinamicamente. A segunda: cadeias acíclicas não resolvem problemas de estado compartilhado. Dois nós que não têm relação de dependência direta podem ainda assim ler e escrever no mesmo arquivo ou tabela. O DAG garante ordem entre nós conectados, mas não imposição sobre concorrência em recursos compartilhados. Se você tem dois ramos paralelos que gravam na mesma partition de tabelas, vai precisar de locks ou serialização externa.
A terceira: grafos muito largos ficam difíceis de manter. Eu já vi DAGs com mais de 200 nós na mesma camada horizontal. A ordenação topológica funciona perfeitamente, mas o custo de manter, debugar e fazer deploy aumenta dramaticamente. A recomendação prática é dividir em múltiplos DAGs menores com dependências entre eles via xcom ou triggers externas, em vez de tentar colocar tudo num único grafo.
Quando uma cadeia acíclica não é a melhor solução
Se o seu workflow tem lógica condicional complexa — tipo "se o dados vierem atrasados, rode o branch X, senão rode o Y, e depois talvez execute Z" — um DAG puro fica inviável. A representação visual vira um emaranhado e a manutenção se torna impraticável. Nesse caso, ferramentas como step functions (AWS) ou workflows baseados em estado (StepZen, Temporal) oferecem modelagem mais adequada porque permitem branching condicional sem forçar tudo em arestas fixas. Outro cenário onde DAGs falham: pipelines com backfill agressivo em dados que mudam de schema frequentemente. Cada mudança de schema pode exigir recriação de partições do grafo, e o overhead de reconstruir e validar o DAG a cada deploy pode consumir mais tempo do que o valor agregado. Nesses casos, um pipeline orientado a eventos com streaming (Kafka + Flink, por exemplo) é mais apropriado.
Cadeia acíclica na prática do dia a dia
O essencial é: valide seu grafo antes de deploy, isole ciclos com Kahn ou DFS, mantenha os DAGs com largura razoável (menos de 50 nós por camada é um bom limite), e não confie cegamente na aciclicidade como garantia de correção funcional. O grafo poder ser academicamente válido e ainda assim seu pipeline falhar por dependências não declaradas ou concorrência em recursos compartilhados. Para quem quer começar, a biblioteca networkx do Python já tem is_dag() e topological_sort() prontos. Para produção, integrações com Airflow ou Prefect geralmente oferecem validação automática no parsing do DAG, mas confiar nessa validação sem testes unitários próprios é arriscado. Um teste rápido que constrói o grafo e roda a função do Kahn leva menos de 2 segundos e cobre a maior parte dos casos problemáticos.
Se você quer o código-fonte completo do detector de ciclos com extração de caminho, o repositório padrão que eu mantenho como referência está disponível para consulta. A estrutura é genérica o suficiente para adaptar a qualquer linguagem ou framework de orquestração.