You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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企业版。


正确解决方案思路

要实现“特定异常触发+延迟重试+不占节点”,核心是:

  1. 让Python脚本区分可重试异常和不可重试异常,返回特定退出码或标记;
  2. 使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 11:19:52