kagamihogeの日記

kagamihogeの日記です。

JEP 351: ZGC: Uncommit Unused Memoryをテキトーに訳した

http://openjdk.java.net/jeps/351

JEP 351: ZGC: Uncommit Unused Memory

Owner    Per Liden
Type    Feature
Scope   Implementation
Status  Candidate
Component   hotspot / gc
Discussion  hotspot dash gc dash dev at openjdk dot java dot net
Effort  S
Duration    S
Reviewed by Mikael Vidstedt, Stefan Karlsson
Created 2019/03/08 10:35
Updated 2019/03/14 21:48
Issue   8220347

Summary

未使用ヒープメモリをOSに返却するようZGCを改良します。

Motivation

現行のZGCは、あるメモリが長時間未使用だとしても、uncommitおよびそのメモリのOS返却を行いません。この振る舞いはあらゆる種類のアプリケーションと環境に最適では無く、特にメモリフットプリントが関心事である場合に顕著です。たとえば、

  • リソース使用分で請求されるコンテナ環境。
  • アプリケーションが長期間アイドルしたり、多数のアプリケーションでリソース共有や競合が発生する環境。
  • アプリケーション実行中にヒープ空間に対する要求が極めて変化する場合。たとえば、スタートアップ中に必要なヒープが通常状態よりも大量に必要となるケース。

HotSpot, G1とShenandoahのその他のGCは、今日ではこれら機能を有しており、上記カテゴリのユーザに受け入れられています。ZGCにこれら機能を追加し、同カテゴリのユーザにZGCを受け入れやすくします。

Description

ZGCヒープはZPagesというヒープ領域セットで構成しています。ZPageはそれぞれ可変量のコミット済ヒープメモリと関連付けられています。ZGCがヒープをコンパクト化すると、ZPageは解放されてZPageCacheというページキャッシュに挿入されます。ページキャッシュのZPagesは新規ヒープアロケーションに対して再使用可能状態にあり、アロケーション後はキャッシュから削除されます。ページキャッシュはパフォーマンスにとって極めて重要で、これはメモリのコミットとuncommitが高価なオペレーションなためです。

ページキャッシュのZPagesはヒープの未使用部分を表現し、これはuncommitとOS返却が"可能(could)"です。このためメモリuncommitはページキャッシュの適当に選択したZPagesを単に切り離せば良く、それからページに関連付けられたメモリをuncommitします。ページキャッシュはLRUと分離サイズ(small, medium, large)でZPagesを維持する仕組みがあるため、ZPages切り離しのメカニズムとメモリuncommitは比較的単純です。チャレンジングなのは、キャッシュからZPageを切り離すタイミングの決定ポリシーです。

シンプルなポリシーとしてはタイムアウトや、ページキャッシュを切り離すZPage生存時間を指定するディレイ値があります。タイムアウトは妥当なデフォルト値で、これはコマンドラインオプションでオーバーライドします。Shenandoah GCはこれと似たポリシーを持ち、デフォルト値5分でコマンドラインオプション-XX:ShenandoahUncommitDelay=<milliseconds>でオーバーライドします。

上記のようなポリシーは妥当な動作をします。しかし、もっと賢い切り離しポリシー設定が可能で、これは別途コマンドラインオプションを必要としません。たとえば、GC頻度や何らかのデータに基づいて最適なタイムアウトを得るヒューリスティックがあります。現時点では採用ポリシーは決定していません。各種ポリシーを評価予定です。まず最初は、シンプルなタイムアウトポリシーと-XX:ZUncommitDelay=<seconds>を提供し、それから、より賢いポリシー(もし見つかれば)を導入します。

ポリシーが決定したとしても、ヒープが最小サイズ(-Xms)以下になるようなZGCのメモリuncommitを絶対にしません。つまり、最小ヒープサイズ(-Xms)が最大ヒープサイズ(-Xmx)と等しい状態でJVMを開始する場合、uncommit機能は無効化します。

最後に、Linux/x64のZGCはヒープ返却にtmpfsやhugetlbfsファイルを使います。これらファイルのメモリuncommitはFALLOC_FL_PUNCH_HOLEサポートのあるfallocate(2)が必要で、これが最初に導入されたのはinux 3.5 (tmpfs)と4.3 (hugetlbfs)です。ZGCはこれより古いLinuxでも以前同様に動作する必要があります。ただし、uncommit機能が無効化される点は除きます。

Testing

  • 開発するuncommit機能の検証に一つ以上のjtregテストを使用する。
  • 既存のベンチマーク、SPECjbbとSPECjvmなど、でデフォルトポリシー使用時にレイテンシやスループット劣化が見られないことを検証する。

Risks and Assumptions

現行プランではuncommit機能は常にONで、明示的な無効化オプションはありません。ただし、-Xms-Xmxを同一値にすることで間接的には可能です。これは余計なオプション追加をしないというZGCの意志の現れです。明示オプション(-XX:-ZUncommit)が必要かどうかは時が決めるでしょう。現状、必要でないと判断しています。

Spring Batch 4.1.x - Reference Documentation - JSR-352 Supportのテキトー翻訳

https://docs.spring.io/spring-batch/4.1.x/reference/html/jsr-352.html#jsr-352

