Jenkins Pipeline超大Map触发createArray限制:底层原理与规避方案
我正尝试为一个庞大的properties map初始化函数添加若干选项(无法通过重构修正团队的不合理设计)。流程简化如下:
file1.groovy代码:
def call(Map opts = [:]) { opts = file2(opts) // do some stuff with the opts... }
file2.groovy代码(含大量默认值设置):
def call(Map opts = [:]) { opts.something = opts.something ?: true opts.something2 = opts.something ?: false // 此处省略大量类似默认值赋值语句 }
添加选项后运行时出现报错:
org.jenkinsci.plugins.workflow.actions.ErrorAction$ErrorId: 6e0ca570-b8b5-4c4b-93bb-e3d5661c1294 java.lang.NoSuchMethodError: 'java.lang.Object[] org.codehaus.groovy.runtime.ArrayUtil.createArray(java.lang.Object, java.lang.Object, java.lang.Object, java.lang.Object,
注:报错信息中
java.lang.Object,重复约20行,经排查得知createArray函数存在250个对象的参数数量限制。
核心疑问
- 上述流程中
createArray是如何被调用的?是否是Map在函数间传递时被转换为数组?是否与CPS有关? - 在无法重构现有系统的前提下,是否有办法规避该问题?(目前注释5个新增选项中的2个即可正常运行)
解答
1. createArray的调用逻辑
这个问题和Jenkins Pipeline的CPS(Continuations Passing Style)转换机制直接相关。Jenkins在执行Groovy Pipeline代码时,会将代码做CPS转换,把连续的语句拆解为可序列化的执行步骤。当file2.groovy中存在大量连续的Map属性赋值语句时,CPS转换过程会把这些赋值操作的上下文、左右值等参数打包成一个超大的参数列表,调用ArrayUtil.createArray来生成参数数组。
Groovy的ArrayUtil.createArray是自动生成的重载方法,最多支持250个参数,当参数数量超过阈值时,就会抛出找不到对应重载方法的错误。注意:Map本身并没有被转换为数组,是CPS转换生成的中间代码触发了这个限制。
2. 不重构前提下的规避方案
分批拆分赋值语句:把
file2.groovy里的赋值语句拆分成多个独立代码块,让CPS转换时分开处理每组参数,避免单个createArray调用的参数超量。例如:def call(Map opts = [:]) { // 第一组赋值 { opts.something = opts.something ?: true opts.something2 = opts.something ?: false // ... 约100个赋值语句 }() // 第二组赋值(包含新增的5个选项) { opts.newOpt1 = opts.newOpt1 ?: "val1" opts.newOpt2 = opts.newOpt2 ?: "val2" // ... 剩余赋值语句 }() }用
putAll批量设置默认值:把部分默认值整理成新Map,通过putAll批量合并到原Map中,减少单个赋值语句的数量。例如:def call(Map opts = [:]) { def defaults = [ something: true, something2: false, // ... 批量添加原有默认值 newOpt1: "val1", newOpt2: "val2", newOpt3: "val3", newOpt4: "val4", newOpt5: "val5" ] // 仅覆盖未设置的键 opts.putAll(defaults.findAll { k, v -> !opts.containsKey(k) }) // 剩余无法批量处理的赋值语句(如果有) }拆分新增选项到辅助函数:把新增的5个选项放到单独的辅助函数中,在
file2主函数末尾调用,避免主函数内的赋值语句总数触发阈值。例如:def call(Map opts = [:]) { // 原有大量赋值语句 opts.something = opts.something ?: true // ... // 调用辅助函数设置新增选项 setNewOptions(opts) } def setNewOptions(Map opts) { opts.newOpt1 = opts.newOpt1 ?: "val1" opts.newOpt2 = opts.newOpt2 ?: "val2" opts.newOpt3 = opts.newOpt3 ?: "val3" opts.newOpt4 = opts.newOpt4 ?: "val4" opts.newOpt5 = opts.newOpt5 ?: "val5" }
内容的提问来源于stack exchange,提问作者Rev Tyler

