Macchinetta Batch Framework (2.x) Development Guideline - version 2.5.0.RELEASE, 2024-3-28
> INDEX

Overview

Webコンテナ内でジョブを非同期で実行するための方法について説明する。

本機能は、チャンクモデルとタスクレットモデルとで同じ使い方になる。

Webコンテナによるジョブの非同期実行とは

ジョブを含めたWebアプリケーションをWebコンテナにデプロイし、 送信されたリクエストの情報をもとにジョブを実行することを指す。
ジョブの実行ごとに1つのスレッドを割り当てた上で並列に動作するため、 他のジョブやリクエストに対する処理とは独立して実行できる。

提供機能

Macchinetta Batch 2.xでは、非同期実行(Webコンテナ)向けの実装は提供しない。
本ガイドラインにて実現方法を提示するのみとする。
これは、Webアプリケーションの起動契機はHTTP/SOAP/MQなど多様であるため、 ユーザにて実装することが適切と判断したためである。

利用前提
  • アプリケーションの他にWebコンテナが必要となる。

  • ジョブの実装以外に必要となる、Webアプリケーション、クライアントは動作要件に合わせて別途実装する。

  • ジョブの実行状況および結果はJobRepositoryに委ねる。
    また、Webコンテナ停止後にもJobRepositoryからジョブの実行状況および結果を参照可能とするため、インメモリデータベースではなく、永続性が担保されているデータベースを使用する。

活用シーン

"非同期実行(DBポーリング) - Overview"と同様である。

非同期実行(DBポーリング)との違い

アーキテクチャ上、非同期実行時の即時性と、要求管理テーブルの有無、の2点が異なる。
"非同期実行(DBポーリング)"は要求管理テーブルに登録された複数のジョブが一定の周期で非同期実行される。
それに対し、本機能は要求管理テーブルを必要とせず代わりにWebコンテナ上で非同期実行を受け付ける。
Webリクエスト送信により直ちに実行するため、起動までの即時性が求められるショートバッチに向いている。

Architecture

本方式による非同期ジョブはWebコンテナ上にデプロイされたアプリケーション(war)として動作するが、 ジョブ自身はWebコンテナのリクエスト処理とは非同期(別スレッド)で動作する。

sequence of async web
図 1. 非同期実行(Webコンテナ)のシーケンス図
ジョブの起動
  1. Webクライアントは実行対象のジョブをWebコンテナに要求する。

  2. JobControllerはSpring BatchのJobOperatorに対しジョブの実行開始を依頼する。

  3. ThreadPoolTaskExecutorによって非同期でジョブを実行する。

  4. 実行された対象のジョブを一意に判別するためのジョブ実行ID(job execution id)を返却する。

  5. JobControllerはWebクライアントに対し、ジョブ実行IDを含むレスポンスを返却する。

  6. 目的のジョブを実行する。

    • ジョブの結果はJobRepositoryに反映される。

  7. Jobが実行結果を返却する。これはクライアントへ直接通知できない。

ジョブの実行結果確認
  1. Webクライアントはジョブ実行IDをJobControllerをWebコンテナに送信する。

  2. JobControllerはジョブ実行IDを用いJobExplorerにジョブの実行結果を問い合わせる。

  3. JobExplorerはジョブの実行結果を返却する。

  4. JobControllerはWebクライアントに対しレスポンスを返却する。

    • レスポンスにはジョブ実行IDを設定する。

Webコンテナによるリクエスト受信後、ジョブ実行ID払い出しまでがリクエスト処理と同期するが、 以降のジョブ実行はWebコンテナとは別のスレッドプールで非同期に行われる。
これは再度リクエストで問い合わせを受けない限り、Webクライアント側では非同期ジョブの 実行状態が検知できないことを意味する。

このためWebクライアント側では1回のジョブ実行で、リクエストを「ジョブの起動」で1回、 「結果の確認」が必要な場合は加えてもう1回、Webコンテナにリクエストを送信する必要がある。
特に初回の「ジョブの起動」時に見え方が異なる異常検知については、 後述のジョブ起動時における異常発生の検知についてで説明する。

JobRepositoryJobExplorerを使用して直接RDBMSを参照し、ジョブの実行状態を確認することもできる。 ジョブの実行状態・結果を参照する機能の詳細については、ジョブの管理を参照。

ジョブ実行ID(job execution id)の取り扱いについて

ジョブ実行IDは起動対象が同じジョブ、同じジョブパラメータであっても、ジョブ起動ごとに異なるシーケンス値が払い出される。
リクエスト送信により受付が行われたジョブ実行IDはJobRepositoryにより外部RDBMSで永続化される。
しかし、Webクライアントの障害などによりこのIDが消失した場合、ジョブ実行状況の特定・追跡が困難となる。
このため、Webクライアント側ではレスポンスとして返却されたジョブ実行IDをログに記録するなど、ジョブ実行IDの消失に備えておくこと。

ジョブ起動時における異常発生の検知について

Webクライアントからジョブの起動リクエストを送信後、ジョブ実行ID払い出しを境にして異常検知の見え方が異なる。

  • ジョブ起動時のレスポンスにて異常がすぐ検知できるもの

    • 起動対象のジョブが存在しない。

    • ジョブパラメータの形式誤り。

  • ジョブ起動後、Webコンテナに対しジョブ実行状態・結果の問い合わせが必要となるもの

    • ジョブの実行ステータス

    • 非同期ジョブ実行で使用されるスレッドプールが枯渇したことによるジョブの起動失敗

「ジョブ起動時の異常」は Spring MVCコントローラ内で発生する例外として検知できる。 ここでは説明を割愛するので、別途 Macchinetta Server 1.x 開発ガイドラインの 例外のハンドリングの実装を参照。

また、ジョブパラメータとして利用するリクエストの入力チェックは必要に応じて Spring MVC のコントローラ内で行うこと。
具体的な実装方法については、Macchinetta Server 1.x 開発ガイドラインの 入力チェックを参照。

スレッドプール枯渇によるジョブの起動失敗はジョブ起動時に捕捉できない

スレッドプール枯渇によるジョブの起動失敗は、JobOperatorから例外があがってこないため、別途確認する必要がある。 確認方法の1つは、ジョブの実行状態確認時にJobExplorerを用い、以下の条件に合致しているかどうかである。

  • ステータスがFAILEDである

  • jobExecution.getExitStatus().getExitDescription()にて、 org.springframework.core.task.TaskRejectedExceptionの例外スタックトレースが記録されている

非同期実行(Webコンテナ)のアプリケーション構成

本機能は"非同期実行(DBポーリング)"と同様、 非同期実行特有の構成としてSpring プロファイルのasyncAutomaticJobRegistrarを使用している。

一方で、これら機能を非同期実行(Webコンテナ)使用する上で、いくつかの事前知識と設定が必要となる。 "ApplicationContextの構成"を参照。
具体的なasyncプロファイルとAutomaticJobRegistrarの設定方法については "非同期実行(Webコンテナ)によるアプリケーションの実装方法について"で後述する。

ApplicationContextの構成

上述のとおり、非同期実行(Webコンテナ)のアプリケーション構成として、複数のアプリケーションモジュールが含まれている。
それぞれのアプリケーションコンテキストとBean定義についての種類、および関係性を把握しておく必要がある。

Package structure of async web
図 2. ApplicationContextの構成
BeanDefinitions structure of async web
図 3. Bean定義ファイルの構成
Package structure of async web
図 4. ApplicationContextの構成
BeanDefinitions structure of async web
図 5. Bean定義ファイルの構成

非同期実行(Webコンテナ)におけるApplicationContextでは、 バッチアプリケーションのApplicationContextはWebのコンテキスト内に取り込まれる。
個々のジョブコンテキストはこのWebコンテキストからAutomaticJobRegistrarによりモジュール化され、 Webコンテキストの子コンテキストとして動作する。

以下、それぞれのコンテキストを構成するBean定義ファイルについて説明する。

表 1. Bean定義ファイル一覧
項番 説明

(1)

共通Bean定義ファイル。
アプリケーション内では親コンテキストとなり、子コンテキストであるジョブ間で一意に共有される。

(2)

ジョブBean定義から必ずインポートされるBean定義ファイル。
Spring プロファイルが非同期実行時に指定されるasyncの場合は(1)のLaunchContextConfig.java/launch-context.xmlを読み込まない。

(3)

ジョブごとに作成するBean定義ファイル。
AutomaticJobRegistrarによりモジュラー化され、アプリケーション内ではそれぞれ独立した子コンテキストとして使用される。

(4)

DispatcherServletから読み込まれる。
ジョブBean定義のモジュラー化を行うAutomaticJobRegistrarや、ジョブの非同期・並列実行で使用されるスレッドプールであるtaskExecutorなど、非同期実行特有のBeanを定義する。
また、非同期実行では(1)のLaunchContextConfig.java/launch-context.xml
を直接インポートし親コンテキストとして一意に共有化される。

(5)

ContextLoaderListenerにより、Webアプリケーション内で共有される親コンテキストとなる。

(6)

JavaConfigのみ、共通Bean定義ファイルからインポートされる定義ファイル。

How to use

ここでは、Webアプリケーション側の実装例として、Macchinetta Server Framework (1.x)を用いて説明する。
あくまで説明のためであり、Macchinetta Server 1.xは非同期実行(Webコンテナ)の必須要件ではないことに留意してほしい。

非同期実行(Webコンテナ)によるアプリケーションの実装概要

以下の構成を前提とし説明する。

  • Webアプリケーションプロジェクトとバッチアプリケーションプロジェクトは独立し、 webアプリケーションからバッチアプリケーションを参照する。

    • Webアプリケーションプロジェクトから生成するwarファイルは、 バッチアプリケーションプロジェクトから生成されるjarファイルを含むこととなる

非同期実行の実装はArchitectureに従い、Webアプリケーション内の Spring MVCコントローラが、JobOperatorによりジョブを起動する。

Web/バッチアプリケーションプロジェクトの分離について

アプリケーションビルドの最終成果物はWebアプリケーションのwarファイルであるが、 開発プロジェクトはWeb/バッチアプリケーションで分離して実装を行うとよい。
これはバッチアプリケーション単体で動作可能なライブラリとなるため、開発プロジェクト上の試験を 容易にする他、作業境界とライブラリ依存関係を明確にする効果がある。

以降、Web/バッチの開発について、以下2つを利用する前提で説明する。

  • Macchinetta Batch 2.xによるバッチアプリケーションプロジェクト

  • Macchinetta Server 1.xによるWebアプリケーションプロジェクト

バッチアプリケーションプロジェクトの作成および具体的なジョブの実装方法については、 プロジェクトの作成チャンクモデルジョブの作成タスクレットモデルジョブの作成を参照。 ここでは、Webアプリケーションからバッチアプリケーションを起動することに終始する。

以下のバッチアプリケーションプロジェクトは、Maven archetype:generate を用いてを作成しているものとして説明する。

表 2. ジョブプロジェクト作成例
名称

