12 pontos por GN⁺ 2025-05-27 | 1 comentários | Compartilhar no WhatsApp
  • Ao fazer tentativas repetidas de conexão em scripts Bash para verificar o status de um servidor web, pode surgir o problema de o servidor entrar inesperadamente em um loop infinito
  • A ferramenta timeout, usada para resolver isso, define um limite de tempo para a execução de um comando e, ao excedê-lo, envia um sinal para tentar o encerramento do processo
  • Ela não pode ser aplicada diretamente a shell built-ins como until, então a solução é usar um wrapper com processo bash ou separar em outro script

Espera por servidor web em Bash e o problema de loop infinito

  • No trabalho prático, scripts Bash são usados para fazer configuração de servidor web e verificação de status
  • A estrutura adia a próxima etapa enquanto o servidor está subindo e, em geral, funciona sem problemas
  • Porém, se ocorrer um crash durante a inicialização do servidor, o script pode cair em um loop infinito, o que exigiu uma solução

Exemplo de uso de until e suas limitações

  • O health check do servidor web é repetido com uma sintaxe como a seguinte
    until curl --silent --fail-with-body 10.0.0.1:8080/health; do  
    	sleep 1  
    done  
    
  • Quando o servidor falha, ocorre a situação em que sleep 1 se repete para sempre

Introdução ao utilitário timeout

  • O comando timeout encerra um comando enviando um sinal (como SIGTERM) se ele não for concluído dentro do tempo especificado
  • Exemplo: em timeout 1s sleep 5, após 1 segundo é feita uma tentativa de encerrar o processo sleep
  • Ao encerrar, ele retorna um código de saída anormal (por exemplo, 124)

Tentativa de combinar timeout com until e o problema

  • Naturalmente, tenta-se combinar timeout com until da seguinte forma
    timeout 1m until curl ...; do  
    	sleep 1  
    done  
    
  • Porém, timeout consegue enviar sinais para processos, enquanto until é uma palavra-chave embutida do shell e não pode receber isso diretamente

Solução: wrapper com processo Bash ou uso de script externo

  • Se todo o loop until for encapsulado com bash -c e executado como um processo separado, será possível aplicar timeout
    timeout 1m bash -c "until curl ...; do sleep 1; done"  
    
  • Outra opção é separar a parte do loop em um script Bash externo e então aplicar timeout a esse script
    timeout 1m ./until.sh  
    
  • Embora timeout não possa ser aplicado diretamente a shell built-ins, com esses métodos é possível obter o comportamento desejado

