Dois problemas num Jenkinsfile

Ah, as sutilezas do shell script…

Já usei vários servidores Jenkins aqui na Liferay, sempre como desenvolvedor. Na Liferay Cloud, porém, configurar e rodar Jenkins é uma tarefa rotineira. Resolvi então reforçar meu conhecimento seguindo os passos de Guided Tour to Jenkins. Foi quando aconteceu uma coisa curiosa.

O processo de build no Jenkins é definidos por arquivos Jenkinsfile. Um Jenkinsfile contém, entre outras coisas, vários estágios do processo de build. Cada estágio possui um ou mais passos (isto é, comandos a serem executados).

A sessão sobre passos múltiplos me apresentou o retry (que executa um um passo até ele ser bem-sucedido) e timeout (que limita o tempo que um passo pode tomar). Para testá-los, escrevi o seguinte no meu Jenkinsfile:

pipeline {
  agent any
  stages {
    stage('build') {
      steps {
        retry(3) {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            echo trying flakey... 
            [ $((R % 3)) -eq 0 ] && exit 1
          '''
        }
        timeout(time: 5, unit: 'SECONDS') {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            sh 'echo trying slow...
                      [ $((R % 3)) -eq 0 ] && sleep 10
          '''
         }
       }
    }
  }
}Code language: PHP (php)

Mesmo que você não conheça Jenkinsfiles, é possível ter uma ideia do que se passa:

  • No retry(3), coloquei um script shell que, a cada três execuções, falha. Assim, poderia ver Jenkins fazendo novas tentativas de executá-lo.
  • No passo de timeout(5, SECONDS) há um script que, a cada três execuções, demora dez segundos para terminar. Como há um tempo-limite de 5 segundos, essas execuções demoradas vão falhar.

Mandei o aquivo para GitHub configurei Jenkins para “buildar” o repositório e… todos os builds falharam! Isto não estava certo, eu esperava que o build falhasse pouco mais de um terço das vezes. O que estava acontecendo?

O problema é que, na minha cabeça, a condição do teste [ $((R % 3)) -eq 0 ] && exit 1 seria verdadeira um terço das vezes, fazendo com que o script encerrasse com status 1. (Todo status não-nulo é considerado falha.) Só que, quando a condição era falsa, o próprio teste falhava. Era o status do teste que Jenkins verificava!

Entendido o problema, foi fácil resolver: adicionei, ao final do script, um operador OU. Por quê? Porque se o teste passar, o exit 1 é executado e o script falha; se o teste não passar, porém, o sh vai executar o comando depois do ||, que sempre vai retornar zero.

pipeline {
  agent any
  stages {
    stage('build') {
      steps {
        retry(3) {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            echo trying flakey... 
            [ $((R % 3)) -eq 0 ] && exit 1 || echo 'flakey ok'
          '''
        }
        timeout(time: 5, unit: 'SECONDS') {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            sh 'echo trying slow...
            [ $((R % 3)) -eq 0 ] && sleep 10 || echo 'slow ok'
          '''
        }
      }
    }
  }
}Code language: PHP (php)

Funcionou! Exceto que agora o build raramente falhava, e nunca por tempo excedido. Por que será?

É que concatenei || echo 'slow ok' também na frente do passo do timeout. O script desse passo de fato levava 10 segundos para executar um terço das vezes, e Jenkins de fato cancelava o script.

O problema é que Jenkins “mata” o comando sleep 10, que então retorna um status de erro. Só que o echo 'slow ok' também “come” esse status de falha quando o build é cancelado. Por isso, mesmo quando abortado, o build aparecia como bem-sucedido. Por outro lado, eu não poderia simplesmente deixar o teste falhar, como antes.

A solução é aquela tão comum: parar de usar truquezinhos engenhosos e escrever código claro. Troquei os && e || por um condicional if. Convenhamos que até mais legível ficou, né?

pipeline {
  agent any
  stages {
    stage('build') {
      steps {
        retry(3) {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            echo trying flakey... 
            [ $((R % 3)) -eq 0 ] && exit 1 || echo 'flakey ok'
          '''
        }
        timeout(time: 5, unit: 'SECONDS') {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            sh 'echo trying slow...
            if [ $((R % 3)) -eq 0 ] ; then 
              sleep 10
            fi
          '''
        }
      }
    }
  }
}Code language: PHP (php)

Às vezes sinto que estamos na era CVS das ferramentas de CI, aguardando um Subversion que simplifique nossa vida. Mas confesso estar feliz que tenhamos, hoje essas ferramentas disponíveis. Por ora, vou focando em aprender com esses incidentes.

Post Revisions:

This post has not been revised since publication.

Post a Comment

Your email is never shared. Required fields are marked *

*
*

%d bloggers like this: