Quicksort with Jenkins for Enjoyable and No Revenue
By Susam Pal on 25 Jan 2024
I first encountered Jenkins in 2007 whereas contributing to the Apache
Nutch challenge. It was known as Hudson again then. The nightly builds
for the challenge ran on Hudson at the moment. I bear in mind sifting
by my emails and reviewing construct end result notifications to maintain
a watch on the patches that acquired merged into the trunk on a regular basis. Sure,
patches and trunk! We had been nonetheless utilizing SVN again then. Hudson was
renamed to Jenkins in 2011.
Since model 2.0 (launched on 20 Apr 2016), Jenkins helps
pipeline scripts written in Groovy as a first-class entity. A
pipeline script successfully defines the construct job. It could possibly outline
construct properties, construct phases, construct steps, and many others. It could possibly even
invoke different construct jobs, together with itself.
Wait! If a pipeline can invoke itself, can we, maybe, clear up a
recursive drawback with it? Completely! That is exactly what we
are going to do on this submit. We’re going to implement quicksort
as a Jenkins pipeline for enjoyable and never a whit of revenue!
Run Jenkins
Before we get started, I need to tell you how to set up Jenkins just
enough to try the experiments presented later in this post on your
local system. This could be useful if you have never used Jenkins
before or if you do not have a Jenkins instance available with you
right now. If you are already well-versed in Jenkins and have an
instance at your disposal, feel free to skip ahead directly to
the Quicksort part.
The steps under assume a Debian GNU/Linux system. Nonetheless, it
ought to be attainable to do that on any working system so long as you
can run Docker containers. Since software program evolves over time, let me
word down the variations of software program instruments I’m utilizing whereas writing
this submit. Right here they’re:
- Debian GNU/Linux 12.4 (bookworm)
- Docker model 20.10.24+dfsg1, construct 297e128
- Docker picture tagged jenkins/jenkins:2.426.3-lts-jdk17
- Jenkins 2.426.3
We shall be performing solely quick-and-dirty experiments on this submit,
so we don’t want a production-grade Jenkins occasion. We are going to run
Jenkins briefly in a container. The next steps present how one can
do that and how one can configure Jenkins for the upcoming experiments:
-
Set up Docker if it isn’t already current on the system. For
instance, on a Debian system, the next command installs
Docker:sudo apt-get set up docker.io
-
Now run the Jenkins container with this command:
sudo docker run --rm -p 8080:8080 jenkins/jenkins:lts
-
When the container begins, it prints a password in direction of the
backside of the logs. Copy the password. -
Go to http://localhost:8080/
in an online browser. When the Unlock Jenkins web page
seems, paste the password and click on Proceed. -
On the Customise Jenkins web page, click on Set up
instructed plugins. Alternatively, to keep away from putting in
pointless plugins, click on Choose plugins to put in,
deselect all the things besides Pipeline, and
click on Set up. We’d like the pipeline plugin to carry out
remainder of the experiment specified by this submit. -
On the Create First Admin Consumer web page, enter the small print
to create a brand new consumer. -
On the Occasion Configuration web page, click on Save and
End. -
The Jenkins is prepared! web page seems. Click on Begin
utilizing Jenkins. -
Go to Construct Executor Standing > Constructed-In Node
> Configure and alter Variety of executors
from the default worth of2
to10
.
Click on Save.
Good day World
The following steps show how to run your first Jenkins pipeline:
-
Go to Dashboard > New Item. Enter an item
name, say,hello
, select Pipeline, and
click OK. -
On the next page, scroll down to the Pipeline section
at the bottom and paste the following pipeline script and
click Save.node { echo "hello, world" }
-
Now click Build Now. A new build number appears at the
bottom half of the left sidebar. Click on the build number,
then click Console Output to see the output of the
pipeline. Thehello, world
message should be
present in the output.
To edit the pipeline script anytime, go to Dashboard, click
on the pipeline, then go to Configure, scroll down to
the Pipeline section, edit the script, and
click Save.
In real world software development, Jenkins is typically configured
to automatically pull some source code from a project repository
maintained under a version control system and then build it using
the pipeline script found in the file named Jenkinsfile
present at the top-level directory of the project. But since we
only intend to perform fun experiments in this post, we will just
paste our pipeline script directly into the pipeline configuration
page on Jenkins as explained above in order to keep things simple.
Jenkins also supports another way of writing pipelines using a
declarative style. They are known as declarative
pipelines. In this post, however, we will be writing
only scripted pipelines so that we can write simple Groovy
code for our experiments without having to bother about too many
pipeline-specific notions like stages, steps, etc.
Factorial
Now let us write a simple pipeline that calculates the factorial of
a nonnegative integer. This will help us to demonstrate how a build
job can recursively call itself. We are not going to write
something like the following:
properties([
parameters([
string(
name: 'INPUT',
defaultValue: '0',
description: 'A nonnegative integer'
)
])
])
def factorial(n) {
return n == 0 ? 1 : n * factorial(n - 1)
}
node {
echo "${factorial(params.INPUT as int)}"
}
The code above is an example of a function that calls itself
recursively. However, we want the build job (not the
function) to call itself recursively. So we write the following
instead:
properties([
parameters([
string(
name: 'INPUT',
defaultValue: '0',
description: 'A nonnegative integer'
)
])
])
def MAX_INPUT = 10
node {
echo "INPUT: ${params.INPUT}"
currentBuild.description = "${params.INPUT} -> ..."
def n = params.INPUT as int
if (n > MAX_INPUT) {
echo "ERROR: Input must not be greater than ${MAX_INPUT}"
}
env.RESULT = n == 0 ? 1 : n * (
build(
job: env.JOB_NAME,
parameters: [string(name: 'INPUT', value: "${n - 1}")]
).getBuildVariables().RESULT as int
)
echo "RESULT: ${env.RESULT}"
currentBuild.description = "${params.INPUT} -> ${env.RESULT}"
}
This code example demonstrates a few things worth noting:
-
The
properties
step at the top sets up a build
parameter namedINPUT
with a default value
of0
. This will allow us to enter an input number
while building the job. -
Within the
node
block, we first check that the
input is not too large. If the input number is larger than 10,
the pipeline refuses to run. This is just a tiny safety check
to prevent the overzealous among you from inadvertently causing
havoc in your Jenkins instance by triggering a job with a large
input and depleting all the executors with an excess of
recursive jobs. -
Then we perform the classic recursion to compute the factorial
of a given nonnegative integer. The only thing that may appear
unusual here is that instead of just writingfactorial(n -
we make a
1)build()
call to invoke the job
itself recursively and passn - 1
as a build
parameter input to that job. -
Each recursively called job writes its output to an environment
variable namedRESULT
and exits. Then the
higher-level job invocation looks up the environment variables
in the build result of the job that just finished with
thegetBuildVariables()
call, reads the
RESULT
variable, and multiplies the value found
there byn
. -
The lines that update
currentBuild.description
are
there only to show handy descriptions of what is going on (the
input and the result) in the build history that appears on the
left sidebar. A screenshot presented later illustrates this.
To run the above pipeline, perform the following steps on the
Jenkins instance:
-
Go to Dashboard > New Item. Enter an item
name, say,factorial
, select Pipeline, and
click OK. -
On the next page, scroll down to the Pipeline section
at the bottom and paste the pipeline script code presented
above. -
Click Build Now. The first build sets
theINPUT
build parameter to0
(the
default value specified in the pipeline script). The
result1
shoud appear in the Console
Output page. -
After the first build completes, the Build Now option on
the left sidebar gets replaced with the Build with
Parameters option. Click it, then enter a number,
say,5
and click Build. Now we should see
Jenkins recursively triggering a total of 6 build jobs and each
build job printing the factorial of the integer it receives as
input. The top-level build job prints120
as its
result.
Here is a screenshot that shows what the build history looks like on
the left sidebar:
In the screenshot above, build number 2 is the build we triggered to
compute the factorial of 5. This build resulted in recursively
triggering five more builds which we see as build numbers 3 to 7.
The little input and output numbers displayed below each build
number comes from the currentBuild.description
value we
set in the pipeline script.
If we click on build number 7, we find this on the build page:
This was a simple pipeline that demonstrates how a build job can
trigger itself, pass input to the triggered build and retrieve its
output. We did not do much error checking or handling. We have
kept the code as simple as reasonably possible. The focus here was
only on demonstrating the recursion.
Quicksort
Now we will implement quicksort in Jenkins. Sorting numbers using
the standard library is quite straightforward in Groovy. Here is an
example in the form of Jenkins pipeline:
properties([
parameters([
string(
name: 'INPUT',
defaultValue: '4, 3, 5, 4, 5, 8, 7, 9, 1',
description: 'Comma-separated list of integers'
)
])
])
node {
def numbers = params.INPUT.split('s*,s*').collect {it as int}
echo "${numbers.sort()}"
}
It can’t get simpler than this. However, we are not here to
demonstrate the standard library methods. We are here to
demonstrate recursion in Jenkins! We write the following pipeline
script instead:
properties([
parameters([
string(
name: 'INPUT',
defaultValue: '4, 3, 5, 4, 5, 8, 7, 9, 1',
description: 'Comma-separated list of integers'
)
])
])
def MAX_INPUT_SIZE = 10
node {
echo "INPUT: ${params.INPUT}"
currentBuild.description = "${params.INPUT} -> ..."
def numbers = params.INPUT.split('s*,s*').collect {it as int}
if (numbers.size() > MAX_INPUT_SIZE) {
echo "ERROR: Input must not contain more than ${MAX_INPUT_SIZE} integers"
}
def pivot = numbers[0]
def others = numbers.drop(1)
def lo = others.findAll { it <= pivot }
def hi = others.findAll { it > pivot }
def builds = [:]
def results = [lo: [], hi: []]
if (lo) {
builds.lo = {
results.lo = build(
job: env.JOB_NAME,
parameters: [string(name: 'INPUT', value: lo.join(', '))
]).getBuildVariables().RESULT.split('s*,s*') as List
}
}
if (hi) {
builds.hi = {
results.hi = build(
job: env.JOB_NAME,
parameters: [string(name: 'INPUT', value: hi.join(', '))
]).getBuildVariables().RESULT.split('s*,s*') as List
}
}
parallel builds
env.RESULT = (results.lo + [pivot] + results.hi).join(', ')
echo "RESULT: ${env.RESULT}"
currentBuild.description = "${params.INPUT} -> ${env.RESULT}"
}
Some of the code is similar to the one in the previous section. For
example, the properties
step to set up the build
parameter, the build()
call, setting the result
in env.RESULT
, etc. should look familiar. Let us pay
attention to what is different.
Firstly, we have two build()
calls instead of just one.
In fact, we have two closures with one build()
call in
each closure. Then we use the parallel
step to execute
both these closures in parallel. In each build job, we pick the
first integer in the input as the pivot, then compare all the
remaining integers with this pivot and separate them
into lo
(low numbers) and hi
(high
numbers). Then we call the build job recursively to repeat this
algorithm twice: once on the low numbers and again on the high
numbers.
Unlike most textbook implementations of quicksort which lets the
recursion run all the way to the base case in which an empty list is
received and the recursive call returns without doing anything, the
above implementation is slightly optimised to avoid making recursive
builds when we find that the list of low numbers or the list of high
numbers is empty. We lose a little bit of simplicity by doing this
but it helps in avoiding wasteful build jobs that just receive an
empty list of numbers as input and exit without doing anything
meaningful.
I hope this was fun!