1 comentários

 
GN⁺ 2025-05-27
Comentários do Hacker News
  • Meu truque pouco conhecido favorito é usar fault injection do strace para testar falhas em várias system calls

    $ strace -e trace=clone -e fault=clone:error=EAGAIN
    

    Há uma explicação mais detalhada no link relacionado

    • Acho essa funcionalidade realmente impressionante e compartilho a sensação de que gostaria de tê-la conhecido antes
      Como não havia como testar os ramos de falha, eu costumava substituir temporariamente partes da função por código provisório, mas esse truque sugere uma abordagem mais concisa

    • Opinião de que esse método parece realmente útil
      Levanta a dúvida se existe algo parecido também no Windows

  • Sugestão de que, em health checks de serviço, a melhor abordagem é definir tanto o tempo máximo de timeout quanto o número máximo de tentativas
    Em geral, tenta-se repetir até X vezes e considera-se falha dentro de um tempo máximo de Y
    Enfatiza-se a necessidade de decidir a falha o mais rápido possível, em vez de esperar tempo demais
    Em serviços padronizados, o health check só começa depois que as dependências do contêiner estão garantidas e prontas para operar
    No Kubernetes, veja Init Container; no AWS ECS, dependsOn; no Docker Compose, a configuração depends_on
    É fornecido um exemplo em shell POSIX
    Mas também é mencionado que o próprio curl já tem essa funcionalidade embutida, então ela pode ser usada assim, sem script separado

    curl --silent --fail-with-body --connect-timeout 5 --retry-all-errors --retry-delay 1 --retry-max-time 300 --retry 300 10.0.0.1:8080/health
    
  • Relato de ter tentado várias vezes implementar timeout só com builtins do bash, já que no Mac o comando timeout não é fornecido por padrão
    Explica-se que o comando sleep é padrão POSIX, então pode ser usado
    É fornecido abaixo um exemplo de implementação da funcionalidade de timeout

    # TIMEOUT SYSTEM(resumo)
    # function timeout <num_seconds> <command>
    # aciona <command> após certo tempo
    

    Uma função chamada times_up faz o tratamento do timeout
    É fornecido um exemplo de teste com timeout de 10 segundos repetindo um for 20 vezes

    • Relato de que, há 12 anos, foi implementado um método parecido seguindo um conselho do Stack Overflow
      Os detalhes podem ser vistos no link de referência
      Enfatiza-se que o código usava apenas shell builtins e sleep, e que a compatibilidade com POSIX era obrigatória
      Também se menciona que, no exemplo, a sintaxe {1..20} do bash não é POSIX, então é preciso cuidado
      Minha melhoria foi fazer a função retornar true quando não há timeout e false quando ele ocorre, para simplificar o tratamento de erro dentro do script

    • Compartilha-se um método bem simples de executar o comando e sleep em paralelo e, depois do tempo especificado, encerrar o comando com um sinal

      <command> & sleep <timeout>; kill -SIGALRM %1
      
    • Compartilha-se um exemplo de script de 13 anos atrás que implementava timeout com read -t
      Link

  • Informa-se que o curl já tem a flag --retry-connrefused, então essa funcionalidade pode ser usada diretamente sem loop em shell

  • Ao usar bash -c, se for necessário passar variáveis, recomenda-se adicionar os argumentos assim

    bash -c 'some command "$1" "$2"' -- "$var1" "$var2"
    

    Explica-se por que usar "--" e qual é o papel de argv[0]
    Também se menciona que printf %q pode ser usado, mas há preferência pela abordagem compatível com Bourne

    • Explica-se que "--" tem um significado muito claro como sinal de fim de opções no bash e na maioria das CLIs Unix/Linux
      Referência relacionada

    • O Busybox decide qual programa executar com base no valor de argv[0], então ele pode ser definido como o comando desejado, como ls, mv ou cp

  • Quando preciso de lógica de repetição, o método que costumo usar é o seguinte

    for i in {0..60}; do
      true -- "$i"
      if eventually_succeeds; then break; fi
      sleep 1s
    done
    

    Não é muito elegante, mas em geral é correto, e em um nível mais avançado também se pode aplicar backoff exponencial
    Há vantagens também em termos de extensibilidade

    • O shellcheck recomenda tratar esse caso usando a variável _
      Link de referência

    • Ressalta-se que a função eventually_succeeds, dependendo da situação, pode precisar de timeout ou de código defensivo adicional
      Relembra-se a importância de sempre escrever código defensivo em POSIX/processos/IO

  • Relato de que, quando os filhos eram pequenos, isso foi usado como uma espécie de controle parental para permitir assistir apenas um programa de 30 minutos com o comando abaixo

    timeout 1800 mplayer show.mp4 ; sudo pm-suspend
    

    Avaliação de que a ideia foi aplicada de forma muito útil

    • Comentário adicional de que esse foi o caso de uso explicado da forma mais legal
  • Afirma-se que não se gosta muito de usar comandos inline ou arquivos de script temporários quando é necessário enviar sinais para subprocessos
    O método preferido é transformar a lógica desejada em uma função, exportá-la e depois envolvê-la com timeout bash -c
    Isso se relaciona com o método seguro de passagem de argumentos mencionado por aidenn0

    #!/usr/bin/env bash
    
    long_fn () { # implemente a lógica desejada
     sleep $1
    }
    to () {
     local duration="$1"; shift
     local fn_name="$1"; shift
     export -f "$fn_name"
     timeout "$duration" bash -c "$fn_name"' "$@"' _ $@
    }
    
    time to 1s long_fn 5
    
    • Aponta-se que é preciso usar "$@" no final
      Caso contrário, argumentos com espaços não são passados corretamente
      Compartilha-se um exemplo de long_fn para verificar esse ponto
  • Relembra-se um post de blog antigo que mencionava timeout
    Recomenda-se o blog relacionado para quem tiver curiosidade sobre linguagens de programação gerais, e não shell, ou sobre o funcionamento interno

  • Relato de experiência adicionando timeout de comandos em um ambiente Kubernetes
    Informa-se que scripts shell POSIX como await-cmd.sh, await-http.sh e await-tcp.sh estão maduros e podem ser bastante úteis em certas situações
    Link do projeto relacionado