调用Jenkins API导致Groovy函数未被CPS转换的问题咨询
问题描述
我编写了如下测试函数:
def doSomething() { def node = Jenkins.get().getNode("node-name") def count = 0 retry (5) { println "hello" count += 1 if (count < 3) { throw new InterruptedException() } } }
该函数在Pipeline的stage步骤中通过script块调用:
stage(..) { steps { script { doSomething() } } }
运行时出现CPS序列化异常:
an exception which occurred: in field com.cloudbees.groovy.cps.impl.BlockScopeEnv.locals in object com.cloudbees.groovy.cps.impl.BlockScopeEnv@5e799cd4 in field com.cloudbees.groovy.cps.impl.CpsClosureDef.capture in object com.cloudbees.groovy.cps.impl.CpsClosureDef@e7332c3 in field com.cloudbees.groovy.cps.impl.CpsClosure.def in object org.jenkinsci.plugins.workflow.cps.CpsClosure2@4fed11ca in field org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.closures in object org.jenkinsci.plugins.workflow.cps.CpsThreadGroup@74cb9088 in object org.jenkinsci.plugins.workflow.cps.CpsThreadGroup@74cb9088 Caused: java.io.NotSerializableException: hudson.model.Hudson at org.jboss.marshalling.river.RiverMarshaller.doWriteObject(RiverMarshaller.java:274) at org.jboss.marshalling.river.BlockMarshaller.doWriteObject(BlockMarshaller.java:65) ...
移除def node = Jenkins.get().getNode..代码后异常消失。想咨询:是否因为CPS代码中无法调用Jenkins API?若是,原因是什么?
解答
不是CPS代码完全不能调用Jenkins API,问题出在Jenkins API返回的对象不可序列化,而Pipeline的CPS机制要求所有在闭包上下文里的变量必须是可序列化的。
具体原因:
- Jenkins Pipeline采用CPS(Continuation-Passing Style)执行模式,会在执行暂停(比如重试、等待阶段切换)时序列化整个执行上下文,以便后续恢复执行。
Jenkins.get().getNode("node-name")获取到的节点对象,底层关联了hudson.model.Hudson(Jenkins核心实例),这个对象未实现Serializable接口,无法被序列化。retry闭包会捕获所在作用域的所有变量(包括node),当Jenkins尝试序列化闭包上下文时,发现node引用了不可序列化的Hudson对象,就抛出了NotSerializableException。
解决办法:
- 如果不需要在闭包里使用
node变量,在闭包执行前切断对它的引用:def doSomething() { def node = Jenkins.get().getNode("node-name") // 处理node的逻辑,比如获取节点信息 node = null // 切断引用,避免被闭包捕获 def count = 0 retry (5) { println "hello" count += 1 if (count < 3) { throw new InterruptedException() } } } - 如果需要调用Jenkins API,用
@NonCPS注解标记相关方法,让方法脱离CPS序列化机制执行(注意:@NonCPS方法里不能调用Pipeline步骤,也不能返回不可序列化对象给CPS上下文):@NonCPS def getJenkinsNode(String nodeName) { return Jenkins.get().getNode(nodeName) } def doSomething() { def node = getJenkinsNode("node-name") // 仅提取可序列化的属性,比如节点名称 def nodeName = node.name node = null def count = 0 retry (5) { println "hello, ${nodeName}" count += 1 if (count < 3) { throw new InterruptedException() } } } - 优先使用Pipeline原生步骤(比如
node步骤)操作节点,这类步骤已做序列化兼容处理,避免直接调用底层Jenkins API。
内容的提问来源于stack exchange,提问作者rjs
相关产品推荐
相关产品推荐

