Mais um Heisenbug passando pelo crocodilo

Mais um Heisenbug passando pelo crocodilo

$> set -o pipefail

$> fortune | head -1 > /dev/null && echo "Повезло!" || echo "Вы проиграли"
Повезло!

$> fortune | head -1 > /dev/null && echo "Повезло!" || echo "Вы проиграли"
Вы проиграли

é fortune programa condicional sem exit(rand()).

Você pode explicar o que há de errado aqui?

Digressão lírico-histórica

Conheci esse Heisenbug pela primeira vez há um quarto de século. Então para o gateway no FaxNET foi necessário criar vários utilitários via tubos "jogando damas" no FreeBSD. Como esperado, considerei-me um programador avançado e bastante experiente. Portanto, pretendia fazer tudo com o máximo cuidado e atenção possível, prestando especial atenção ao tratamento de erros...

Minha experiência anterior lidando com bugs no sendmail e uucp/uupc aumentou minha diligência no “tratamento completo de erros”. Não faz sentido mergulhar nos detalhes dessa história, mas lutei com esse Heisenbug por duas semanas, durante 10 a 14 horas. Por isso, foi lembrado, e ontem esse velho conhecido passou por aqui novamente para nos visitar.

Resposta DR

Utilitário head lata feche o canal de fortune imediatamente assim que ele lê a primeira linha. Se fortune gera mais de uma linha, então a chamada correspondente write() retornará um erro ou informará que menos bytes são gerados do que o solicitado. Por sua vez, escrito com tratamento cuidadoso de erros fortune tem o direito de reflectir esta situação no seu estatuto de saída. Então devido à instalação set -o pipefail vai funcionar || echo "Вы проиграли".

No entanto, head pode não chegar a tempo fechar antes fortune terminará a saída de dados. Então vai funcionar && echo "Повезло!".

Em um dos meus hoje GNUMakefile existe tal um fragmento:

echo '#define MDBX_BUILD_COMPILER "$(shell set -o pipefail; $(CC) --version | head -1 || echo 'Please use GCC or CLANG compatible compiler')"'

Traduzido em humano

É comum aqui GNU Make и bater no modo do compilador usando a opção --version pergunta quem ele é e, se a opção não for suportada, um esboço é inserido "Por favor, use um compilador compatível com GCC ou CLANG".

como clichê pode ser encontrado em qualquer lugar. Apareceu neste local há muito tempo e funcionou perfeitamente em qualquer lugar (Linux, Solaris, OSX, FreeBSD, WSL etc.). Mas ontem em Alt LinuxName na plataforma Elbrus 2000 (E2K) Percebi:

#define MDBX_BUILD_COMPILER "lcc:1.23.20:Sep--4-2019:e2k-v3-linux Please use GCC or CLANG compatible compiler"

Francamente, não reconheci imediatamente meu antigo “conhecido”. Além disso, o projeto já foi testado diversas vezes no Elbrus e em diversas distribuições diferentes, incluindo Alt. Com vários compiladores, versões do GNU Make e bash. Portanto, eu não queria ver meu erro aqui.

Ao tentar reproduzir o problema e/ou entender o que estava acontecendo, mais coisas estranhas começaram a acontecer.
Feitiço de linha de comando:

echo "#define MDBX_BUILD_COMPILER '$(set -o pipefail; LC_ALL=C cc --version | head -1 || echo "Please use GCC or CLANG compatible compiler")'"

De vez em quando produzia texto extra, então não... Muitas vezes uma das opções durava bastante tempo, mas se você cutucasse por mais tempo, sempre teria as duas!

Naturalmente, strace nosso tudo! E depois de digitar um discurso strace, mas não tendo tempo de pressionar Enter, reconheci meu velho amigo, Sr. Heisenbug, e os desenvolvedores compilador eu mesmo há 25 anos, Nostalgia… E resolvi ficar triste e escrever esse bilhete 😉

A propósito, como qualquer pessoa que se preze Heisenbugdebaixo strace prefere não reproduzir.

Então o que está acontecendo?

  • Utilitário head tem o direito (ou melhor, é forçado) de fechar o canal que está sendo lido assim que ler o número de linhas solicitado.
  • O escritor do programa gerador de dados (neste caso cc) lata imprimir várias linhas e livre faça isso por meio de várias chamadas write().
  • Se o leitor terá tempo de fechar o canal do seu lado antes do final da gravação do lado do escritor, então o escritor receberá um erro.
  • Programa escritor tem o direito ambos ignoram o erro de gravação do canal e o refletem em seu código de conclusão.
  • Devido à instalação set -o pipefail o código de conclusão do pipeline será diferente de zero (errôneo) se o resultado for diferente de zero de pelo menos um elemento, e então funcionará || echo "Please use GCC or CLANG compatible compiler".

Pode haver variações dependendo de como o programa gravador trabalha com os sinais. Por exemplo, o programa pode terminar de forma anormal (com geração automática de um status de encerramento diferente de zero/erro), ou write() retornará o resultado da gravação de menos bytes do que o solicitado e definirá errno = EPIPE.

Quem é o culpado?

No caso descrito um pouco de tudo. Tratamento de erros em cc (lcc:1.23.20:Sep—4-2019:e2k-v3-linux) não é redundante. Em muitos casos, é melhor agir com cautela, embora isso exponha falhas repentinas em um padrão projetado para o comportamento tradicional.

O que fazer?

Errado:

fortune | head -1 && echo "Повезло, но вы рискуете!" || echo "WTF?"

Correto:

  1. (fortune && echo "Успешно" || echo "Ошибка") | head -1

    Aqui, o fechamento antecipado de um pipe será tratado pelo interpretador de comandos ao atender o pipeline aninhado ("dentro" dos parênteses). Assim, se fortune reportará um erro ao escrever para um canal fechado no status, então a saída || echo "Ошибка" não vai chegar a lugar nenhum, pois o canal já está fechado.

  2. fortune | cat - | head -1 && echo "Успешно" || echo "Ошибка"

    Aqui está o utilitário cat atua como um amortecedor porque ignora o erro EPIPE após a retirada. Isso é o suficiente por enquanto conclusão fortune pequeno (várias linhas) e cabe no buffer do canal (de 512 bytes a ≈64K, na maioria dos sistemas operacionais ≥4K). Caso contrário, o problema poderá retornar.

Como processar corretamente EPIPE e outros erros de gravação?

Não existe uma solução única e certa, mas existem recomendações simples:

  • EPIPE necessário deve ser processado (e refletido no status de saída) ao gerar dados que requerem integridade. Por exemplo, durante a operação de arquivadores ou utilitários de backup.
  • EPIPE é melhor ignorar ao exibir informações e mensagens auxiliares. Por exemplo, ao exibir informações sobre opções --help ou --version.
  • Se o código que está sendo desenvolvido puder ser usado em um pipeline antes | headem seguida EPIPE É melhor ignorar, caso contrário é melhor processar e refletir no status de saída.

Gostaria de aproveitar esta oportunidade para expressar minha gratidão às equipes MCST и Alt LinuxName para um excelente trabalho produtivo. Sua determinação é incrível!
Continuem assim, camaradas, reuniões no outono!

Obrigado berez para corrigir erros de digitação e erros.
KDPV de Georgy A.

Fonte: habr.com

Adicionar um comentário