Groovy Gotchas – Loops, Closures, and Jenkins DSLs

(Image (C) Tai Kedzierski)

I used to be making an attempt to grasp how some syntax in Groovy was working and it led me to some higher udnerstanding on how closures can be utilized. I am positive it is all within the documentation however… properly I am the sort of individual wh actually must see it in place to grasp…



Listing and map “comprehensions”

Borrowing from Python, I used to be making an attempt to grasp the right way to carry out “checklist comprehension” and “dict comprehension”, however in Groovy. An article I found did give me the reply, and I seen one thing a just a little odd:

// Listing comprehension to double the values of every merchandise
my_list = my_list.gather({ merchandise ->
 merchandise * 2 // the final assertion of the block is the merchandise that will get "returned" for every merchandise
})

// Map comprehension so as to add ".txt" suffix to every entry
my_map = my_map.inject([:]) { new_map, merchandise ->
    new_map[item.getKey()] = "${merchandise.getValue()}.txt"
    new_map // the returnable should be the ultimate assertion
}
Enter fullscreen mode

Exit fullscreen mode

(for particulars, do see the original post)

Two issues that I seen:

  • Within the checklist comprehension, the curly block is a direct argument to .gather() – however within the map comprehension, the curly block is positioned after the decision to .inject()
  • In each circumstances, the block seems to be uspiciously just like the .every { title -> operations; } notation

So I did what any system-dismantling youngster would do… and tried one thing …

def myfunc(message, factor) { println factor }

myfunc("Hey") { println "different motion" }
Enter fullscreen mode

Exit fullscreen mode

This confirmed me that factor did truly populate…. with a closure !

basic_closure$_run_closure1@76f2bbc1
Enter fullscreen mode

Exit fullscreen mode

And thus my journey right down to Wonderland bgan …



.every { } will not be a real loop

Notationally, it seems to be like utilizing the every { } operation on an iterable merchandise leads to a loop that steps by every merchandise.

Nicely, not likely: the every() perform (which, sure, it’s) actually takes one argument: a perform handler, and calls it as soon as for every merchandise within the iterable it operates from.

For this reason this won’t work:

def error_on_null(my_sqeuence) {
    my_sequence.every { merchandise ->
        if(merchandise == null)
            break // Groovy will inform you that "break" should be utilized in a loop!
    }
}
Enter fullscreen mode

Exit fullscreen mode

If I attempt to write the above in Python, that is what’s truly taking place:


# For context - assume a Sequence sort
# that implements the "every" perform, like:

#class Sequence(checklist):
#    def every(self, operation):
#        for v in self: # self, the `checklist`
#            operation(v)


def error_on_null(my_sequence):
    def __is_null(merchandise):
        if merchandise is None:
            break # we're not instantly in a loop on this scope

    my_sequence.every(__is_null)
Enter fullscreen mode

Exit fullscreen mode

We are able to see extra explicitly on this manner what is occurring: the content material that checks null-ness is definitely in a block and scope of its personal – it doesn’t incorporate its personal loop, and so it’s a syntactic error to attempt to use break there.

When utilizing curly braces for a code block, we are literally defining an nameless perform, and passing it alongside to the every() perform which itself implements the loop. This nameless perform is what is known in Groovy as a closure, a bit of code declared in a single scope, and executed anyplace else, most likely at a deferred time.

Equally, with .gather( {} ) we’re passing a closure, that may then be referred to as by .gather‘s inner logic.



Closure parameters

Closures can have parameters too.

def greet = { greeting, title ->
  println "$greeting , $title"
}

greet("Hey", "Tam")
Enter fullscreen mode

Exit fullscreen mode

And that is the way you get the title -> notation within the .every { } name we’re a lot extra aware of.



Area Particular Language: Jenkinsfile

I at all times did surprise how Jenkinsfile pipelines declared its personal code blocks like

stage("Construct stuff") {
    sh "make clear && make && make set up"
}
Enter fullscreen mode

Exit fullscreen mode

It seems, the stage() perform is outlined one thing like this

def stage(stage_name, operation) {
    org.hudson.and so forth.setStageName(stage_name) // for instance
    operation()
}
Enter fullscreen mode