https://qiita.com/kagamihoge/items/12fbbc2eac5b8a5ac1e0 俺の訳一覧リスト

*1

1. JSR-352 Support

Spring Batch 3.0以降はJSR-352を完全に実装しています。このセクションは仕様自体の入れ替えというより、JSR-352固有のコンセプトをSpring Batchに適用する方法について説明します。JSR-352の情報についてはJCP https://jcp.org/en/jsr/detail?id=352 を参照してください。

1.1. General Notes about Spring Batch and JSR-352

Spring BatchとJSR-352は構造的には同じです。どちらもjobはstepで作り、reader, processor, writer, listenerを持ちます。しかし、微妙に異なる箇所があります。たとえば、Spring Batchのorg.springframework.batch.core.SkipListener#onSkipInWrite(S item, Throwable t)は引数を2つ、スキップされたアイテムとスキップ原因の例外、を取ります。JSR-352の方(javax.batch.api.chunk.listener.SkipWriteListener#onSkipWriteItem(List<Object> items, Exception ex))も同様に引数を2つとります。しかし、最初の引数はそのchunkの全アイテムListで2つ目はスキップ原因のExceptionです。こうした差異があるため、Spring Batchでjobを実行する方法が2つ、従来通りのSpring BatchのjobかJSR-352ベースのjob、がある点には注意が必要です。Spring Batchのアーティファクト(reader, writerなど)のjobをJSR-352のJSLで設定してJsrJobOperatorで実行する場合、JSR-352のルールで動作します。なお、JSR-352で開発したバッチアーティファクトはSpring Batchのjobでは動作しない点に注意してください。

1.2. Setup

1.2.1. Application Contexts

Spring Batch内でのJSR-352ベースのjobは2つのアプリケーションコンテキストを構成します。親コンテキストは、JobRepository, PlatformTransactionManagerなどSpring Batchのインフラ部分に関連するbeanを持ち、子コンテキストは実行するjobの設定を持ちます。親コンテキストはフレームワークが提供するjsrBaseContext.xmlで定義します。このコンテキストはJSR-352-BASE-CONTEXTシステムプロパティでオーバーライドできます。

※ ベースコンテキスト(base context)はプロパティインジェクションなどのJSR-352プロセッサで処理しないため、追加処理の必要なコンポーネントはそこで設定しないでください*2

1.2.2. Launching a JSR-352 based job

JSR-352でバッチジョブを実行する方法は大変シンプルです。ジョブを実行するのに必要なコードは以下の通りです。

JobOperator operator = BatchRuntime.getJobOperator();
jobOperator.start("myJob", new Properties());

上記は開発者にとって分かりやすいですが、落とし穴があります*3。Spring Batchは裏側でいくつかのインフラとなるbeanを初期化します。これは開発者がオーバーライド可能です。以下は初回BatchRuntime.getJobOperator()呼び出し時に初期化される一覧です。

Bean Name Default Configuration Notes
dataSource 設定値使用のApache DBCP BasicDataSource デフォルトではHSQLDBが初期化される
transactionManager org.springframework.jdbc.datasource.DataSourceTransactionManager 上記で定義するdataSource beanを参照する
A Datasource initializer   batch.drop.scriptbatch.schema.scriptプロパティのスクリプトを実行するために設定します。デフォルトではHSQLDBスキーマスクリプトを実行します。この振る舞いはbatch.data.source.initプロパティでdisableに出来ます。
jobRepository JDBCベースのSimpleJobRepository JobRepositoryは上述のデータソースとトランザクションマネージャを使用します。スキーマのテーブルプレフィクス(デフォルトBATCH_)はbatch.table.prefixプロパティで設定可能です
jobLauncher org.springframework.batch.core.launch.support.SimpleJobLauncher job実行に使用
batchJobOperator org.springframework.batch.core.launch.support.SimpleJobOperator JsrJobOperatorが各種機能を提供するのにこのbeanをラップする
jobExplorer org.springframework.batch.core.explore.support.JobExplorerFactoryBean JsrJobOperatorが提供する機能のルックアップに使用する
jobParametersConverter org.springframework.batch.core.jsr.JsrJobParametersConverter JobParametersConverterのJSR-352固有実装
jobRegistry org.springframework.batch.core.configuration.support.MapJobRegistry SimpleJobOperatorが使用
placeholderProperties org.springframework.beans.factory.config.PropertyPlaceholderConfigure 上述のプロパティを設定するためにbatch-${ENVIRONMENT:hsql}.propertiesをロードする。ENVIRONMENTはシステムプロパティ(デフォルトhsql)でSpring BatchがサポートするDBを指定可能です

※ 上記beanのいずれもJSR-352ベースのjob実行ではオプション扱いです。どのbeanも必要に応じてカスタマイズのためにオーバライドが可能です。

1.3. Dependency Injection

JSR-352はかなりの程度Spring Batchのプログラミングモデルをベースにしています。よって、明示的に何らかのDI実装を用意する必要はありません。Spring BatchはJSR-352で定義するバッチアーティファクトのロードに3つの方法をサポートしています。

  • Implementation Specific Loader - Spring BatchはSpring上で動作するので、JSR-352バッチジョブ内のSpring DIもサポートする。
  • Archive Loader - JSR-352は論理名とクラス名をマッピングするbatch.xmlを定義する。このファイルを使用する場合は/META-INF/ディレクトリ内に配置する。
  • Thread Context Class Loader - JSR-352はインライン完全修飾クラス名によるJSLでバッチアーティファクトの実装を指定する設定が可能です。Spring BatchはJSR-352設定のjobも同様にサポートします。

