💨

Jenkins Script 例外処理(NotSerializableException)

2025/01/08に公開

発生した例外処理

シリアライズまわり

java.io.NotSerializableException: java.util.LinkedHashMap$LinkedEntryIterator

デバッグ方法

具体的な例外が何なのかは
Jenkinsスクリプト内の該当付近の箇所に、以下を仕込んで確認する

echo "Error: " + e.toString()

クロージャ記法に変更することで、シリアライズの例外問題(NotSerializedException)が解消されました。
■前提:今回使用する変数宣言

HashSet<String> failedFiles = new HashSet()
Map<String, String> hogehogeMap = [:]

■修正前

for (hogehogePair in hogehogeMap) {
    try {
        String srcFilePath = "${env.WORKSPACE}\\${hogehogePair.key}.bin"
        String dstFilePath = "${hogehogePair.value}"
        
        echo "Copying ${srcFilePath} to ${dstFilePath}"
        
        timeout(time: 60, unit: 'MINUTES') {            
            powershell encoding: 'UTF-8', returnStdout: true, script: """
                [Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding('UTF-8')                
                Copy-Item -Path "${srcFilePath}" -Destination "${dstFilePath}" -Force
            """
        }
    }
    catch (Exception e) {
        echo "Failed to execute"
        failedFiles.add("${hogehogePair.key}")
    }
}

■修正後

hogehogeMap.each { binName, filePath ->
    try {
        String srcFilePath = "${env.WORKSPACE}\\${binName}.bin"
        String dstFilePath = "${filePath}"
        
        echo "Copying ${srcFilePath} to ${dstFilePath}"
        
        timeout(time: 60, unit: 'MINUTES') {            
            powershell encoding: 'UTF-8', returnStdout: true, script: """
                [Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding('UTF-8')                
                Copy-Item -Path "${srcFilePath}" -Destination "${dstFilePath}" -Force
            """
        }
    }
    catch (Exception e) {
        echo "Failed to execute"
        failedFiles.add("${binName}")
    } 
}

解消された背景

NotSerializableException が解消された理由は、Groovyのクロージャー記法を使用したことで、オブジェクトのシリアライズ可能性に影響を与えた可能性が高い


1. 背景

NotSerializableException は、JavaやGroovyでシリアライズしようとしたオブジェクトが Serializable インターフェースを実装していない場合に発生
この問題は、Jenkins Pipelineスクリプトのようなシリアライズが頻繁に発生する環境では特に顕著である

GroovyのClosure(クロージャー)は、特定の文脈で処理を簡潔に記述できる一方で、次のような特性がある:

  1. クロージャー内の変数やメソッドは、自動的に親スコープ(クラスやメソッド)にバインドされる。
  2. クロージャーはデフォルトでシリアライズ可能 (Serializable) 。

参考サイト:
https://docs.groovy-lang.org/next/html/gapi/groovy/lang/Closure.html

※SerializableをImplementしていることがわかる

Package: groovy.lang
[Java] Class Closure<V>
groovy.lang.Closure
All Implemented Interfaces and Traits:
Cloneable, GroovyCallable, Runnable, Serializable

2. クロージャーを使用して解消された理由

クロージャー記法を使用することで、以下の改善が期待できる:

2.1. スコープの明確化

クロージャーを使用すると、each 内で使用される変数(binNamefilePath)が、親スコープから暗黙的に分離されるため、
このスコープ分離により、不要な非シリアライズ可能な参照を排除できる。

例えば、以下のようなコード:

hogehogeMap.each { binName, filePath ->
    // シリアライズされるのは binName と filePath の値だけ
}

は、クロージャー内のみに binNamefilePath を閉じ込めるため、シリアライズ可能性が高まる。

2.2. 暗黙的な参照の除去

クロージャーを使用しない場合、ループ内の変数や親スコープの不要な参照がシリアライズ対象に含まれる可能性があり、これにより、非シリアライズ可能なオブジェクト(例えば、Groovyの一部の内部オブジェクトや外部クラス)が混入する場合がある。

2.3. Pipeline特有の制約への対応

Jenkins Pipelineはスクリプトをシリアライズして一時停止・再開する仕組みを持っている。
このため、Pipelineスクリプト内で使用されるすべてのオブジェクトは、シリアライズ可能である必要がありため、
クロージャー記法を使用することで、Jenkinsが扱いやすい形になった可能性があります。


3. なぜクロージャーで解消されたのか?

具体的には、以下の理由が考えられる:

  1. クロージャー自体が Serializable

    • Groovyのクロージャーはデフォルトでシリアライズ可能なので、シリアライズエラーの原因になりにくい。
  2. 不要な参照の排除

    • 親スコープからの非シリアライズ可能なオブジェクト(例えば、Jenkins内部のクラスや一時変数など)がクロージャーに混入しなくなった。
  3. Groovyの内部処理の最適化

    • クロージャーを使用することで、GroovyがJenkins Pipeline用にオブジェクトを最適化・簡素化した。

4. クロージャーの使用が適切な場面

以下のような場合には、クロージャーを使用すると良さそう

  • Jenkins Pipelineスクリプトでループ処理を行う場合。
  • 非シリアライズ可能なエラーが発生する可能性がある場合。
  • コードを簡潔に記述したい場合。

5. まとめ

クロージャー記法に変えたことで NotSerializableException が解消されたのは、以下のような理由が考えられる:

  • クロージャーのスコープ分離により、非シリアライズ可能な参照が排除された。
  • クロージャーがデフォルトでシリアライズ可能であるため。
  • Jenkins Pipeline特有のシリアライズ要件にクロージャーが適合した。

クロージャーはGroovy特有の強力な記法であり、Jenkinsのような環境では特に有用であることがわかったため、スクリプトの設計でクロージャーを積極的に活用するのがよさそうです。

Discussion