groupId

jp.co.ntt.fw.macchinetta.batch.sample

artifactId

asyncbatch

version

1.0-SNAPSHOT

package

jp.co.ntt.fw.macchinetta.batch.sample

また説明の都合上、ブランクプロジェクトに初めから登録されているジョブを使用する。

表 3. 説明に用いるジョブ
名称 説明

ジョブ名

job01

非同期実行(Webコンテナ)ジョブ設計の注意点

非同期実行(Webコンテナ)の特性として個々のジョブは短時間で完了しWebコンテナ上でステートレスに 動作するケースが適している。
また複雑さを避ける上では、ジョブ定義を単一のステップのみで構成し、ステップの終了コードによるフローの分岐や 並列処理・多重処理を定義しないことが望ましい。

ジョブ実装を含むjarファイルが作成可能な状態として、Webアプリケーションの作成を行う。

Webアプリケーションの実装

Macchinetta Server 1.xが提供するブランクプロジェクトを用い、Webアプリケーションの実装方法を説明する。 詳細は、Macchinetta Server 1.x 開発ガイドラインの Webアプリケーション向け開発プロジェクトの作成を参照。

ここでは非同期実行アプリケーションプロジェクトと同様、以下の名称で作成したものとして説明する。

表 4. Webコンテナプロジェクト作成例
名称

groupId

jp.co.ntt.fw.macchinetta.batch.sample

artifactId

asyncapp

version

1.0-SNAPSHOT

package

jp.co.ntt.fw.macchinetta.batch.sample

groupIdの命名について

プロジェクトの命名は任意であるが、Maven マルチプロジェクトとしてバッチアプリケーションを Webアプリケーションの子モジュールとする場合、groupIdは統一しておくと管理しやすい。
ここでは両者のgroupIdjp.co.ntt.fw.macchinetta.batch.sampleとしている。

各種設定

バッチアプリケーションをWebアプリケーションの一部に含める

pom.xmlを編集し、バッチアプリケーションをWebアプリケーションの一部に含める。

バッチアプリケーションをjar としてNEXUSやMavenローカルリポジトリに登録し、 Webアプリケーションとは別プロジェクトとする場合はこの手順は不要である。
ただし、Mavenによりビルドされる対象が別プロジェクトとなり、バッチアプリケーションの修正を行ってもWebアプリケーションのビルド時に反映されないため注意すること。
バッチアプリケーションの修正をWebアプリケーションに反映させるためには同リポジトリに登録する必要がある。

directory structure
図 6. ディレクトリ構成
asyncapp/pom.xml
<project>
  <!-- omitted -->
  <modules>
    <module>asyncapp-domain</module>
    <module>asyncapp-env</module>
    <module>asyncapp-initdb</module>
    <module>asyncapp-web</module>
    <module>asyncapp-selenium</module>
    <module>asyncbatch</module> <!-- (1) -->
  </modules>
</project>
asyncapp/asyncbatch/pom.xml
<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>jp.co.ntt.fw.macchinetta.batch.sample</groupId> <!-- (2) -->
  <artifactId>asyncbatch</artifactId>
  <version>1.0-SNAPSHOT</version> <!-- (2) -->
  <!-- (1) -->
  <parent>
    <groupId>jp.co.ntt.fw.macchinetta.batch.sample</groupId>
    <artifactId>asyncapp</artifactId>
    <version>1.0-SNAPSHOT</version>
    <relativePath>../pom.xml</relativePath>
  </parent>
  <!-- omitted -->
</project>
表 5. 削除・追加内容
項番 説明

(1)

Webアプリケーションを親とし、バッチアプリケーションを子とするための設定を追記する。

(2)

子モジュール化にともない、不要となる記述を削除する。

依存ライブラリの追加

バッチアプリケーションをWebアプリケーションの依存ライブラリとして追加する。

asyncapp/async-web/pom.xml
<project>
  <!-- omitted -->
  <dependencies>
  <!-- (1) -->
    <dependency>
        <groupId>${project.groupId}</groupId>
        <artifactId>asyncbatch</artifactId>
        <version>${project.version}</version>
    </dependency>
    <!-- omitted -->
  </dependencies>
  <!-- omitted -->
</project>
表 6. 追加内容
項番 説明

(1)

バッチアプリケーションをWebアプリケーションの依存ライブラリとして追加する。

Webアプリケーションの実装

ここではWebアプリケーションとして、以下Macchinetta Server 1.x 開発ガイドラインを参考に、RESTful Webサービスを作成する。

Webアプリケーションの設定

まず、Webアプリケーションのブランクプロジェクトから、各種設定ファイルの追加・削除・編集を行う。

説明の都合上、バッチアプリケーションの実装形態としてRESTful Web Service を用いた実装を行っている。
従来のWebアプリケーション(Servlet/JSP)やSOAPを使用した場合でも同様な手順となるので、適宜読み替えること。

AppendBeanDefinitionsOnBlank
図 7. ブランクプロジェクトから追加・削除するBean定義ファイル
AppendBeanDefinitionsOnBlankXMLConfig
図 8. ブランクプロジェクトから追加・削除するBean定義ファイル
表 7. 追加・削除するBean定義ファイル
項番 説明

(1)

(2)を作成するため、不要となるので削除する。

(2)

RESTful Web Service用のSpringMvcRestConfig.java/spring-mvc-rest.xmlを作成する。必要となる定義の記述例を以下に示す。