JSR-352ベースのバッチジョブでSpringのDIを使うには、Springアプリケーションコンテキストを使用するバッチアーティファクトをbeanとして設定します。bean定義後は、jobはそのbeanをbatch.xmlで定義されたかのように参照可能です。

Java Configuration

@Configuration
public class BatchConfiguration {

    @Bean
    public Batchlet fooBatchlet() {
        FooBatchlet batchlet = new FooBatchlet();
        batchlet.setProp("bar");
        return batchlet;
       }
}


<?xml version="1.0" encoding="UTF-8"?>
<job id="fooJob" xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="1.0">
    <step id="step1" >
        <batchlet ref="fooBatchlet" />
    </step>
</job>

Springコンテキストのassembly(importsなど)は、Springベースのアプリケーション同様に、JSR-352のjobでも動作しまう。JSR-352ベースのjobとの唯一の相違点は、コンテキスト定義のエントリーポイントは/META-INF/batch-jobs/のjob定義となります。

thread context class loaderの方法を使うには、refで完全修飾クラス名を渡します。この方法かbatch.xmlを使う場合、参照クラスはbean生成のために引数無しコンストラクタが必要です。

<?xml version="1.0" encoding="UTF-8"?>
<job id="fooJob" xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="1.0">
    <step id="step1" >
        <batchlet ref="io.spring.FooBatchlet" />
    </step>
</job>

1.4. Batch Properties

1.4.1. Property Support

JSR-352は、JSLの設定方法を使用して、Job,Step,バッチアーティファクトレベルで定義するプロパティを使用可能です。バッチプロパティは以下の方法で各レベルで設定します。

<properties>
    <property name="propertyName1" value="propertyValue1"/>
    <property name="propertyName2" value="propertyValue2"/>
</properties>

Propertiesは各バッチアーティファクトに対して設定します。

1.4.2. @BatchProperty annotation

バッチアーティファクトPropertiesはクラスのフィールドに@BatchProperty@Inject(仕様上両方のアノテーションが必要)を付与することで参照します。JSR-352の定義上、プロパティのフィールドはString型が必須です。型変換の実装と実行は開発者の責任です。

javax.batch.api.chunk.ItemReaderアーティファクトは上述のプロパティ定義と共に使用可能で以下のようにアクセスします。

public class MyItemReader extends AbstractItemReader {
    @Inject
    @BatchProperty
    private String propertyName1;

    ...
}

"propertyName1"フィールドの値は"propertyValue1"になります。

1.4.3. Property Substitution

デフォルトプロパティ(Property substitution)*4演算子とシンプルな条件付きの式を使います。一般的な使用法は#{operator['key']}です。

サポートする演算子は以下です。

  • jobParameters - job開始/再開後にjobパラメータ値にアクセス。
  • jobProperties - JSLのjobレベルのプロパティにアクセス。
  • systemProperties - 名前付きシステムプロパティにアクセス。
  • partitionPlan - パーティション化stepのパーティーションプランの名前付きプロパティにアクセス。
#{jobParameters['unresolving.prop']}?:#{systemProperties['file.separator']}

左側が使いたい値で、右側がデフォルト値です。この例では、#{jobParameters['unresolving.prop']}が解決不能であれば、システムプロパティfile.separatorの値となります。両方とも解決不能であれば、空文字列を返します。複数の条件を使用可能で、';'で区切ります。

1.5. Processing Models

JSR-352にはSpring Batchと同様の2つの処理モデルがあります。

  • Item based processing - javax.batch.api.chunk.ItemReaderjavax.batch.api.chunk.ItemWriter、任意でjavax.batch.api.chunk.ItemProcessor
  • Task based processing - javax.batch.api.Batchletの実装。この処理モデルはorg.springframework.batch.core.step.tasklet.Taskletと同様。

1.5.1. Item based processing

このコンテキストにおけるitemベースの処理はItemReaderで読み込むアイテム数がセットするchunkサイズです。stepの設定には、item-count(デフォルト10)を指定し、任意でcheckpoint-policyをitem(デフォルト値)に設定します。

...
<step id="step1">
    <chunk checkpoint-policy="item" item-count="3">
        <reader ref="fooReader"/>
        <processor ref="fooProcessor"/>
        <writer ref="fooWriter"/>
    </chunk>
</step>
...

itemベースのチェックポイントを使う場合、time-limitも使えます。指定アイテム数を処理すべきタイムリミットを設定します。タイムアウトに達すると、chunkは読み込んだitemがあったとしても、item-countの設定に関わらず、完了します。

1.5.2. Custom checkpointing

JSR-352はstepのチェックポイント内のコミットインターバルで処理を呼び出します。上述の通りitemベースのチェックポイントはその1つです。しかし、多くの場合これは十分にロバストではありません。このため、仕様でjavax.batch.api.chunk.CheckpointAlgorithmインタフェースの実装によりカスタムチェックポイントを作成可能です。この機能はSpring Batchのカスタムcompletion policyと同等です。CheckpointAlgorithmの実装を使うには、checkpoint-policyを、以下のようにstepのfooCheckpointerCheckpointAlgorithmの実装を参照します。

