web-dev-qa-db-fra.com

Pourquoi stash/unstash ne fonctionne pas dans ce Jenkinsfile?

J'ai un serveur Jenkins qui fonctionne sur site et qui utilise un fichier Jenkins pour gérer un pipeline qui utilise le plug-in executor test parallèle pour exécuter tous mes tests JUnit sur plusieurs agents afin d'accélérer le test. Nous avons un serveur lame que nous avons fabriqué (bien moins cher qu’en acheter un!) Et nos tests sont passés de près de 2 heures à 22 minutes. Le plugin JUnit fonctionne très bien avec des tests en parallèle.

Le plugin Jacoco ne le fait cependant pas. J'essaie donc de fusionner les fichiers de couverture en un fichier afin que le plug-in Jacoco puisse publier les résultats de la couverture. Stash/unstash fonctionne dans le stockage des sources, mais ne fonctionne pas lorsque j'essaie de stocker les différents fichiers de sortie Jacoco pour les libérer dans le maître.

Des idées pourquoi?

Voici mon Jenkinsfile:

#!/usr/bin/env groovy

def branch
def hash

node('remote') {
  sh 'echo starting'

  branch = env.gitlabBranch ?: '**'
  echo "Branch: $branch"

  checkout([$class: 'GitSCM',
        branches: [[name: "$branch"]],
        extensions: [
          [$class: 'PruneStaleBranch'],
          [$class: 'CheckoutOption', timeout: 120],
          [$class: 'CloneOption', depth: 0, noTags: true, shallow: true, timeout: 180]
        ],
        doGenerateSubmoduleConfigurations: false,
        submoduleCfg: [],
        userRemoteConfigs: [[credentialsId: 'gitlabLabptop', url: '[email protected]:protocase/my_project_url.git']]
       ]
      )

  hash = sh (script: 'git rev-parse HEAD', returnStdout: true).trim()

  ### - this stash works fine -###
  stash name: 'sources', includes: '**', excludes: '**/.git,**/.git/**'
}

def numBranches = 9
def splits = splitTests count(numBranches)
def branches = [:]

for (int i = 0; i < splits.size(); i++) {
  def index = i // fresh variable per iteration; i will be mutated

  branches["split${i}"] = {
    timeout(time: 125, unit: 'MINUTES') {
      node('remote') {
    sh 'echo starting a node'
    deleteDir()

    ### - this unstash works fine - ###
    unstash 'sources'

    def exclusions = splits.get(index);
    writeFile file: 'test/exclusions.txt', text: exclusions.join("\n")

    sh 'ant clean'

    sh 'rm -rf build'

    sh 'ant jar'

    sh 'ant -buildfile build-test.xml buildTests'

    sh 'ant -buildfile build-test.xml jenkinsBatch'

    junit 'build/test/results/*.xml'

    sh "mv build/test/jacoco/jacoco.exec build/test/jacoco/jacoco${index}.exec"
    echo "name: coverage$index, unclude jacoco${index}"

       ### - this stash appears to work - ### 
       stash name: "coverage$index", includes: "build/test/jacoco/jacoco${index}.exec"
       echo "stashed"

      }
    }
  }
}

parallel branches


def branchIndecis = 0..numBranches

node('master') {
  if (currentBuild.result != "ABORTED") {

    echo "collecting exec files"

    branchIndecis.each {
      echo "unstash coverage${it}"

      ### !!! this unstash causes an error !!! ###
      unstash name: "coverage${it}"



      echo "make file name"
      def coverageFileName = "build/test/jacoco/jacoco${it}.exec"
      echo "merge file"
      sh "ant -buildfile build-test.xml -Dfile=${coverageFileName} coverageMerge"
    }

    echo "collected exec files"

    step([$class: 'JacocoPublisher',
      execPattern:'build/test/jacoco/jacoco.exec',
      classPattern: 'build/classes',
      sourcePattern: 'src'])

    echo "finishing ${branch} - ${hash}"

  }
}

le résultat obtenu est:

[split7] [jdesigner] Running Shell script
[split7] + mv build/test/jacoco/jacoco.exec build/test/jacoco/jacoco7.exec
[Pipeline] [split7] echo
[split7] name: coverage7, unclude jacoco7
[Pipeline] [split7] stash
[split7] Stashed 1 file(s)
[Pipeline] [split7] echo
[split7] stashed
[Pipeline] [split7] }
[Pipeline] [split7] // node
[Pipeline] [split7] }
[Pipeline] [split7] // timeout
[Pipeline] [split7] }
[Pipeline] // parallel
[Pipeline] node
Running on eightyeight in /var/jenkins/workspace/jdesigner
[Pipeline] {
[Pipeline] echo
collecting exec files
[Pipeline] echo
unstash coverage0
[Pipeline] unstash
[Pipeline] }
[Pipeline] End of Pipeline
Finished: FAILURE

[edit] la réserve pour la couverture0 est

[split0] Recording test results
[Pipeline] [split0] sh
[split0] [jdesigner] Running Shell script
[split0] + mv build/test/jacoco/jacoco.exec build/test/jacoco/jacoco0.exec
[Pipeline] [split0] echo
[split0] name: coverage0, include jacoco0
[Pipeline] [split0] stash
[split0] Stashed 1 file(s)
[Pipeline] [split0] echo
[split0] stashed
[Pipeline] [split0] }
[Pipeline] [split0] // node
[Pipeline] [split0] }
[Pipeline] [split0] // timeout
[Pipeline] [split0] }
[split3]     [junit] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 18.737 sec
[split3]     [junit] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 18.737 sec

