'Scripted Jenkinsfile executes scheduling on wrong nodes
The system/setup
Here is my setup:
- 1 machine running Jenkins which also provides the
built-innode (formermaster). It has 4 workers assigned to it - 1 FreeBSD machine as remote SSH node with the name
FreeBSD 13.0 x86_64. It has 1 worker - 1 Ubuntu machine as remote SSH node with the name
Ubuntu 20.04 x86_64. It has 1 worker
I would like to have a sequence of Checkout, Build, Test and Upload steps executed serially on each of those FreeBSD 13.0 x86_64 and Ubuntu 20.04 x86_64 nodes. However, I would like each of those nodes to work independently on their list of serial tasks. Something like this:
For this, I have created the following scripted Jenkinsfile
String[] nodesNames = [
'FreeBSD 13.0 x86_64',
'Ubuntu 20.04 x86_64'
]
Map builders = [:]
for (nodeName in nodesNames) {
builders[nodeName] = {
node(nodeName) {
stage('Checkout') {
sh 'sleep 5'
}
stage('Build') {
sh 'sleep 10'
}
stage('Test') {
sh 'sleep 15'
}
stage('Upload') {
sh 'sleep 20'
}
}
}
}
parallel builders
The problem
Jenkins will need an executor to execute the Jenkinsfile itself, which is scheduling the real work. Again, not the work, but the scheduling of the work. And it picks one up from the nodes like this:
Running on Ubuntu 20.04 x86_64 in /home/jenkins/workspace/test
That is the problem: it picks up the wrong node. That node has one executor, and it should never run the scheduling. Scheduling should be done on the built-in node. This will later result in the following message:
Still waiting to schedule task
Waiting for next available executor on ‘Ubuntu 20.04 x86_64’
The outcome
Because one of the 1-worker-node is doing both scheduling and actual work, we will end up with a seemengly "parallel" execution but in fact, is as serial as it can be. Here is a picture taken in the middle of the whole process. Notice how the FreeBSD machine is alternatively doing work then scheduling. As it happens, is giving work to itself. When the work for itself is finished, it will start giving work to Ubuntu.
The solution?
How can one tell Jenkins to execute the Jenkins file itself (the scheduling part) on the built-in node (the former master) and not use a precious worker from the actual remote nodes?
The non-maintainable solution
(Update after initial question and as a response for @MaratC)
We can use declarative syntax. However, it has a major/crippling flaw: imagine one needs to add another machine. It will basically repeat a lot of code. After the 4th-5th machine, it becomes unmaintainable.
pipeline {
agent { node('built-in') }
stages {
stage('Build all') {
parallel {
stage('FreeBSD') {
agent { node('FreeBSD 13.0 x86_64') }
stages {
stage('Checkout') {
steps { sh 'sleep 5' }
}
stage('Build') {
steps { sh 'sleep 10' }
}
stage('Test') {
steps { sh 'sleep 15' }
}
stage('Upload') {
steps { sh 'sleep 20' }
}
}
}
stage('Ubuntu') {
agent { node('Ubuntu 20.04 x86_64') }
stages {
stage('Checkout') {
steps { sh 'sleep 5' }
}
stage('Build') {
steps { sh 'sleep 10' }
}
stage('Test') {
steps { sh 'sleep 15' }
}
stage('Upload') {
steps { sh 'sleep 20' }
}
}
}
}
}
}
}
Solution 1:[1]
First of all, you can combine declarative and scripted syntax (note I didn't check the following code):
pipeline {
agent { node('built-in') }
stages {
stage('Build all') {
script {
def myBuilders = getParallelBuilders()
parallel myBuilders
}
}
}
}
def getParallelBuilders() {
String[] nodesNames = [
'FreeBSD 13.0 x86_64',
'Ubuntu 20.04 x86_64'
]
Map builders = [:].asSynchronized() // don't ask
for (nodeName in nodesNames) {
def final_name = nodeName // don't ask
builders[final_name] = {
node(final_name) {
stage('Checkout') {
sh 'sleep 5'
}
stage('Build') {
sh 'sleep 10'
}
stage('Test') {
sh 'sleep 15'
}
stage('Upload') {
sh 'sleep 20'
}
}
}
return builders
}
But I think that your problem may be solved faster by disallowing the planning code to run on Ubuntu and FreeBSD nodes, by configuring these nodes to only run what is planned to run on the labels (and not just everything). This is achieved by selecting "Only build jobs with label expressions matching this node" in the node configuration screen.
Solution 2:[2]
The solution to original scripted Jenkinsfile question
String[] nodesNames = [
'FreeBSD 13.0 x86_64',
'Ubuntu 20.04 x86_64'
]
def tasks = [:]
def createTask(tasks, title, nodeName) {
tasks[title] = {
stage (title) {
node(nodeName) {
stage('Checkout') {
sh 'hostname; sleep 1'
}
stage('Build') {
sh 'hostname; sleep 2'
}
stage('Test') {
sh 'hostname; sleep 3'
}
stage('Upload') {
sh 'hostname; sleep 4'
}
}
}
}
}
for (nodeName in nodesNames) {
createTask(tasks, nodeName, nodeName)
}
node('built-in') {
parallel tasks
}
It builds everything as expected:
Solution proposed by @MaratC:
(Thank you for the suggestion)
It was slight/minor changed to make it work, and it looks like this:
pipeline {
agent { node('built-in') }
stages {
stage('Build all') {
steps {
script {
def myBuilders = getParallelBuilders()
parallel myBuilders
}
}
}
}
}
def getParallelBuilders() {
String[] nodesNames = [
'FreeBSD 13.0 x86_64',
'Ubuntu 20.04 x86_64'
]
//org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: //
// Scripts not permitted to use staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods asSynchronized java.util.Map
Map builders = [:]//.asSynchronized() // don't ask:
for (nodeName in nodesNames) {
def final_name = nodeName // don't ask
builders[final_name] = {
node(final_name) {
stage('Checkout') {
sh 'hostname; sleep 1'
}
stage('Build') {
sh 'hostname; sleep 2'
}
stage('Test') {
sh 'hostname; sleep 3'
}
stage('Upload') {
sh 'hostname; sleep 4'
}
}
}
}
return builders
}
And the behavior:
Notice that the Checkout step is executed, and the only step is listed in the graph. However, notice the blue ongoing task below. Something is off.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | shiretu |