...
<step id="step1">
    <chunk checkpoint-policy="custom">
        <checkpoint-algorithm ref="fooCheckpointer"/>
        <reader ref="fooReader"/>
        <processor ref="fooProcessor"/>
        <writer ref="fooWriter"/>
    </chunk>
</step>
...

1.6. Running a job

JSR-352ベースのjob実行のエントリーポイントはjavax.batch.operations.JobOperatorです。Spring Batchにはこのインタフェースの実装(org.springframework.batch.core.jsr.launch.JsrJobOperator)があります。この実装クラスはjavax.batch.runtime.BatchRuntimeがロードします。JSR-352ベースのバッチジョブの起動は以下のように実装します。

JobOperator jobOperator = BatchRuntime.getJobOperator();
long jobExecutionId = jobOperator.start("fooJob", new Properties());

上記コードは以下を行います。

  • ベースApplicationContextの初期化 - バッチの各種機能を使えるように、フレームワークで基盤部分の初期化を行います。これはJVMごとに1回発生します。初期化コンポーネント@EnableBatchProcessingのそれと似ています。詳細はJsrJobOperatorjavadocを参照してください。
  • jobが要求するApplicationContextのロード - 上の例では、フレームワークは/META-INF/batch-jobsのfooJob.xmlを参照し、前に解説した共有コンテキストの子コンテキストとしてロードします。
  • job起動 - コンテキスト内に定義したjobを非同期に実行する。JobExecutionのidが返される。

※ すべてのJSR-352ベースのバッチジョブは非同期に実行します。

SimpleJobOperatorJobOperator#startを呼ぶ場合、Spring Batchは初回実行か以前の実行のリトライかを判断します。JSR-352のJobOperator#start(String jobXMLName, Properties jobParameters)を使う場合、フレームワークは常に新規JobInstanceを生成します(JSR-352のjobパラメータは一意性を持たない(JSR-352 job parameters are non-identifying))。jobをリスタートするには、JobOperator#restart(long executionId, Properties restartParameters)を使用して下さい。

1.7. Contexts

JSR-352には2つのコンテキストオブジェクトがあり、jobのメタデータにアクセスするものと、バッチアーティファクトからstepにアクセスします。javax.batch.runtime.context.JobContextjavax.batch.runtime.context.StepContextです。これらはstepレベルアーティファクトBatchlet, ItemReaderなど)で利用可能で、JobContextはjobレベルアーティファクトJobListenerなど)でも利用可能です。

カレントのスコープ内でJobContextStepContextの参照を得るには、@Injectを使います。

@Inject
JobContext jobContext;

@Autowire for JSR-352 contexts Springの@Autowireは上記コンテキストのインジェクションには使用出来ません。

Spring Batchでは、JobContextStepContextはこれらに対応するexecutionオブジェクト(JobExecutionStepExecution)をラップします。StepContext#setPersistentUserData(Serializable data)はSpring BatchのStepExecution#executionContextに保存します。

1.8. Step Flow

JSR-352ベースのjobの内部の、stepのflowはSpring Batchのそれと同様の動作します。ただし、多少微妙に異なる点があります。

  • Decision’s are steps - In a regular Spring Batch job, a decision is a state that does not have an independent StepExecution or any of the rights and responsibilities that go along with being a full step.. However, with JSR-352, a decision is a step just like any other and will behave just as any other steps (transactionality, it gets a StepExecution, etc). This means that they are treated the same as any other step on restarts as well.*5
  • next属性とstep transitions - Spring Batchのjobでは、これらは同一stepで一緒に使う事が可能です。JSR-352でも同一stepで使う事が可能で、next属性が評価において優先します。
  • transitions要素の順序 - Spring Batchのjobでは、transition要素は最も一致するものからしないものにソートしてその順序で評価します。JSR-352のjobsはXMLで指定した順序でtransition要素を評価します。

1.9. Scaling a JSR-352 batch job

Spring Batchには4つのスケーリングの方法があります(最後の2つは複数JVMで実行)。

  • Split - パラレルに複数stepを実行。
  • Multiple threads - 複数スレッドで単一stepを実行。
  • Partitioning - パラレル処理でデータを分割(master/slave)。
  • Remote Chunking - ロジックのprocessor pieceをリモートに実行。

JSR-352はバッチジョブのスケーリングに2つのオプションがあります。両オプションとも単一JVMのみをサポートします。

  • Split - Spring Batchのものと同等。
  • Partitioning - Spring Batchと概念的には同等だが微妙に実装は異なる。

1.9.1. Partitioning

