引言
最近在工作中使用Jenkins进行持续集成任务,遇到这样一个问题:
java.io.NotSerializableException: java.util.regex.Matcher
经过调查发现这是Jenkins的CPS插件报出的,原因是在使用正则表达式相关操作的函数时,代码写法不规范,导致不能序列化。那么什么是CPS呢, Jenkins为什么又需要做序列化的操作呢?
什么是CPS
在函数式编程中,CPS (Continuation-Passing Style)是一种编程风格:所有的控制块都通过 continuation 来显式传递。简单来说,在CPS风格中,函数不能有返回语句,它的调用者要想获得它的结果,需要显式传递一个回调函数来获取结果并继续执行。而为了保证整个程序执行下去,这个回调函数还会一直嵌套下去。这里的回调函数就是一个“continuation”。暂时找不到好的翻译,就保留它的原文。
下面来看一个递归的例子,比如常见的计算数学阶乘的函数:
// 原来代码
function fact(n) {
if (n == 0)
return 1 ;
else
return n * fact(n-1) ;
}
// CPS风格代码
function fact(n,ret) {
if (n == 0)
ret(1) ;
else
fact(n-1, function (t0) {
ret(n * t0) }) ;
}
// 调用入口
fact (5, function (n) {
console.log(n) ; // Output 120
})
上面是传统的递归实现,下面是CPS风格的实现。可以看到return语句被替换成一个回调函数的调用,而相乘这个动作会在接下来的递归里通过嵌套的回调函数完成,直到终止条件。你可以理解为,在每一层的递归中,回调函数都会给参数多乘上一个系数,并调用上一层的回调函数,直到最外层打印结果。
CPS的应用场景
1. 在Jenkins中的应用
回到最初的问题,Jenkins为什么会使用CPS编程技术呢?原因是在Jenkins Pipeline的实现中,期望在任何时候都可以中断代码的执行,保存状态,并在适当时候恢复执行。这可以应对Jenkins agent宕机的场景。如果一个函数执行过后就返回了,那么就会丢失一部分状态,CPS代码由于在中间不返回结果,因此可以解决这个问题。
然而,你也的Pipeline代码是Groovy语言的,其本身并不是CPS风格的,这就需要一个解释器将代码编译成CPS风格,Jenkins里面通过 groovy-cps 这个插件来完成。你也可以查看这个插件的稳定获得更多的关于CPS和Groovy解释器的介绍。
我们知道,如果想要持久化一个对象的状态,那么这个对象需要时可序列化的(Serializable)。Jenkins如果想要保存Pipeline的状态,就会要求CPS的代码也是可序列化的。如果有一些代码是不可序列化,或者序列化不安全的,我们可以在Pipeline代码中加上@NonCPS标记。并且,声明了该标记的函数也只能调用其他有该标记的函数。Jenkins会对这类函数进行常规编译,不生成可序列化的CPS代码。
在写跟正则表达式相关的功能时,可能会用到的~操作符就是序列化不安全,因此要声明标记@NonCPS:
def t = (text =~ TEST_REGEX)
如果忘记在使用上面语句的方法前声明该标记,就会抛出文章开头的不可序列化的异常,我遇到的问题的原因就在此。
2. 在异步请求中应用
另外一个我们容易想到的场景就是异步请求,我们在JavaScript代码里经常用回调函数来处理请求的结果,并保持程序不被挂起。这其实就一种CPS风格:
request("./index", function (text) {
document.getElementById("test").innerHTML = text ;
}) ;
request函数的实现使用Ajax对象:
function request (url, onSuccess, onFail) {
var req ;
function process() {
if (req.readyState == 4) {
if (req.status == 200) {
if (onSuccess)
onSuccess(req.responseText, url, req) ;
} else {
if (onFail)
onFail(url, req) ;
}
}
}
if (window.XMLHttpRequest)
req = new XMLHttpRequest();
req.onreadystatechange = process;
req.open("GET", url, async);
req.send(null);
return req ;
}
这里面的回调函数onSuccess和onFail就是所谓的 continuation。
参考资料
- http://matt.might.net/articles/by-example-continuation-passing-style/
- https://github.com/jenkinsci/workflow-cps-plugin
- https://github.com/cloudbees/groovy-cps/
- https://en.wikipedia.org/wiki/Continuation-passing_style

本文介绍了Jenkins Pipeline中CPS编程的概念及其应用场景。CPS是一种编程风格,通过显式传递回调函数来处理控制流程。在Jenkins中,CPS用于确保Pipeline在中断后能恢复执行,通过groovy-cps插件将Groovy代码转换为CPS风格,以实现可序列化。当遇到序列化异常时,可能是因为未正确声明函数标记。此外,CPS也在异步请求中发挥类似作用,如JavaScript中的回调函数处理。
1198

被折叠的 条评论
为什么被折叠?