notez la ligne 

[split0] name: coverage0, include jacoco0

est juste ma déclaration echo où je répète le nom de cette partie du script:

    sh "mv build/test/jacoco/jacoco.exec build/test/jacoco/jacoco${index}.exec"
    echo "name: coverage$index, include jacoco${index}"

    stash name: "coverage$index", includes: "build/test/jacoco/jacoco${index}.exec"
    echo "stashed"

Notez que le stockage réel n'est pas effectué sur le nœud, il est répertorié en tant que pipeline même s'il est effectué sur un nœud distant. J'ai vu des choses qui indiquent que le stash est fait sur le maître mais pas vraiment où ce répertoire est situé.

[[SUPPLEMENT EDIT]] - merci à eis pour les recommandations.

Le répertoire jobs/jdesigner/builds/1639/stashes/du maître contient des fichiers de couverture # .tar.gz qui incluent les fichiers jacoco # .exec appropriés. Quand je mets un piège à essayer autour du désincarcéré:

try {
    unstash name: "coverage${it}"
} catch (error) {
    echo "error unstashing: ${error}"
}

le résultat obtenu est:

collecting exec files
[Pipeline] echo
unstash coverage0
[Pipeline] unstash
[Pipeline] echo
error unstashing: Java.io.NotSerializableException: groovy.lang.IntRange
[Pipeline] echo
make file name
5
vextorspace

TLDR: il s’agissait là d’un problème où le style itératif était à l’origine du problème, puisque la clé it utilisée n’était pas Serializable.

Ce qui rend le débogage difficile, c’est que le message d’erreur n’a pas été correctement signalé, probablement en raison de this issue . La capture de l'exception dans le code et la génération de rapports "manuels" ont résolu le problème.

Le problème réel a été résolu en utilisant des clés Serializable.


Version plus longue:

Depuis dans votre exemple cela fonctionne:

node('remote') {
    ### - this stash works fine -###
    stash name: 'sources', includes: '**', excludes: '**/.git,**/.git/**'
}
node('remote') {    
    ### - this unstash works fine - ###
    unstash 'sources'
}

Mais cela ne veut pas:

node('remote') {

   ### - this stash appears to work - ### 
   stash name: "coverage$index", includes: "build/test/jacoco/jacoco${index}.exec"
   echo "stashed"

}
node('master') {
   echo "unstash coverage${it}"

   ### !!! this unstash causes an error !!! ###
   unstash name: "coverage${it}"
}

Au départ, je pensais que celui qui travaillait est stocké et non stocké sur votre nœud distant, tandis que celui qui ne fonctionne pas est stocké sur votre nœud distant, mais vous essayez de le désarchiver sur votre nœud maître (où il ne sera naturellement pas trouvé).

Cependant, ce n'était pas le cas. Selon this

Lorsque vous cachez un fichier sur un esclave, les fichiers sont envoyés au maître . Les fichiers seront stockés dans le dossier Job, dans la version associée dossier sous le dossier stash. Chaque cachette sera stockée en tant que goudron fichier. Ces fichiers sont supprimés à la fin de la construction.

La séparation maître-distance ne devrait donc pas faire la différence. En outre, s’il s’agissait d’une cachette introuvable, vous pouvez voir dans les sources qu’il échouerait avec "No such saved stash ‘" + name + "’, car, selon AbortException javadoc ", le message spécifié sera signalé lorsque cette exception sera interceptée. . ". Cela ne se produit clairement pas.

Au lieu de cela, on devrait déboguer en utilisant un bloc try-catch pour découvrir quelle est la véritable exception qui casse la construction.

Pour expliquer pourquoi il n'est pas signalé correctement par défaut, il s'agit de this issue : "Une erreur de sérialisation à la fin du flux n'a pas été signalée correctement dans le journal de génération, mais uniquement dans le journal Jenkins". Le rapport de bogue affirme que son problème est "corrigé", mais apparemment uniquement parce que, dans les nouvelles versions, certains tests de ce comportement n'ont pas déclenché le problème et qu'il est donc possible qu'il existe encore.

Avec le message d'erreur capturé, on pouvait voir que le problème était this - nous essayions de sérialiser une clé non sérialisable lorsque nous la passions.

6
eis

Deux possibilités me paraissent lors de l'utilisation de processus parallèles dans Jenkins:

  1. Vous essayez peut-être de décompresser dans un (ou plusieurs) processus avant que stash name: "coverage$index", includes: "build/test/jacoco/jacoco${index}.exec" ne soit terminé sur le noeud master

  2. Vous pourriez avoir des conflits de noms entre les processus.

Pour expliquer (2):

Le processus 1 crée une réserve nommée stashed_files

Le processus 2 stocke sous le même nom, stashed_files, puis élimine avec succès. stashed_files est supprimé.

Le processus 1 tente de décompresser stashed_files. Une erreur est générée lors de l'annulation de la sauvegarde, car stashed_files a été supprimé par le processus 2.

Du code Groovy utile pour contourner ce problème peut être trouvé dans cette question .

0
Nick