概念的には、JSR-352のパーティショニングはSpring Batchと同等です。処理対象の入力の識別のためにメタデータが各スレーブに渡され、スレーブはマスターに処理結果を返します。しかし、いくつか重要な違いがあります。

  • Partitioned Batchlet - 複数スレッドで複数インスタンスBatchletを動かす。各インスタンスJSLもしくはPartitionPlanのプロパティをそれぞれ固有で持つ。
  • PartitionPlan - Spring Batchのパーティショニングでは、ExecutionContextを各パーティションに渡します。JSR-352では、単一のjavax.batch.api.partition.PartitionPlanメタデータProperties配列と共に各パーティションに渡します。
  • PartitionMapper - JSR-352はパーティションメタデータの生成に2種類の方法があります。1つ目はJSL (partition properties)です。2つ目はjavax.batch.api.partition.PartitionMapperの実装です。機能的には、このインタフェースはSpring Batchのorg.springframework.batch.core.partition.support.Partitionerと、パーティショニングのメタデータをプログラム的に生成する、という点で似ています。
  • StepExecutions - Spring Batchでは、パーティションstepはmaster/slaveで動作します。JSR-352でも同一設定で動作します。ただし、スレーブのstepはofficial StepExecutionsを取得しません。このため、JsrJobOperator#getStepExecutions(long jobExecutionId)はマスターにだけStepExecutionを返します。

※ 子StepExecutionsはジョブリポジトリには存在し、JobExplorerおよびSpring Batch Adminを介して利用可能です。

  • Compensating logic - Spring Batchがstepでパーティショニングのmaster/slaveを実装する場合、StepExecutionListenersで何らかの補正処理を実行可能です。しかし、JSR-352のslaveは、エラー発生時の補正処理と、動的なexit status設定が可能なように、コンポーネントのコレクションを渡します。コンポーネントは以下の通りです。
Artifact Interface Description
javax.batch.api.partition.PartitionCollector スレーブstepからマスターに情報を送り返す手段の提供。スレーブスレッドごとに1インスタンス
javax.batch.api.partition.PartitionAnalyzer PartitionCollectorが収集する情報と完了パーティションの結果ステータスを受け取るエンドポイント。
javax.batch.api.partition.PartitionReducer パーティションstepの補正ロジックの提供。

1.10. Testing

JSR-352ベースのjobはすべて非同期実行なため、job完了の確認が困難です。テスト用に、Spring Batchはorg.springframework.batch.test.JsrTestUtilsを提供します。このユーティリティークラスは、jobの開始・jobのリスタート・完了待ち、が出来ます。jobが完了するとJobExecutionを返します。

*1:あんま興味無いセクションなんで他にまして訳がテキトーな点に注意してください。

*2:The base context is not processed by the JSR-352 processors for things like property injection so no components requiring that additional processing should be configured there. が原文

*3:the devil is in the details. が原文。ぐぐれば分かるが「思わぬところに落とし穴」という意味合いの慣用句。オサレに訳せなかったんで直訳

*4:正確に訳すのなら「代替プロパティ」とかになるんだろうけど。まぁええわ。

*5:よくわからんかった

Spring Batch 4.1.x - Reference Documentation - Common Batch Patternsのテキトー翻訳

https://docs.spring.io/spring-batch/4.1.x/reference/html/common-patterns.html#commonPatterns

https://qiita.com/kagamihoge/items/12fbbc2eac5b8a5ac1e0 俺の訳一覧リスト

1. Common Batch Patterns

ある種のジョブはSpring Batch標準コンポーネントのみの組み合わせで構築できます。ItemReaderItemWriterの実装は様々なケースに適用可能です。しかし、基本的には、カスタムコードの実装が必要です。アプリケーション開発者に対するSpring BatchのエントリーポイントはTasklet, ItemReader, ItemWriterと各種リスナーです。シンプルなバッチジョブではSpring BatchのItemReaderを使用出来ますが、ItemWriterItemProcessorにカスタムの処理な書き込みが必要となる場合があります。

このチャプターでは、カスタムビジネスロジックにおける共通パターンの例について解説します。これらは主としてリスナーを活用します。なお、ItemReaderItemWriter実装はリスナーも実装可能です。

1.1. Logging Item Processing and Failures

1アイテムごとにstepでエラーハンドリングを行い、特別なチャネルにロギングしたりDBにレコードを追加したり、をするための共通パターンです。chunk指向Step(stepファクトリビーンで生成)では単純に、readエラーはItemReadListenerwriteエラーはItemWriteListenerの実装により実現します。以下コードはreadとwriteエラーログ出力するリスナの例です。

public class ItemFailureLoggerListener extends ItemListenerSupport {

    private static Log logger = LogFactory.getLog("item.error");

    public void onReadError(Exception ex) {
        logger.error("Encountered error on read", e);
    }

    public void onWriteError(Exception ex, List<? extends Object> items) {
        logger.error("Encountered error on write", ex);
    }
}

リスナ実装したら以下例のようにstepに登録します。

Java Configuration

@Bean
public Step simpleStep() {
        return this.stepBuilderFactory.get("simpleStep")
                                ...
                                .listener(new ItemFailureLoggerListener())
                                .build();
}

※ リスナーのonError()での処理は、後にロールバックされるトランザクション内での処理になります。DBなどトランザクショナルなリソースをonError()で使う場合、リスナーメソッドに宣言的トランザクション(詳細はSpring Core Reference Guide参照)を追加し、propagation attributeをREQUIRES_NEWにしてください。

1.2. Stopping a Job Manually for Business Reasons

Spring BatchにはJobLauncherstop()がありますが、これはアプリケーションプログラマというよりオペレータが使うものです。ただ、ビジネスロジック内でのジョブ実行停止したい場合がありえます。

一番シンプルな方法はRuntimeExceptionのスローです(無期限リトライやスキップしない例外)。例えば以下ではカスタム例外を使用しています。