asyncapp/asyncapp-web/src/main/java/jp/co/ntt/fw/macchinetta/batch/sample/config/web/SpringMvcRestConfig.java
@Configuration
@Import(LaunchContextConfig.class) // (1)
@EnableAspectJAutoProxy
@ComponentScan("jp.co.ntt.fw.macchinetta.batch.sample.app.api") // (2)
@EnableWebMvc
public class SpringMvcRestConfig implements WebMvcConfigurer {

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(
            @Value("classpath*:/META-INF/spring/*.properties") Resource... properties) {
        PropertySourcesPlaceholderConfigurer bean = new PropertySourcesPlaceholderConfigurer();
        bean.setLocations(properties);
        return bean;
    }

    @Bean("jsonMessageConverter")
    public MappingJackson2HttpMessageConverter jsonMessageConverter(
            ObjectMapper objectMapper) {
        MappingJackson2HttpMessageConverter bean = new MappingJackson2HttpMessageConverter();
        bean.setObjectMapper(objectMapper);
        return bean;
    }

    @Bean("objectMapper")
    public ObjectMapper objectMapper() {
        Jackson2ObjectMapperFactoryBean bean = new Jackson2ObjectMapperFactoryBean();
        bean.setDateFormat(stdDateFormat());
        bean.afterPropertiesSet();
        return bean.getObject();
    }

    @Bean
    public StdDateFormat stdDateFormat() {
        return new StdDateFormat();
    }

    // (3)
    @Bean
    public AutomaticJobRegistrar automaticJobRegistrar(JobRegistry jobRegistry,
           ApplicationContextFactory[] applicationContextFactories) throws Exception {
        final AutomaticJobRegistrar automaticJobRegistrar = new AutomaticJobRegistrar();
        final DefaultJobLoader defaultJobLoader = new DefaultJobLoader();
        defaultJobLoader.setJobRegistry(jobRegistry);
        automaticJobRegistrar.setApplicationContextFactories(applicationContextFactories);
        automaticJobRegistrar.setJobLoader(defaultJobLoader);
        automaticJobRegistrar.afterPropertiesSet();
        return automaticJobRegistrar;
    }

    @Bean
    public ApplicationContextFactory[] applicationContextFactories(
        final ApplicationContext ctx) throws IOException {
            return new ApplicationContextFactoryHelper(ctx).load(
                   "classpath:jp/co/ntt/fw/macchinetta/batch/sample/jobs/*.class");
    }

    // (4)
    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(3);
        taskExecutor.setMaxPoolSize(3);
        taskExecutor.setQueueCapacity(10);
        return taskExecutor;
    }

    // (5)
    @Bean
    public JobLauncher jobLauncher(JobRepository jobRepository,
        TaskExecutor taskExecutor) {
        TaskExecutorJobLauncher jobLauncher = new TaskExecutorJobLauncher();
        jobLauncher.setJobRepository(jobRepository);
        jobLauncher.setTaskExecutor(taskExecutor);
        return jobLauncher;
    }
}
asyncapp/asyncapp-web/src/main/resources/META-INF/spring/spring-mvc-rest.xml の記述例
<!-- omitted -->
<!-- (1) -->
<import resource="classpath:META-INF/spring/launch-context.xml"/>

<bean id="jsonMessageConverter"
      class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"
      p:objectMapper-ref="objectMapper"/>

<bean id="objectMapper"
      class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean">
  <property name="dateFormat">
      <bean class="com.fasterxml.jackson.databind.util.StdDateFormat"/>
  </property>
</bean>

<mvc:annotation-driven>
  <mvc:message-converters register-defaults="false">
    <ref bean="jsonMessageConverter"/>
  </mvc:message-converters>
</mvc:annotation-driven>

<mvc:default-servlet-handler/>

<!-- (2) -->
<context:component-scan base-package="jp.co.ntt.fw.macchinetta.batch.sample.app.api"/>

<!-- (3) -->
<bean class="org.springframework.batch.core.configuration.support.AutomaticJobRegistrar">
    <property name="applicationContextFactories">
        <bean class="org.springframework.batch.core.configuration.support.ClasspathXmlApplicationContextsFactoryBean">
            <property name="resources">
                <list>
                  <value>classpath:/META-INF/jobs/**/*.xml</value>
                </list>
            </property>
        </bean>
    </property>
    <property name="jobLoader">
        <bean class="org.springframework.batch.core.configuration.support.DefaultJobLoader"
              p:jobRegistry-ref="jobRegistry"/>
    </property>
</bean>

<!-- (4) -->
<task:executor id="taskExecutor" pool-size="3" queue-capacity="10"/>

<!-- (5) -->
<bean id="jobLauncher" class="org.springframework.batch.core.launch.support.TaskExecutorJobLauncher"
      p:jobRepository-ref="jobRepository"
      p:taskExecutor-ref="taskExecutor"/>
<!-- omitted -->
asyncapp/asyncapp-web/src/main/webapp/WEB-INF/web.xml の記述例
<!-- omitted -->
<context-param>
    <param-name>contextClass</param-name>
    <param-value>
        org.springframework.web.context.support.AnnotationConfigWebApplicationContext
    </param-value>
</context-param>

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
    <param-name>contextConfigLocation</param-name>
    <!-- Root ApplicationContext -->
    <param-value>
        jp.co.ntt.fw.macchinetta.batch.sample.config.app.ApplicationContextConfig
    </param-value>
</context-param>