Exit fullscreen mode

When stage() known as in my Jenkinsfile, it receives the closure I provide after it as an operation to carry out. My closure (the construct steps) has entry to the variables and namespace in the remainder of the file – and so could be handed alongside to the Jenkins-level stage() perform which proceeds then to calling it (most likely wrapped round some extra complicated error-handling logic).



The Closure Gotcha

Beforehand I posted a couple of behaviour I didn’t perceive the place variables have been seeingly interpolated on the final potential second. The explanation was, after all, due to closures !

I managed to copy the difficulty with the next snippet:

// Mock Jenkinsfile directives

def string(opts) {
    def title = opts.get("title")
    def worth = opts.get("worth")
    return (String) "${title} from ${worth}"
}

def nodesByLabel(label) {
    return ["agent-001", "agent-002", "agent-003"]
}

def construct(opts) {
    def job = opts.get("job")
    def parameters = opts.get("parameters")
    println "Constructing $job with params $parameters"
}

def parallel(ops_map) {
    for(operation in ops_map) {
        def op = operation.getValue()
        op()
    }
}

// --- My script

prepared_node_tests = [:]

def prepare_all_nodes_for_test(agent_label) {
    def nodelist = nodesByLabel(label: "${agent_label}")

    for (agentName in nodelist) {
        println "Making ready job for " + agentName

        prepared_node_tests[ agentName ] = { // THIS IS A CLOSURE
            construct job: 'Single_build_job',
            parameters: [
                string(name: 'TEST_AGENT_LABEL', value: agentName),
            ]
        }
    }
}


prepare_all_nodes_for_test("custom_label")
parallel prepared_node_tests
Enter fullscreen mode

Exit fullscreen mode

The output:

Making ready job for agent-001
Making ready job for agent-002
Making ready job for agent-003
Constructing Single_build_job with params [TEST_AGENT_LABEL from agent-003]
Constructing Single_build_job with params [TEST_AGENT_LABEL from agent-003]
Constructing Single_build_job with params [TEST_AGENT_LABEL from agent-003]
Enter fullscreen mode

Exit fullscreen mode

The reason being that the closure evaluates at a deferred time, with data of agentName on the time of execution – which is after the loop has accomplished, and so the worth at execution time finally ends up being its final worth from the loop in each case …!

In my resolution in my criticism publish, I moved the closure out a perform, which resulted in it taking the worth with which the perform was referred to as – which stays fixed for every name, and isn’t affected by the loop.

Lastly, a thriller solved!



Extra gotchas

A previous gotcha in the form of GStrings additionally threw me for some time.

One other merchandise I discovered while trawling for solutions not too long ago is how “glabal” variables work in a Groovy file script, and the way code not encapsulated in a perform pertains to variables equally not encapsulated. In essence:

implicit_property = "High degree"
def implicit_local = "Not out there inside capabilities"

println "High degree"
println implicit_property
println implicit_local

def a_func() {
  println "In perform"
  println implicit_property // OK
  println implicit_local // Fail - it's "native" to the "predominant()"
}
Enter fullscreen mode

Exit fullscreen mode

Groovy compiles to Java and so the above truly finally ends up wanting like this:


// assume "println" is a factor

class my_file_name {
  static String implicit_property = "High degree"

  public static void predominant(String[] args) {
    String implicit_local = "Not out there inside capabilities"

    println "High degree"
    println implicit_property
    println implicit_local
  }

  public void a_func() {
    println "In perform"
    println implicit_property // OK
    println implicit_local // Fail - it's "native" to the "predominant()"
  }
}
Enter fullscreen mode

Exit fullscreen mode

…. see what occurred…? 😱



Conclusion

A dive down this rabbit-hole lastly allowed me to make sense of a core a part of the Groovy language , and establish a really attention-grabbing gotcha.

This feels extra of a symptom of closures being very implicit in Groovy – and probably of the false similarities in notation beteen perform declarations, and objects – that closure in my Jenkinsfile appeared to me like an “object” in JavaScript (assume JSON notation) and as such I had not anticipated it to be the supply of the deferred execution.

One other day, one other lesson.

Add a Comment

Your email address will not be published. Required fields are marked *