public class PoisonPillItemProcessor<T> implements ItemProcessor<T, T> {

    @Override
    public T process(T item) throws Exception {
        if (isPoisonPill(item)) {
            throw new PoisonPillException("Poison pill detected: " + item);
        }
        return item;
    }
}

別の方法としてstepを止めるには、以下例のように、ItemReadernullを返します。

public class EarlyCompletionItemReader implements ItemReader<T> {

    private ItemReader<T> delegate;

    public void setDelegate(ItemReader<T> delegate) { ... }

    public T read() throws Exception {
        T item = delegate.read();
        if (isEndItem(item)) {
            return null; // ここでstep終了
        }
        return item;
    }

}

上の例はCompletionPolicyのデフォルト実装のnullがバッチ完了を意味する挙動を利用しています。より複雑な完了ポリシーを実装してStepに設定するにはSimpleStepFactoryBeanを使用します。

Java Configuration

@Bean
public Step simpleStep() {
        return this.stepBuilderFactory.get("simpleStep")
                                .<String, String>chunk(new SpecialCompletionPolicy())
                                .reader(reader())
                                .writer(writer())
                                .build();
}

また別の方にStepExecutionにフラグを設定し、アイテム処理中にフレームワーク内でStep実装がこれをチェックします。これを実装するには、StepExecutionにアクセスするためにStepListenerを実装してStepに登録します。以下はフラグを設定するリスナーの例です。

public class CustomItemWriter extends ItemListenerSupport implements StepListener {

    private StepExecution stepExecution;

    public void beforeStep(StepExecution stepExecution) {
        this.stepExecution = stepExecution;
    }

    public void afterRead(Object item) {
        if (isPoisonPill(item)) {
            stepExecution.setTerminateOnly(true);
       }
    }

}

フラグをセットする場合、デフォルトの振る舞いはstepでJobInterruptedExceptionをスローします。この振る舞いはStepInterruptionPolicyで制御できます。なお、例外スローかしないかの選択肢しか無いので、ジョブは常にabnormal endingになります。

1.3. Adding a Footer Record

フラットファイルに書き込む場合、すべての処理完了後にファイル末尾にフッター行を追加したい場合があります。これはSpring BatchのFlatFileFooterCallbackにより実装できます。FlatFileFooterCallback(と対になるFlatFileHeaderCallback)はFlatFileItemWriterのオプションプロパティで以下のようにwriterに追加します。

Java Configuration

@Bean
public FlatFileItemWriter<String> itemWriter(Resource outputResource) {
        return new FlatFileItemWriterBuilder<String>()
                        .name("itemWriter")
                        .resource(outputResource)
                        .lineAggregator(lineAggregator())
                        .headerCallback(headerCallback())
                        .footerCallback(footerCallback())
                        .build();
}

フッターのコールバックには1つだけメソッドがあり、フッター書き込み時に呼び出されます。

public interface FlatFileFooterCallback {

    void writeFooter(Writer writer) throws IOException;

}

1.3.1. Writing a Summary Footer

フッター行に対するよくある要望に、出力処理中の情報を集約してファイル末尾にそれを追加する、があります。このフッターはファイルの要約やチェックサムに使われます。

例えば、バッチジョブがフラットファイルにTrade行を書き込むとして、全Tradesの総合計をフッターに配置したい場合、ItemWriter実装を以下のようにします。

public class TradeItemWriter implements ItemWriter<Trade>,
                                        FlatFileFooterCallback {

    private ItemWriter<Trade> delegate;

    private BigDecimal totalAmount = BigDecimal.ZERO;

    public void write(List<? extends Trade> items) throws Exception {
        BigDecimal chunkTotal = BigDecimal.ZERO;
        for (Trade trade : items) {
            chunkTotal = chunkTotal.add(trade.getAmount());
        }

        delegate.write(items);

        // アイテム正常書き込み後に総合計を加算
        totalAmount = totalAmount.add(chunkTotal);
    }

    public void writeFooter(Writer writer) throws IOException {
        writer.write("Total Amount Processed: " + totalAmount);
    }

    public void setDelegate(ItemWriter delegate) {...}
}

TradeItemWritertotalAmountTradeアイテム書き込み後に加算します。すべてのTrade処理後、フレームワークwriteFooterを呼び、ファイルにtotalAmountを挿入します。なお、writeには一時変数chunkTotalがあり、これにchunkのTradeの合計を格納します。これは、writeでskipが発生した場合はtotalAmountを変更しないためです。writeメソッドを最後まで実行し、例外がスローされなかったことを確認できたら、totalAmountを更新します。

writeFooterの使用には、TradeItemWriterFlatFileFooterCallbackの実装クラス)をfooterCallbackとしてFlatFileItemWriterにワイヤリングします。以下はその方法の例です。

Java Configuration

@Bean
public TradeItemWriter tradeItemWriter() {
        TradeItemWriter itemWriter = new TradeItemWriter();

        itemWriter.setDelegate(flatFileItemWriter(null));

        return itemWriter;
}

@Bean
public FlatFileItemWriter<String> flatFileItemWriter(Resource outputResource) {
        return new FlatFileItemWriterBuilder<String>()
                        .name("itemWriter")
                        .resource(outputResource)
                        .lineAggregator(lineAggregator())
                        .footerCallback(tradeItemWriter())
                        .build();
}

