Groovy CPS 简介
Groovy CPS 中的 CPS 全称是 Continuation Passing Style,翻译过来就是延续传递风格。它允许流水线脚本在任意位置暂停执行,并保存当前执行状态,待外部事件(如审批、异步触发)完成后,再恢复执行。这种机制使得 Jenkins 能够支持复杂的流水线控制和分布式执行。
Jenkins Pipeline 执行模型
Jenkins pipeline 通过 Groovy CPS 实现流水线的暂停与恢复能力。
- 暂停执行:当遇到等待节点(如输入审批、异步事件)时暂停执行,脚本状态序列化保存到本地。
- 恢复执行:Jenkins 反序列化脚本状态,并从中断点继续运行。
Groovy CPS 的限制
- 代码必须是 CPS 可转换的:比如部分 toString() 标准库方法不兼容,I/O、线程、闭包嵌套、反射等特性都不受支持。
- 不能直接持有复杂的非序列化对象:比如 pipeline 脚本上下文对象 Script,想要持有必须用 transient 标记,并实现恢复方法。
- 调试和异常栈会比较复杂:pipeline 中的 Groovy 代码,需通过 Groovy CPS 编译器转成 CPS 代码,而非普通顺序代码,导致异常栈里可能会被 CPS 重写。
@NonCPS 方法限制
@NoeCPS 会告诉 CPS 不去转换某个方法。但它的使用也有明确限制:
- 方法不能访问 script(会序列化失败或运行时报错)。
- 方法必须是纯逻辑,不能依赖任何 Pipeline DSL,比如 sh(), echo(), input(), checkout() 等。
- 方法返回值必须是基本类型或可序列化的结构(不能返回闭包、不可序列化对象)。
使用 transient
修饰与动态绑定
想要避免 Jenkins Pipeline 在暂停恢复时因上下文丢失导致的空指针异常和流水线失败,可以使用 transient
修饰与动态绑定方案,提高共享库代码的健壮性和用户体验。
下面是一个自定义 Logger 的实践示例:
class Logger implements Serializable {
private transient def script
Logger(def script) {
this.script = script
}
Logger bindScript(def script) {
this.script = script
return this
}
void info(String msg) {
script.echo "[INFO] ${msg}"
}
}
class ContextResolver implements Serializable {
private transient def script
private Logger log
ContextResolver(def script) {
bindScript(script)
}
void bindScript(def script) {
this.script = script
if (log != null) {
log.bindScript(script)
} else {
this.log = new Logger(script)
}
}
}
实践注意
持有 script 对象的逻辑,尽量放在 vars/
。src/
只写纯逻辑代码,避免持有 script 对象。尽量避免 @NonCPS
,保证流水线的可序列化和稳定性。如果需要持有 script 对象,应注意以下事项:
- 必须为不可序列化的字段添加 transient 修饰,避免序列化异常。
- 持有 script,应实现 bindScript() 进行动态绑定。
- 流水线恢复后,调用 bindScript(this) 确保上下文可用。