diff --git a/pipeline-examples/parallel-with-limits/README.md b/pipeline-examples/parallel-with-limits/README.md new file mode 100644 index 0000000..110207f --- /dev/null +++ b/pipeline-examples/parallel-with-limits/README.md @@ -0,0 +1,18 @@ +# Synopsis +Demonstrate an example of a Jenkinsfile to execute a large number of jobs in parallel, while +limiting the total number of jobs that can run concurrently + +# Background + +The requirement was to execute over 200 long running free style jobs with a limited number of resources. With 40 threads, it took 13 hours to complete the execution of all jobs +- Multiples of this pipeline needed to run during the day on different branches +- It was necessary to prevent a single pipeline from consuming all available threads at one time +- It was required that users be allowed stop the pipeline and all child jobs easily +- It was required to dynamically build the list of jobs to execute. Jobs are added and removed frequently +- It was desired to keep to a minumum the number jobs added to the Jenkins job queue at one time +- All jobs run in a shared pool of VMs used by different teams. It is not possible to manually assign and maintain labels for each team. VMs must be able to be added and removed from the shared pool as needed. +- The Throttle plugin was used for many years, but the performance of the plugin became an issue as the number of nodes grew past 50 and the number of jobs exceded several hundreds + + +Note: other locking/limiting mechanisms were explored, but the Lockable Resources plugin ended up being the most elegant solution. +If the Lockable Resources plugin allowed the creation of multiple ephemeral resources on the fly, the sample pipeline would be even simpler. At the moment this does not work: lock(label: "mylabel", quantity: 5){} \ No newline at end of file diff --git a/pipeline-examples/parallel-with-limits/parallelWithLimits.groovy b/pipeline-examples/parallel-with-limits/parallelWithLimits.groovy new file mode 100644 index 0000000..a788f87 --- /dev/null +++ b/pipeline-examples/parallel-with-limits/parallelWithLimits.groovy @@ -0,0 +1,66 @@ +/* +This pipeline will execute all jobs in the folder that contains it. +This pipeline uses the LockableResources plugin to control how many parallel jobs run at the same time. +The maximum number of jobs that can run at the same time is specified by the optional RESOURCES parameter. +The optional LABEL parameter can be used to customize how resources are grouped. +If this job is stopped, all child jobs started by this job will be stopped by Jenkins. + */ + +import org.jenkins.plugins.lockableresources.LockableResourcesManager + +node { + // the label of all resources used by this pipeline + def label = params.getOrDefault('LABEL', currentBuild.rawBuild.project.parent.name) + println "LABEL: $label" + + // number of resources to be used by this pipeline + def resources = Integer.valueOf(params.getOrDefault('EXECUTORS', 5)) + println "RESOURCES: $resources" + + // the list of stages that will be executed in parallel + def parallelStages = [:] + + stage('Prepare') { + // setup lockable resources needed for this pipeline + setupLockableResources(label, resources) + + // list of jobs to be executed + def jobs = currentBuild.rawBuild.project.parent.items.findAll({ + project -> project.name != currentBuild.rawBuild.project.name && !project.isDisabled() + }).collect { job -> return job.name } + + println "Total jobs to run: ${jobs.size()}" + + jobs.each() { job -> + parallelStages[job] = { + stage(job) { + // Lock one of the resources assigned the specified label + lock(label: label, quantity: 1) { + build(job: job, wait: true, propagate: false, quietPeriod: 10) + } + } + } + } + } + + stage('Run') { + // run all stages in parallel + parallel parallelStages + } +} + +// Setup lockable ephemeral resources that will be deleted after the pipeline completes. +// Ephemeral resources are not configurable via the Global Configuration screen, +// this is an advantage when working with large number of resources. +@NonCPS +def setupLockableResources(label, executors) { + println "Setting up lockable resources for this build" + def lrm = LockableResourcesManager.get() + for (i = 0; i < executors; i++) { + lrm.createResourceWithLabel("${label}-$i", label) + // Get lockable resource by name + def lockableResource = lrm.fromName("${label}-$i") + // Make resource ephemeral, resources will be automatically deleted when this pipeline ends + lockableResource.setEphemeral(true) + } +}