このTradeItemWriterような作り方はStepが非リスタート可能の場合のみ正しく動作します。これは、このクラスはstateful(totalAmountがある)だが、そのtotalAmountをDBに永続化していないためです。よって、リスタート時にtotalAmountを取得できません。このクラスをリスタート可能にするには、ItemStreamは以下例のようにopenupdateを実装します。

public void open(ExecutionContext executionContext) {
    if (executionContext.containsKey("total.amount") {
        totalAmount = (BigDecimal) executionContext.get("total.amount");
    }
}

public void update(ExecutionContext executionContext) {
    executionContext.put("total.amount", totalAmount);
}

updateメソッドはExecutionContexttotalAmountの現在値を保存します。openメソッドはExecutionContextからtotalAmountを取得し処理開始時の値として使用し、Stepの前回終了時点からリスタートします。

1.4. Driving Query Based ItemReaders

chapter on readers and writersで、ページングを使用するDB入力について解説しました。DB2など、たいていのDBで、他のオンラインアプリケーションなどで必要なテーブルを読み込む場合、過度の悲観的ロックは問題を起こす場合があります。加えて、過度に大きなデータセットに対するカーソルオープンが特定DBで問題を起こす場合があります。よって、データ読み取りに'Driving Query'を取るプロジェクトがあります。この方針は、以下図のように、返す必要のあるオブジェクト全体ではなく、キーをイテレートする動作をします。

Figure 1. Driving Query Job

上記例は、カーソルベースのサンプルで使用したのと同じ'FOO'テーブルです。ただし、行全体を選択するのではなく、ここのSQLステートメントではIDのみ選択しています。よって、readFOOオブジェクトではなく、Integerを返します。このIDの数値を使用し、後でFooオブジェクトという"詳細"を取得します。

Figure 2. Driving Query Example

ItemProcessorはdriving queryから得たキーを基に'Foo'オブジェクトへと変換します。DAOを使用してキーを基にオブジェクト全体をクエリします。

1.5. Multi-Line Records

基本的にはフラットファイルは各レコードを1行になりますが、レコードを複数のフォーマットで複数行に展開することもあります。以下はそうした展開例の抜粋です。

HEA;0013100345;2007-02-15
NCU;Smith;Peter;;T;20014539;F
BAD;;Oak Street 31/A;;Small Town;00235;IL;US
FOT;2;2;267.34

1行の開始は'HEA'で始まり、1行の最終行は'FOT'で始まります。これを正しく扱うには以下を考慮する必要があります。

  • 1度に1レコード読み込むのでなく、ItemReaderは複数行レコードのグループとして読み込み、そのグループをItemWriterに渡します。
  • 各行の種類ごとにトークン処理を行う。

単一レコードを複数行に展開し、かつ、全体で何行来るかは事前に不明なため、ItemReaderはレコード全体を注意深く読みこむ必要があります。これを行うには、FlatFileItemReaderのラッパーとしてItemReaderを実装します。

Java Configuration

@Bean
public MultiLineTradeItemReader itemReader() {
        MultiLineTradeItemReader itemReader = new MultiLineTradeItemReader();

        itemReader.setDelegate(flatFileItemReader());

        return itemReader;
}

@Bean
public FlatFileItemReader flatFileItemReader() {
        FlatFileItemReader<Trade> reader = new FlatFileItemReaderBuilder<Trade>()
                        .name("flatFileItemReader")
                        .resource(new ClassPathResource("data/iosample/input/multiLine.txt"))
                        .lineTokenizer(orderFileTokenizer())
                        .fieldSetMapper(orderFieldSetMapper())
                        .build();
        return reader;
}

各行に応じた適切なトークン分割をするため、特に重要なのは固定幅入力なので、デリゲート先のFlatFileItemReaderに対してPatternMatchingCompositeLineTokenizerを使います。FlatFileItemReader in the Readers and Writers chapterに詳細があります。それから、デリゲート先のreaderは各行をラップ元のItemReaderFieldSetで返すためにPassThroughFieldSetMapperを使います。

Java Content

@Bean
public PatternMatchingCompositeLineTokenizer orderFileTokenizer() {
        PatternMatchingCompositeLineTokenizer tokenizer =
                        new PatternMatchingCompositeLineTokenizer();

        Map<String, LineTokenizer> tokenizers = new HashMap<>(4);

        tokenizers.put("HEA*", headerRecordTokenizer());
        tokenizers.put("FOT*", footerRecordTokenizer());
        tokenizers.put("NCU*", customerLineTokenizer());
        tokenizers.put("BAD*", billingAddressLineTokenizer());

        tokenizer.setTokenizers(tokenizers);

        return tokenizer;
}

ラッパーはレコード終端を解釈する必要があります。よって、レコード終端に達するまで、デリゲート先のreadを繰り返し呼びます。各行を読み込むためには、ラッパーで返すアイテムを組み立てる必要があります。フッターに達すると、アイテムはItemProcessorItemWriterに渡せるようになります。

private FlatFileItemReader<FieldSet> delegate;

public Trade read() throws Exception {
    Trade t = null;

    for (FieldSet line = null; (line = this.delegate.read()) != null;) {
        String prefix = line.readString(0);
        if (prefix.equals("HEA")) {
            t = new Trade(); // Record must start with header
        }
        else if (prefix.equals("NCU")) {
            Assert.notNull(t, "No header was found.");
            t.setLast(line.readString(1));
            t.setFirst(line.readString(2));
            ...
        }
        else if (prefix.equals("BAD")) {
            Assert.notNull(t, "No header was found.");
            t.setCity(line.readString(4));
            t.setState(line.readString(6));
          ...
        }
        else if (prefix.equals("FOT")) {
            return t; // Record must end with footer
        }
    }
    Assert.isNull(t, "No 'END' was found.");
    return null;
}

1.6. Executing System Commands

バッチジョブ内から外部のコマンドを呼ぶ必要があるケースは多いです。そうした処理はスケジューラで別途に開始出来ますが、実行時のメタデータの利点が失われます。また、マルチステップジョブを複数ジョブに分割する必要も発生します。

よくあるケースなので、Spring Batchは以下のようにシステムコマンドを呼ぶTaskletを用意しています。

Java Configuration

@Bean
public SystemCommandTasklet tasklet() {
        SystemCommandTasklet tasklet = new SystemCommandTasklet();

        tasklet.setCommand("echo hello");
        tasklet.setTimeout(5000);

        return tasklet;
}

1.7. Handling Step Completion When No Input is Found

DBからの取得結果やファイルが0件なケースは良くあります。Stepでは単純に何も処理が無いので0アイテムを読み込んで完了します。Spring Batch標準クラスのItemReaderはすべてデフォルトではこの方針になっています。入力があるのに何も書き込まないのは、混乱を招く場合があります(ファイル名がおかしくなったり、他同様な問題の発生)。For this reason, the metadata itself should be inspected to determine how much work the framework found to be processed. しかし、入力0件が例外的の場合にはどうすれば良いでしょうか。この場合、0件処理のメタデータをプログラム的にチェックして失敗とするのがベストです。これは一般的なユースケースなため、Spring Batchはその機能をリスナーNoWorkFoundStepExecutionListenerで提供しています。定義は以下の通りです。

public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport {

    public ExitStatus afterStep(StepExecution stepExecution) {
        if (stepExecution.getReadCount() == 0) {
            return ExitStatus.FAILED;
        }
        return null;
    }

}

上のStepExecutionListenerは'afterStep'でStepExecutionreadCountプロパティで読み込み0件かどうかをチェックします。0件の場合、FAILEDのexit codeでリターンし、Stepは失敗になります。そうでない場合、nullが返され、Stepのステータスは何も変更しません。

1.8. Passing Data to Future Steps

stepから別のstepにデータを渡したい場合が良くあります。これにはExecutionContextを使います。2つのExecutionContextsStepJob、はそれぞれ一長一短です。StepExecutionContextはstep中のみ、JobExecutionContextJob全体です。また、StepExecutionContextStepのchunkがコミットする際に更新し、JobExecutionContextStep終了時にだけ更新します。

このためStep実行中はStepExecutionContextに全データを保存してください。これによりStep実行中にデータが適切に保存されます。もしデータをJobExecutionContextに置く場合、Stepの実行中には永続化しません。Stepが失敗するとそのデータはロストします。

public class SavingItemWriter implements ItemWriter<Object> {
    private StepExecution stepExecution;

    public void write(List<? extends Object> items) throws Exception {
        // ...

        ExecutionContext stepContext = this.stepExecution.getExecutionContext();
        stepContext.put("someKey", someObject);
    }

    @BeforeStep
    public void saveStepExecution(StepExecution stepExecution) {
        this.stepExecution = stepExecution;
    }
}

次以降のStepsでデータを利用可能にするには、step終了後にJobExecutionContextへ"昇格"の必要があります。Spring BatchではExecutionContextPromotionListenerを使います。このリスナーにはExecutionContextで昇格させたいキーを設定します。また、オプションで、昇格するexit codeのリストパターンも設定出来ます(COMPLETEDがデフォルト)。他リスナー同様、以下サンプルのようにStepに登録します。

Java Configuration

@Bean
public Job job1() {
        return this.jobBuilderFactory.get("job1")
                                .start(step1())
                                .next(step1())
                                .build();
}

@Bean
public Step step1() {
        return this.stepBuilderFactory.get("step1")
                                .<String, String>chunk(10)
                                .reader(reader())
                                .writer(savingWriter())
                                .listener(promotionListener())
                                .build();
}

@Bean
public ExecutionContextPromotionListener promotionListener() {
        ExecutionContextPromotionListener listener = new ExecutionContextPromotionListener();

        listener.setKeys(new String[] {"someKey" });

        return listener;
}

以下サンプルのように、JobExecutionContextから保存した値を取得するには以下のようにします。

public class RetrievingItemWriter implements ItemWriter<Object> {
    private Object someObject;

    public void write(List<? extends Object> items) throws Exception {
        // ...
    }

    @BeforeStep
    public void retrieveInterstepData(StepExecution stepExecution) {
        JobExecution jobExecution = stepExecution.getJobExecution();
        ExecutionContext jobContext = jobExecution.getExecutionContext();
        this.someObject = jobContext.get("someKey");
    }
}