Robô RPA preso em loop infinito e sem nunca dar erro

Um robô de RPA que eu mantenho pra um cliente baixou um arquivo, gravou os dados certos no banco, e devia ter encerrado a execução. Só que ele continuou rodando. Sem erro. Sem trava. Sem nenhum aviso no Telegram avisando que algo estava errado.
Só percebi o comportamento errado no início do dia seguinte, quando vi que o robô ainda estava rodando. E foi aí que descobri: o problema não era um bug óbvio, era um robô que estava fazendo exatamente o que o código mandava, só que preso num ciclo que ninguém tinha previsto.
O problema
O robô baixa documentos de um portal de um cliente da indústria automotiva e grava os dados extraídos num banco de dados interno. Pra saber o que ainda falta processar, ele lê o status de cada item na tela do portal: itens marcados como “New” (novo) ainda precisam ser baixados; depois de baixados, o portal deveria atualizar o status pra “Downloaded” ou “Aceito”.
Num certo dia, o arquivo foi baixado corretamente e os dados foram gravados no banco sem nenhum problema. Não houve perda de dado nenhuma.
O problema começou depois disso: o portal externo tem um comportamento inesperado, mesmo depois de todos os documentos de um item serem baixados, o status dele na tela continua aparecendo como “New”. Como o robô usa exatamente esse status pra decidir o que processar, ele entendia que o item continuava pendente e voltava a tentar baixar de novo. E de novo. E de novo.
Por que isso não gerava nenhum alerta
A primeira coisa que eu esperava encontrar era um erro no log. Não tinha nenhum.
O código original não tinha nenhum limite de tentativas. Se um item não progredia, ele simplesmente tentava de novo, indefinidamente, sem contar quantas vezes já tinha tentado e sem lançar exceção nenhuma. Tecnicamente, não era um bug que trava o programa, o robô continuava rodando normalmente, só que sem sair do lugar.
Do ponto de vista do código, não havia erro algum acontecendo. Só um item que “ainda não tinha sido baixado”, igual a todos os outros itens legítimos que o robô processa no dia a dia. Não existia distinção entre “esse item é novo de verdade” e “esse item só não teve o status atualizado”.
A solução: um contador de tentativas sem progresso
A correção não mexeu na lógica de download em si, o robô continuava capaz de processar itens normais sem nenhuma mudança. O que faltava era uma rede de segurança: um jeito de o robô reconhecer “eu já tentei isso um monte de vezes e não avancei nada” e agir diferente a partir daí.
A ideia, na prática, fica parecida com isto:
tentativas_sem_progresso = {}
LIMITE_TENTATIVAS = 5
def processar_item(item):
progrediu = tentar_baixar(item)
if progrediu:
tentativas_sem_progresso[item.id] = 0
return
tentativas_sem_progresso[item.id] = tentativas_sem_progresso.get(item.id, 0) + 1
if tentativas_sem_progresso[item.id] >= LIMITE_TENTATIVAS:
logging.error(f"Item {item.id} sem progresso após {LIMITE_TENTATIVAS} tentativas. Encerrando.")
raise ItemSemProgressoError(item.id)
Cada item tem seu próprio contador. Toda vez que o robô consegue avançar de verdade (baixar algo novo, no caso), o contador zera. Se ele tentar processar o mesmo item várias vezes seguidas sem nenhum avanço, o contador sobe, e ao atingir o limite o robô registra um erro de verdade no log e encerra a execução, em vez de continuar tentando pra sempre.
Isso é o padrão conhecido como circuit breaker: em vez de deixar uma operação falhar (ou, nesse caso, não progredir) indefinidamente, existe um limite que interrompe o ciclo e transforma “nada está acontecendo” em um erro explícito, visível, que alguém vai ver.
Resultado
Validado direto em produção: o mesmo item que continuava aparecendo como “New” por causa do comportamento do portal foi tentado 5 vezes pelo robô. No quinto ciclo sem progresso, ele reconheceu a situação, registrou o erro no log e encerrou a execução normalmente, em vez de continuar rodando escondido.
Pontos importantes
Ausência de erro não é sinônimo de robô funcionando: um robô pode rodar continuamente, sem lançar nenhuma exceção, e ainda assim não estar fazendo progresso nenhum. Monitorar só “deu erro ou não deu erro” não é suficiente; vale ter também um limite explícito de tentativas sem avanço. Usei um princípio parecido, na direção oposta, num watchdog que mantém um dashboard sempre visível numa fábrica: fazer a falha aparecer de forma clara é sempre melhor do que deixá-la escondida.
Todo RPA que depende de status de sistema externo devia desconfiar desse status: a lógica assumia que o portal sempre atualizaria o status depois do download. Quando esse contrato foi quebrado, mesmo que só num caso raro, o robô não tinha nenhum plano B. Um circuit breaker simples resolve isso sem precisar prever cada jeito específico que o sistema externo pode falhar.
Perguntas frequentes
Por que um robô de RPA pode ficar em loop infinito sem gerar nenhum erro?
Isso acontece quando a lógica do robô depende do estado de um sistema externo pra decidir se um item já foi processado, e esse sistema não atualiza o estado como esperado. Do ponto de vista do código, nenhuma exceção é lançada, nenhuma condição de erro é atingida, então o robô continua rodando normalmente, só que sem sair do lugar. Sem um limite de tentativas, esse comportamento nunca aparece como falha nos logs.
O que é um circuit breaker e como aplicar isso em RPA com Python?
Circuit breaker é um padrão de resiliência que interrompe uma operação depois de um número definido de falhas ou tentativas sem sucesso, em vez de deixar ela repetir pra sempre. Em RPA com Python, a forma mais simples é um contador que soma 1 a cada tentativa sem progresso e zera quando há progresso real; ao atingir um limite (por exemplo 5), o robô loga o erro e encerra a execução em vez de continuar tentando.
Como impedir que um robô RPA fique preso reprocessando o mesmo item pra sempre?
Adicionando um contador de tentativas consecutivas sem progresso pra cada item, separado do fluxo principal. Se o robô tentar processar o mesmo item um número de vezes seguidas sem conseguir avançar (baixar um arquivo novo, mudar um status, etc.), ele deve parar de tentar e registrar isso como erro, em vez de assumir que o item só 'ainda não está pronto'.
Por que confiar cegamente no status de um sistema externo pode quebrar a lógica de um robô RPA?
Porque o robô está assumindo um contrato que o sistema externo pode não cumprir: que o status muda de forma confiável depois de uma ação (como um download). Se esse contrato for violado, mesmo que só num caso raro, qualquer lógica que decida 'o que falta processar' com base nesse status pode entrar num ciclo sem fim, já que do ponto de vista do robô o item nunca deixa de estar pendente.
Como saber se um robô RPA está travado mesmo sem nenhum log de erro?
Log de erro sozinho não é suficiente pra monitorar RPA, porque um robô pode estar rodando continuamente sem nunca lançar uma exceção e ainda assim não estar progredindo. Vale monitorar também sinais indiretos, como tempo de execução muito acima do normal, ausência de notificação de conclusão dentro de uma janela esperada, ou (o mais direto) um limite de tentativas que force o robô a admitir falha depois de N tentativas sem avanço.
Curtiu o conteúdo ou tem um problema parecido pra resolver? Bora trocar uma ideia.
Fale comigo no WhatsApp