<servlet>
    <servlet-name>restApiServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextClass</param-name>
        <param-value>
          org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        </param-value>
    </init-param>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <!-- (6) -->
        <param-value>
          jp.co.ntt.fw.macchinetta.batch.sample.config.web.SpringMvcRestConfig
        </param-value>
    </init-param>
    <!-- (7) -->
    <init-param>
        <param-name>spring.profiles.active</param-name>
        <param-value>async</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>restApiServlet</servlet-name>
    <url-pattern>/api/v1/*</url-pattern>
</servlet-mapping>
<!-- omitted -->
asyncapp/asyncapp-web/src/main/webapp/WEB-INF/web.xml の記述例
<!-- omitted -->
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
    <param-name>contextConfigLocation</param-name>
    <!-- Root ApplicationContext -->
    <param-value>
        classpath*:META-INF/spring/applicationContext.xml
    </param-value>
</context-param>

<servlet>
    <servlet-name>restApiServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <!-- (6) -->
        <param-value>classpath*:META-INF/spring/spring-mvc-rest.xml</param-value>
    </init-param>
    <!-- (7) -->
    <init-param>
        <param-name>spring.profiles.active</param-name>
        <param-value>async</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>restApiServlet</servlet-name>
    <url-pattern>/api/v1/*</url-pattern>
</servlet-mapping>
<!-- omitted -->
表 8. RESTful Web Service の有効化例
項番 説明

(1)

バッチアプリケーション内にあるLaunchContextConfig.java/launch-context.xmlをimportし、必須となるBean定義を取り込む。

(2)

コントローラを動的にスキャンするためのパッケージを記述する。

(3)

個々のジョブBean定義ファイルをモジュラー化することにより子コンテキストとして動的ロードを行うAutomaticJobRegistrarのBean定義を記述する。

(4)

非同期で実行するジョブが用いるTaskExecutorを定義する。
JobLauncherのTaskExecutorAsyncTaskExecutor実装クラスを設定することで非同期実行が可能となる。 AsyncTaskExecutor実装クラスの1つであるThreadPoolTaskExecutorを利用する。

また、並列起動可能なスレッドの多重度を指定ことができる。
この例では3スレッドがジョブの実行に割り当てられ、それを超えたリクエストは10までがキューイングされる。 キューイングされたジョブは未開始の状態ではあるが、REST リクエストは成功とみなされる。
さらにキューイングの上限を超えたジョブのリクエストはorg.springframework.core.task.TaskRejectedExceptionが発生し、 ジョブの起動要求が拒否される。

(5)

(4)のtaskExecutorを有効化するため、LaunchContextConfig.java/launch-context.xmlで定義されているjobLauncherをオーバーライドする。

(6)

DispatcherServletが読み込むBean定義として、
上述で記載したSpringMvcRestConfig.java/spring-mvc-rest.xmlを指定する。

(7)

Spring Framework のプロファイルとして、非同期バッチを表すasyncを明示する。

asyncプロファイルの指定をしなかった場合

この場合、Webアプリケーション横断で共有すればよいLaunchContextConfig.java/launch-context.xmlに定義されたBeanが、ジョブごとに重複して生成される。
重複した場合でも機能上動作するため誤りに気づきにくく、予期しないリソース枯渇や性能劣化が発生する恐れがある。 必ず指定すること。

スレッドプールのサイジング

スレッドプールの上限が過剰である場合、膨大なジョブが並走することとなり、 アプリケーション全体のスループットが劣化する恐れがある。 サイジングを行ったうえで適正な上限値を定めること。
非同期実行のスレッドプールとは別に、Webコンテナのリクエストスレッドや 同一筐体内で動作している他のアプリケーションも含めて検討する必要がある。

また、スレッドプール枯渇に伴うTaskRejectException発生の確認、および再実行は Webクライアントから別途リクエストを送信する必要がある。 そのため、スレッドプール枯渇時、ジョブ起動を待機させるqueue-capacityは必ず設定すること。

RESTful Web Service API の定義

REST APIで使用するリクエストの例として、 ここでは「ジョブの起動」、「ジョブの状態確認」の2つを定義する。

表 9. REST API 定義例
項番 API パス HTTPメソッド 要求/応答 電文形式 電文の説明

(1)

ジョブの起動

/api/v1/job/ジョブ名

POST

リクエスト

JSON

ジョブパラメータ

レスポンス

JSON

ジョブ実行ID
ジョブ名
メッセージ

(2)

ジョブの実行状態確認

/api/v1/job/ジョブ実行ID

GET

リクエスト

N/A

N/A

レスポンス

JSON

ジョブ実行ID
ジョブ名
ジョブ実行ステータス
ジョブ終了コード
ステップ実行ID
ステップ名
ステップ終了コード

コントローラで使用するJavaBeansの実装

JSON電文としてRESTクライアントに返却される以下3クラスを作成する。

  • ジョブ起動操作 JobOperationResource

  • ジョブの実行状態 JobExecutionResource

  • ステップの実行状態 StepExecutionResource

これらクラスはJobOperationResourceのジョブ実行ID(job execution id)を除きあくまで参考実装であり、フィールドの実装は任意である。

ジョブ起動操作情報実装例
// asyncapp/asyncapp-web/src/main/java/jp/co/ntt/fw/macchinetta/batch/sample/app/api/jobinfo/JobOperationResource.java
package jp.co.ntt.fw.macchinetta.batch.sample.app.api.jobinfo;

public class JobOperationResource {

    private String jobName = null;

    private String jobParams = null;

    private Long jobExecutionId = null;

    private String errorMessage = null;

    private Exception error = null;

    // Getter and setter are omitted.
}
ジョブ実行情報実装例
// asyncapp/asyncapp-web/src/main/java/jp/co/ntt/fw.macchinetta/batch/sample/app/api/jobinfo/JobExecutionResource.java
package jp.co.ntt.fw.macchinetta.batch.sample.app.api.jobinfo;

// omitted.

public class JobExecutionResource {

    private Long jobExecutionId = null;

    private String jobName = null;

    private Long stepExecutionId = null;

    private String stepName = null;

    private List<StepExecutionResource> stepExecutions = new ArrayList<>();

    private String status = null;

    private String exitStatus = null;

    private String errorMessage;

    private List<String> failureExceptions = new ArrayList<>();

    // Getter and setter are omitted.
}
ステップ実行情報実装例
// asyncapp/asyncapp-web/src/main/java/jp/co/ntt/fw/macchinetta/batch/sample/app/api/jobinfo/StepExecutionResource.java
package jp.co.ntt.fw.macchinetta.batch.sample.app.api.jobinfo;

public class StepExecutionResource {

  private Long stepExecutionId = null;

  private String stepName = null;

  private String status = null;

  private List<String> failureExceptions = new ArrayList<>();

    // Getter and setter are omitted.
}

コントローラの実装

@RestControllerを用い、RESTful Web Service のコントローラを実装する。
ここでは簡単のため、JobOperatorをコントローラにインジェクションし、ジョブの起動や実行状態の取得を行う。 もちろんMacchinetta Server 1.xに従って、コントローラからServiceをはさんでJobOperatorを起動してもよい。

コントローラ実装例
// asyncapp/asyncapp-web/src/main/java/jp/co/ntt/fw/macchinetta/batch/sample/app/api/JobController.java
package jp.co.ntt.fw.macchinetta.batch.sample.app.api;

// omitted.

// (1)
@RequestMapping("job")
@RestController
public class JobController {

    // (2)
    @Inject
    JobOperator jobOperator;

    // (2)
    @Inject
    JobExplorer jobExplorer;

    @RequestMapping(value = "{jobName}", method = RequestMethod.POST)
    public ResponseEntity<JobOperationResource> launch(@PathVariable("jobName") String jobName,
            @RequestBody JobOperationResource requestResource) {

        String jobParams = requestResource.getJobParams();
        JobOperationResource responseResource = new JobOperationResource();
        responseResource.setJobName(jobName);
        responseResource.setJobParams(jobParams);
        Properties properties = new Properties();
        if(jobParams == null){
            requestResource.setJobParams("");
        } else {
            if (jobParams.contains(",")) {
                requestResource.setJobParams(jobParams.replace(",", " "));
                jobParams = jobParams.replace(",", " ");
                requestResource.setJobParams(jobParams);
            }
            if (!jobParams.isEmpty()) {
                String[] keyValuePairs = jobParams.split(" ");
                for (String string : keyValuePairs) {
                    String[] keyValuePair = string.split("=");
                    properties.setProperty(keyValuePair[0], keyValuePair[1]);
                }
            }
        }
        try {
            // (3)
            Long jobExecutionId = jobOperator.start(jobName, properties);
            responseResource.setJobExecutionId(jobExecutionId);
            responseResource.setResultMessage("Job launching task is scheduled.");
            return ResponseEntity.ok().body(responseResource);
        } catch (NoSuchJobException | JobInstanceAlreadyExistsException | JobParametersInvalidException e) {
            responseResource.setError(e);
            return ResponseEntity.badRequest().body(responseResource);
        }
    }

    @RequestMapping(value = "{jobExecutionId}", method = RequestMethod.GET)
    @ResponseStatus(HttpStatus.OK)
    public JobExecutionResource getJob(@PathVariable("jobExecutionId") Long jobExecutionId) {

        JobExecutionResource responseResource = new JobExecutionResource();
        responseResource.setJobExecutionId(jobExecutionId);

        // (4)
        JobExecution jobExecution = jobExplorer.getJobExecution(jobExecutionId);

        if (jobExecution == null) {
            responseResource.setErrorMessage("Job execution not found.");
        } else {
            mappingExecutionInfo(jobExecution, responseResource);
        }

        return responseResource;
    }

    private void mappingExecutionInfo(JobExecution src, JobExecutionResource dest) {
      dest.setJobName(src.getJobInstance().getJobName());
      for (StepExecution se : src.getStepExecutions()) {
          StepExecutionResource ser = new StepExecutionResource();
          ser.setStepExecutionId(se.getId());
          ser.setStepName(se.getStepName());
          ser.setStatus(se.getStatus().toString());
          for (Throwable th : se.getFailureExceptions()) {
              ser.getFailureExceptions().add(th.toString());
          }
          dest.getStepExecutions().add(ser);
      }
      dest.setStatus(src.getStatus().toString());
      dest.setExitStatus(src.getExitStatus().toString());
    }
}
表 10. コントローラの実装
項番 説明

(1)

@RestControllerを指定する。 さらに@RequestMapping("job")により、web.xmlのサーブレットマッピングとあわせると、 REST APIの基底パスはcontextName/api/v1/job/となる。

(2)

JobOperatorJobExplorerのフィールドインジェクションを記述する。

(3)

JobOperator を使用して新規に非同期ジョブを起動する。
返り値としてジョブ実行IDを受け取り、REST クライアントに返却する。

(4)

JobExplorer を使用し、ジョブ実行IDをもとにジョブの実行状態(JobExecution)を取得する。
あらかじめ設計された電文フォーマットに変換した上でRESTクライアントに返却する。

Spring Batchが提供するSimpleJobOperatorの仕様変更に伴い、 JobControllerの実装も下記のように変更している。

  • パラメータを複数指定する場合の区切り文字を、カンマから空白文字に変更

  • パラメータのnullチェックを、別途実装するように変更 詳細は、Spring Batch/BATCH-1461を参照されたい。

  • パラメータをStringからPropertiesに変換処理を追加し、startメソッドに渡す。 詳細は、Spring Batch/BATCH-4304を参照されたい。

Web/バッチアプリケーションモジュール設定の統合

バッチアプリケーションモジュール(asyncbatch)は単体で動作可能なアプリケーションとして動作する。 そのため、バッチアプリケーションモジュール(asyncbatch)は、Webアプリケーションモジュール(asyncapp-web)との間で競合・重複する設定が存在する。 これらは、必要に応じて統合する必要がある。

  1. ログ設定ファイルlogback.xmlの統合
    Web/バッチ間でLogback定義ファイルが複数定義されている場合、正常に動作しない。
    asyncbatch/src/main/resources/logback.xmlの記述内容はasyncapp-env/src/main/resources/の同ファイルに統合した上で削除する。

  2. データソース、MyBatis設定ファイルは統合しない
    データソース、MyBatis設定ファイルの定義はWeb/バッチ間では、以下関係によりアプリケーションコンテキストの定義が独立するため、統合しない。

    • バッチのasyncbatchモジュールはサーブレットに閉じたコンテキストとして定義される。

    • Webのasyncapp-domainasyncapp-envモジュールはアプリケーション全体で使用されるコンテキストとして定義される。

Webとバッチモジュールによるデータソース、MyBatis設定の相互参照

Webとバッチモジュールによるコンテキストのスコープが異なるため、 特にWebモジュールからバッチのデータソース、MyBatis設定、Mapperインタフェースは参照できない。
RDBMSスキーマ初期化もそれぞれ異なるモジュールの設定に応じて独立して行われるため、相互干渉により 意図しない初期化が行われないよう配慮すること。

REST コントローラ特有のCSRF対策設定

Webブランクプロジェクトの初期設定では、RESTコントローラに対しリクエストを送信するとCSRFエラーとして ジョブの実行が拒否される。 そのため、ここでは以下方法によりCSRF対策を無効化した前提で説明している。

ここで作成されるWebアプリケーションはインターネット上には公開されず、CSRFを攻撃手段として 悪用しうる第三者からのRESTリクエスト送信が発生しない前提でCSRF対策を無効化している。 実際のWebアプリケーションでは動作環境により要否が異なる点に注意すること。

ビルド

Mavenコマンドでビルドし、warファイルを作成する。

$ cd asyncapp
$ ls
asyncbatch/  asyncapp-web/  pom.xml
$ mvn clean package
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Build Order:
[INFO]
[INFO] Macchinetta Server Framework (1.x) Web Blank Multi Project (MyBatis3)
[INFO] Macchinetta Batch Framework (2.x) Blank Project
[INFO] asyncapp-web
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building Macchinetta Server Framework (1.x) Web Blank Multi Project (MyBatis3) 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------

(omitted)

[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO]
[INFO] Macchinetta Server Framework (1.x) Web Blank Multi Project (MyBatis3) SUCCESS [  0.226 s]
[INFO] Macchinetta Batch Framework (2.x) Blank Project SUCCESS [  6.481s]
[INFO] asyncapp-web ....................................... SUCCESS [  5.400 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 12.597 s
[INFO] Finished at: 2017-02-10T22:32:43+09:00
[INFO] Final Memory: 38M/250M
[INFO] ------------------------------------------------------------------------
$

デプロイ

TomcatなどのWebコンテナを起動し、ビルドで生成されたwarファイルをデプロイする。 詳細な手順は割愛する。

REST Clientによるジョブの起動と実行結果確認

ここではREST クライアントとしてcurlコマンドを使用し、非同期ジョブを起動する。

$ curl -v \
  -H "Accept: application/json" -H "Content-type: application/json" \
  -d '{}' \
  http://localhost:8088/asyncapp-web/api/v1/job/job01
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
* Trying 127.0.0.1:8088...
* Connected to localhost (127.0.0.1) port 8088 (#0)
> POST /asyncapp-web/api/v1/job/job01 HTTP/1.1
> Host: localhost:8088
> User-Agent: curl/7.88.1
> Accept: application/json
> Content-type: application/json
> Content-Length: 30
>
} [30 bytes data]
< HTTP/1.1 200
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Mon, 22 Jan 2024 02:39:04 GMT
<
{ [94 bytes data]
100   118    0    88  100    30    112     38 --:--:-- --:--:-- --:--:--   150
{"jobName":"job01","jobParams":null,"jobExecutionId":1,"errorMessage":null,"error":null}
* Connection #0 to host localhost left intact
$

上記より、ジョブ実行ID:jobExecutionId = 1として、ジョブが実行されていることが確認できる。
続けてこのジョブ実行IDを使用し、ジョブの実行結果を取得する。

$ curl -v http://localhost:8088/asyncapp-web/api/v1/job/1
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
* Trying 127.0.0.1:8088...
* Connected to localhost (127.0.0.1) port 8088 (#0)
> GET /asyncapp-web/api/v1/job/1 HTTP/1.1
> Host: localhost:8088
> User-Agent: curl/7.88.1
> Accept: */*
>
< HTTP/1.1 200
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Mon, 22 Jan 2024 02:39:26 GMT
<
{ [310 bytes data]
100   303    0   303    0     0   2126      0 --:--:-- --:--:-- --:--:--  2118
{"jobExecutionId":1,"jobName":"job01","stepExecutionId":null,"stepName":null,"stepExecutions":
[{"stepExecutionId":1,"stepName":"job01.step01","status":"COMPLETED","failureExceptions":[]}],
"status":"COMPLETED","exitStatus":"exitCode=COMPLETED;exitDescription=","errorMessage":null,
"failureExceptions":[]}
* Connection #0 to host localhost left intact
$

exitCode=COMPLETEDであることより、ジョブが正常終了していることが確認できる。

シェルスクリプトなどでcurlの実行結果を判定する場合

上記の例ではREST APIによる応答電文まで表示させている。 curlコマンドでHTTPステータスのみを確認する場合はcurl -s URL -o /dev/null -w "%{http_code}\n"とすることで、HTTPステータスが標準出力に表示される。
ただし、ジョブ実行IDはレスポンスボディ部のJSONを解析する必要があるため、必要に応じてREST クライアントアプリケーションを作成すること。

How to extend

ジョブの停止とリスタート

非同期ジョブの停止・リスタートは複数実行しているジョブの中から停止・リスタートする必要がある。 また、同名のジョブが並走している場合に、問題が発生しているジョブのみを対象にする必要もある。 よって、対象とするジョブ実行が特定でき、その状態が確認できる必要がある。
ここではこの前提を満たす場合、非同期実行の停止・リスタートを行うための実装について説明する。

以降、コントローラの実装JobControllerに対して、 ジョブの停止(stop)やリスタート(restart)を追加する方法について説明する。

ジョブの停止・リスタートはJobOperatorを用いた実装をしなくても実施できる。
詳細はジョブの管理を参照し、目的に合う方式を検討すること。
停止・リスタートの実装例
// asyncapp/asyncapp-web/src/main/java/jp/co/ntt/fw/macchinetta/batch/sample/app/api/JobController.java
package jp.co.ntt.fw.macchinetta.batch.sample.app.api;

// omitted.

@RequestMapping("job")
@RestController
public class JobController {

    // omitted.

    @RequestMapping(value = "stop/{jobExecutionId}", method = RequestMethod.PUT)
    @Deprecated
    public ResponseEntity<JobOperationResource> stop(
            @PathVariable("jobExecutionId") Long jobExecutionId) {

      JobOperationResource responseResource = new JobOperationResource();
      responseResource.setJobExecutionId(jobExecutionId);
      boolean result = false;
      try {
          // (1)
          result = jobOperator.stop(jobExecutionId);
          if (!result) {
              responseResource.setErrorMessage("stop failed.");
              return ResponseEntity.badRequest().body(responseResource);
          }
          return ResponseEntity.ok().body(responseResource);
      } catch (NoSuchJobExecutionException | JobExecutionNotRunningException e) {
          responseResource.setError(e);
          return ResponseEntity.badRequest().body(responseResource);
      }
    }

    @RequestMapping(value = "restart/{jobExecutionId}",
                    method = RequestMethod.PUT)
    @Deprecated
    public ResponseEntity<JobOperationResource> restart(
            @PathVariable("jobExecutionId") Long jobExecutionId) {

        JobOperationResource responseResource = new JobOperationResource();
        responseResource.setJobExecutionId(jobExecutionId);
        try {
            // (2)
            Long id = jobOperator.restart(jobExecutionId);
            responseResource.setJobExecutionId(id);
            return ResponseEntity.ok().body(responseResource);
        } catch (JobInstanceAlreadyCompleteException |
                  NoSuchJobExecutionException | NoSuchJobException |
                  JobRestartException | JobParametersInvalidException e) {
            responseResource.setErrorMessage(e.getMessage());
            return ResponseEntity.badRequest().body(responseResource);
        }
    }

    // omitted.
}
表 11. コントローラによる停止・リスタート実装例
項番 説明

(1)

JobOperator#stop()を呼び出すことにより、実行中のジョブに対し停止を指示する。

(2)

JobOperator#restart()を呼び出すことにより、異常終了・停止したステップから再実行させる。

複数起動

ここでの複数起動とは、Webコンテナを複数起動し、それぞれがジョブ要求を待ち受けることを指す。

非同期ジョブの実行管理は外部RDBMSによって行われるため、各アプリケーションの接続先となる 外部RDBMSを共有することで、同一筐体あるいは別筐体にまたがって非同期ジョブ起動を待ち受けることができる。

用途としては特定のジョブに対する負荷分散や冗長化などがあげられる。 しかし、Webアプリケーションの実装 で述べたように、Webコンテナを複数起動し並列性を高めるだけでこれらの効果が容易に得られるわけではない。 効果を得るためには、一般的なWebアプリケーションと同様の対処が求められる場合がある。 以下にその一例を示す。

  • Webアプリケーションの特性上、1リクエスト処理はステートレスに動作するが、 バッチの非同期実行はジョブの起動と結果の確認を合わせて設計しなければ、かえって障害耐性が低下する恐れもある。
    たとえば、ジョブ起動用Webコンテナを冗長化した場合でもクライアント側の障害によりジョブ起動後にジョブ実行IDを ロストすることでジョブの途中経過や結果の確認は困難となる。

  • 複数のWebコンテナにかかる負荷を分散させるために、クライアント側にリクエスト先を振り分ける機能を実装したり、 ロードバランサを導入したりする必要がある。

このように、複数起動の適性は一概に定めることができない。 そのため、目的と用途に応じてロードバランサの利用やWebクライアントによるリクエスト送信制御方式などを検討し、 非同期実行アプリケーションの性能や耐障害性を落とさない設計が必要となる。

Macchinetta Batch Framework (2.x) Development Guideline - version 2.5.0.RELEASE, 2024-3-28