Jenkins流水线脚本优化:Python特定异常触发延迟重提交
解决Jenkins流水线Python特定异常的无节点占用自动重试问题
问题描述
我正在开发运行Python代码的Jenkins流水线脚本,需要实现以下机制:当Python代码执行遇到特定故障/异常(如调用外部API触发的requests.exceptions.ConnectionError)时,延迟3-5分钟自动重提交任务,且不占用节点资源。但目前不确定如何处理特定故障下的重提交流程,尝试了三种方法都没解决:
- 使用
retry()选项:休眠期间会占用节点资源,不符合需求; - 使用
build选项:执行时出现错误java.lang.UnsupportedOperationException: must specify $class with an implementation of interface java.util.List; - 在流水线代码中使用Naginator插件,但未生效。
原始Jenkins流水线代码
pipeline { agent { label 'test' } options { timeout(time: "${env.TIMEOUT}", unit: 'MINUTES') } stages { stage('Set Build Name') { steps { script { if (env.AUTHOR && env.HEAD_SHA){ currentBuild.description = """<p><b><a href=${env.PR_URL}>PR_URL</a></p></b><p><b>BaseBranch:${env.BASE_BRANCH}</p></b>""" currentBuild.displayName = "${env.AUTHOR}-${env.HEAD_SHA[0..6]}" } } } } stage('Run script') { steps { script{ def command = "#!/usr/bin/bash\nset +xe" command += """\nexport PYTHONPATH="${WORKSPACE}/bin:${WORKSPACE}:${WORKSPACE}/.." source /virtual/python/location/activate python -u ${WORKSPACE}/bin/${JOB}.py """ sh script: command } } } } post { failure { mail to: "${env.ERROR_SUBSCRIBERS}", subject: "Failed pipeline: ${env.JOB_NAME}", body: "${currentBuild.absoluteUrl}" } always { archiveArtifacts artifacts: '*.log', allowEmptyArchive: true archiveArtifacts artifacts: '*.diff', allowEmptyArchive: true archiveArtifacts artifacts: 'result.json', allowEmptyArchive: true sh script: 'rm -f *.log *.diff result.json', returnStatus: true } } }
尝试的retry()版本代码
pipeline { agent { label 'test' } options { timeout(time: "${env.TIMEOUT}", unit: 'MINUTES') } stages { stage('Set Build Name') { steps { script { if (env.AUTHOR && env.HEAD_SHA){ currentBuild.description = """<p><b><a href=${env.PR_URL}>PR_URL</a></p></b><p><b>BaseBranch:${env.BASE_BRANCH}</p></b>""" currentBuild.displayName = "${env.AUTHOR}-${env.HEAD_SHA[0..6]}" } } } } stage('Run script') { steps { retry (5) { script{ def retryDelayMinutes = 2 def attempt = 0 def command = "#!/usr/bin/bash\nset +xe" command += """\nexport PYTHONPATH="${WORKSPACE}/bin:${WORKSPACE}:${WORKSPACE}/.." source /virtual/python/location/activate python -u ${WORKSPACE}/bin/${JOB}.py """ def result = sh script: command, returnStatus: true if (result != 0) { if (attempt < maxAttempts) { echo "Build attempt $attempt failed, retrying in $retryDelayMinutes minute(s)..." attempt += 1 sleep retryDelayMinutes * 60 } else { error("Build failed after 5 attempts") } } else { break } } } } } } post { failure { mail to: "${env.ERROR_SUBSCRIBERS}", subject: "Failed pipeline: ${env.JOB_NAME}", body: "${currentBuild.absoluteUrl}" } always { archiveArtifacts artifacts: '*.log', allowEmptyArchive: true archiveArtifacts artifacts: '*.diff', allowEmptyArchive: true archiveArtifacts artifacts: 'result.json', allowEmptyArchive: true sh script: 'rm -f *.log *.diff result.json', returnStatus: true } } }
尝试的build版本代码
pipeline { agent { label 'test' } options { timeout(time: "${env.TIMEOUT}", unit: 'MINUTES') } stages { stage('Set Build Name') { steps { script { if (env.AUTHOR && env.HEAD_SHA){ currentBuild.description = """<p><b><a href=${env.PR_URL}>PR_URL</a></p></b><p><b>BaseBranch:${env.BASE_BRANCH}</p></b>""" currentBuild.displayName = "${env.AUTHOR}-${env.HEAD_SHA[0..6]}" } } } } stage('Run script') { steps { retry (5) { script{ def command = "#!/usr/bin/bash\nset +xe" command += """\nexport PYTHONPATH="${WORKSPACE}/bin:${WORKSPACE}:${WORKSPACE}/.." source /virtual/python/location/activate python -u ${WORKSPACE}/bin/${JOB}.py """ catchError { sh script: command } } } } } } post { failure { mail to: "${env.ERROR_SUBSCRIBERS}", subject: "Failed pipeline: ${env.JOB_NAME}", body: "${currentBuild.absoluteUrl}" script { while (retryCount < maxRetries) { retryCount++ def paramsMap = [:] def parametersAction = currentBuild.rawBuild.getAction(ParametersAction) if (parametersAction != null) { parametersAction.parameters.each { param -> paramsMap[param.name] = param.value.toString() } } // Re-run the job build job: "${env.JOB_NAME}", parameters: paramsMap, propagate: false, wait: false } } } always { archiveArtifacts artifacts: '*.log', allowEmptyArchive: true archiveArtifacts artifacts: '*.diff', allowEmptyArchive: true archiveArtifacts artifacts: 'result.json', allowEmptyArchive: true sh script: 'rm -f *.log *.diff result.json', returnStatus: true } } }
注:使用的是Jenkins企业版。
正确解决方案思路
要实现“特定异常触发+延迟重试+不占节点”,核心是:
- 让Python脚本区分可重试异常和不可重试异常,返回特定退出码或标记;
- 使用Jenkins插件(推荐Naginator)或者正确的
build步骤实现无节点占用的延迟重试。
方案一:Naginator插件(推荐)
Naginator插件专门用于构建失败后延迟重试,且重试过程不占用节点资源。之前未生效大概率是配置或代码判断逻辑问题。
步骤1:改造Python脚本,标记可重试异常
修改Python代码,捕获特定异常时输出明确标记或返回特定退出码(比如退出码2代表可重试):
import sys import requests try: # 你的业务代码 response = requests.get("https://external-api.example.com") response.raise_for_status() except requests.exceptions.ConnectionError as e: print("[RETRYABLE_ERROR] Connection failed: ", e) sys.exit(2) # 返回特定退出码标记可重试 except Exception as e: print("[NON_RETRYABLE_ERROR] Fatal error: ", e) sys.exit(1) # 普通错误,不触发重试
步骤2:Jenkins流水线配置Naginator
在流水线中,通过options配置Naginator,仅当构建失败原因匹配特定标记或退出码时触发重试:
pipeline { agent { label 'test' } options { timeout(time: "${env.TIMEOUT}", unit: 'MINUTES') // 配置Naginator插件:最多重试5次,延迟3-5分钟随机 naginator( maxRetryCount: 5, delay: [ type: 'RANDOM', min: 3, max: 5, unit: 'MINUTES' ], retryCondition: 'REGEXP', regexp: '\\[RETRYABLE_ERROR\\]', // 匹配Python输出的标记 regexpForLog: true ) } stages { stage('Set Build Name') { steps { script { if (env.AUTHOR && env.HEAD_SHA){ currentBuild.description = """<p><b><a href=${env.PR_URL}>PR_URL</a></p></b><p><b>BaseBranch:${env.BASE_BRANCH}</p></b>""" currentBuild.displayName = "${env.AUTHOR}-${env.HEAD_SHA[0..6]}" } } } } stage('Run script') { steps { script{ def command = "#!/usr/bin/bash\nset +xe" command += """\nexport PYTHONPATH="${WORKSPACE}/bin:${WORKSPACE}:${WORKSPACE}/.." source /virtual/python/location/activate python -u ${WORKSPACE}/bin/${JOB}.py """ sh script: command } } } } post { failure { mail to: "${env.ERROR_SUBSCRIBERS}", subject: "Failed pipeline: ${env.JOB_NAME}", body: "${currentBuild.absoluteUrl}" } always { archiveArtifacts artifacts: '*.log', allowEmptyArchive: true archiveArtifacts artifacts: '*.diff', allowEmptyArchive: true archiveArtifacts artifacts: 'result.json', allowEmptyArchive: true sh script: 'rm -f *.log *.diff result.json', returnStatus: true } } }
注意事项
- 确保Jenkins已安装Naginator插件,且版本适配Jenkins企业版;
regexp要准确匹配Python脚本输出的标记,避免误触发重试。
方案二:使用build步骤实现延迟重试(无插件依赖)
之前的错误是因为build步骤的parameters参数需要是List<ParameterValue>类型,而非Map。改造后实现:
步骤1:同样改造Python脚本返回特定退出码
同方案一的Python脚本改造,返回退出码2表示可重试。
步骤2:流水线中判断退出码,触发延迟重试
pipeline { agent { label 'test' } options { timeout(time: "${env.TIMEOUT}", unit: 'MINUTES') } environment { MAX_RETRIES = 5 RETRY_DELAY_MIN = 3 RETRY_DELAY_MAX = 5 CURRENT_RETRY = "${env.CURRENT_RETRY ?: 0}" // 用环境变量记录重试次数 } stages { stage('Set Build Name') { steps { script { if (env.AUTHOR && env.HEAD_SHA){ currentBuild.description = """<p><b><a href=${env.PR_URL}>PR_URL</a></p></b><p><b>BaseBranch:${env.BASE_BRANCH}</p></b>""" currentBuild.displayName = "${env.AUTHOR}-${env.HEAD_SHA[0..6]}" } } } } stage('Run script') { steps { script{ def command = "#!/usr/bin/bash\nset +xe" command += """\nexport PYTHONPATH="${WORKSPACE}/bin:${WORKSPACE}:${WORKSPACE}/.." source /virtual/python/location/activate python -u ${WORKSPACE}/bin/${JOB}.py """ def exitCode = sh script: command, returnStatus: true // 判断是否为可重试错误 if (exitCode == 2) { def retryCount = env.CURRENT_RETRY.toInteger() if (retryCount < env.MAX_RETRIES.toInteger()) { // 生成3-5分钟随机延迟 def delay = new Random().nextInt(env.RETRY_DELAY_MAX.toInteger() - env.RETRY_DELAY_MIN.toInteger() + 1) + env.RETRY_DELAY_MIN.toInteger() echo "Detected retryable error, scheduling retry in ${delay} minutes..." // 转换参数为List格式(解决之前的Java异常) def params = [] def parametersAction = currentBuild.rawBuild.getAction(ParametersAction) if (parametersAction != null) { parametersAction.parameters.each { param -> params.add(string(name: param.name, value: param.value.toString())) } } // 更新重试次数参数 params.add(string(name: 'CURRENT_RETRY', value: (retryCount + 1).toString())) // 触发新构建,不等待,立即释放当前节点 build job: "${env.JOB_NAME}", parameters: params, propagate: false, wait: false } else { error("Build failed after ${env.MAX_RETRIES} retry attempts") } } else if (exitCode != 0) { error("Build failed with non-retryable error, exit code: ${exitCode}") } } } } } post { failure { mail to: "${env.ERROR_SUBSCRIBERS}", subject: "Failed pipeline: ${env.JOB_NAME}", body: "${currentBuild.absoluteUrl}" } always { archiveArtifacts artifacts: '*.log', allowEmptyArchive: true archiveArtifacts artifacts: '*.diff', allowEmptyArchive: true archiveArtifacts artifacts: 'result.json', allowEmptyArchive: true sh script: 'rm -f *.log *.diff result.json', returnStatus: true } } }
关键修正点
- 将参数转换为
List<ParameterValue>格式(用string()方法创建参数),解决之前的java.lang.UnsupportedOperationException错误; - 使用环境变量记录重试次数,避免无限重试;
- 随机延迟3-5分钟,符合需求;
- 触发新构建时
wait: false,当前节点立即释放,不占用资源。
内容的提问来源于stack exchange,提问作者Snanix
相关产品推荐
相关产品推荐

