Overview
Webコンテナ内でジョブを非同期で実行するための方法について説明する。
本機能は、チャンクモデルとタスクレットモデルとで同じ使い方になる。
ジョブを含めたWebアプリケーションをWebコンテナにデプロイし、
送信されたリクエストの情報をもとにジョブを実行することを指す。
ジョブの実行ごとに1つのスレッドを割り当てた上で並列に動作するため、
他のジョブやリクエストに対する処理とは独立して実行できる。
Macchinetta Batch 2.xでは、非同期実行(Webコンテナ)向けの実装は提供しない。
本ガイドラインにて実現方法を提示するのみとする。
これは、Webアプリケーションの起動契機はHTTP/SOAP/MQなど多様であるため、
ユーザにて実装することが適切と判断したためである。
-
アプリケーションの他にWebコンテナが必要となる。
-
ジョブの実装以外に必要となる、Webアプリケーション、クライアントは動作要件に合わせて別途実装する。
-
ジョブの実行状況および結果は
JobRepository
に委ねる。
また、Webコンテナ停止後にもJobRepository
からジョブの実行状況および結果を参照可能とするため、インメモリデータベースではなく、永続性が担保されているデータベースを使用する。
"非同期実行(DBポーリング) - Overview"と同様である。
非同期実行(DBポーリング)との違い
アーキテクチャ上、非同期実行時の即時性と、要求管理テーブルの有無、の2点が異なる。 |
Architecture
本方式による非同期ジョブはWebコンテナ上にデプロイされたアプリケーション(war)として動作するが、 ジョブ自身はWebコンテナのリクエスト処理とは非同期(別スレッド)で動作する。
-
Webクライアントは実行対象のジョブをWebコンテナに要求する。
-
JobController
はSpring BatchのJobOperator
に対しジョブの実行開始を依頼する。 -
ThreadPoolTaskExecutor
によって非同期でジョブを実行する。 -
実行された対象のジョブを一意に判別するためのジョブ実行ID(
job execution id
)を返却する。 -
JobController
はWebクライアントに対し、ジョブ実行IDを含むレスポンスを返却する。 -
目的のジョブを実行する。
-
ジョブの結果は
JobRepository
に反映される。
-
-
Job
が実行結果を返却する。これはクライアントへ直接通知できない。
-
Webクライアントはジョブ実行IDを
JobController
をWebコンテナに送信する。 -
JobController
はジョブ実行IDを用いJobExplorer
にジョブの実行結果を問い合わせる。 -
JobExplorer
はジョブの実行結果を返却する。 -
JobController
はWebクライアントに対しレスポンスを返却する。-
レスポンスにはジョブ実行IDを設定する。
-
Webコンテナによるリクエスト受信後、ジョブ実行ID払い出しまでがリクエスト処理と同期するが、
以降のジョブ実行はWebコンテナとは別のスレッドプールで非同期に行われる。
これは再度リクエストで問い合わせを受けない限り、Webクライアント側では非同期ジョブの
実行状態が検知できないことを意味する。
このためWebクライアント側では1回のジョブ実行で、リクエストを「ジョブの起動」で1回、
「結果の確認」が必要な場合は加えてもう1回、Webコンテナにリクエストを送信する必要がある。
特に初回の「ジョブの起動」時に見え方が異なる異常検知については、
後述のジョブ起動時における異常発生の検知についてで説明する。
|
ジョブ実行ID(job execution id)の取り扱いについて
ジョブ実行IDは起動対象が同じジョブ、同じジョブパラメータであっても、ジョブ起動ごとに異なるシーケンス値が払い出される。 |
ジョブ起動時における異常発生の検知について
Webクライアントからジョブの起動リクエストを送信後、ジョブ実行ID払い出しを境にして異常検知の見え方が異なる。
-
ジョブ起動時のレスポンスにて異常がすぐ検知できるもの
-
起動対象のジョブが存在しない。
-
ジョブパラメータの形式誤り。
-
-
ジョブ起動後、Webコンテナに対しジョブ実行状態・結果の問い合わせが必要となるもの
-
ジョブの実行ステータス
-
非同期ジョブ実行で使用されるスレッドプールが枯渇したことによるジョブの起動失敗
-
「ジョブ起動時の異常」は Spring MVCコントローラ内で発生する例外として検知できる。 ここでは説明を割愛するので、別途 Macchinetta Server 1.x 開発ガイドラインの 例外のハンドリングの実装を参照。 また、ジョブパラメータとして利用するリクエストの入力チェックは必要に応じて Spring MVC のコントローラ内で行うこと。 |
スレッドプール枯渇によるジョブの起動失敗はジョブ起動時に捕捉できない
スレッドプール枯渇によるジョブの起動失敗は、
|
非同期実行(Webコンテナ)のアプリケーション構成
本機能は"非同期実行(DBポーリング)"と同様、
非同期実行特有の構成としてSpring プロファイルのasync
とAutomaticJobRegistrar
を使用している。
一方で、これら機能を非同期実行(Webコンテナ)使用する上で、いくつかの事前知識と設定が必要となる。
"ApplicationContextの構成"を参照。
具体的なasync
プロファイルとAutomaticJobRegistrar
の設定方法については
"非同期実行(Webコンテナ)によるアプリケーションの実装方法について"で後述する。
ApplicationContextの構成
上述のとおり、非同期実行(Webコンテナ)のアプリケーション構成として、複数のアプリケーションモジュールが含まれている。
それぞれのアプリケーションコンテキストとBean定義についての種類、および関係性を把握しておく必要がある。
非同期実行(Webコンテナ)におけるApplicationContext
では、
バッチアプリケーションのApplicationContext
はWebのコンテキスト内に取り込まれる。
個々のジョブコンテキストはこのWebコンテキストからAutomaticJobRegistrar
によりモジュール化され、
Webコンテキストの子コンテキストとして動作する。
以下、それぞれのコンテキストを構成するBean定義ファイルについて説明する。
項番 | 説明 |
---|---|
(1) |
共通Bean定義ファイル。 |
(2) |
ジョブBean定義から必ずインポートされるBean定義ファイル。 |
(3) |
ジョブごとに作成するBean定義ファイル。 |
(4) |
|
(5) |
|
(6) |
|
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 を用いてを作成しているものとして説明する。
名称 | 値 |
---|---|
groupId |
jp.co.ntt.fw.macchinetta.batch.sample |
artifactId |
asyncbatch |
version |
1.0-SNAPSHOT |
package |
jp.co.ntt.fw.macchinetta.batch.sample |
また説明の都合上、ブランクプロジェクトに初めから登録されているジョブを使用する。
名称 | 説明 |
---|---|
ジョブ名 |
job01 |
非同期実行(Webコンテナ)ジョブ設計の注意点
非同期実行(Webコンテナ)の特性として個々のジョブは短時間で完了しWebコンテナ上でステートレスに
動作するケースが適している。 |
ジョブ実装を含むjarファイルが作成可能な状態として、Webアプリケーションの作成を行う。
Macchinetta Server 1.xが提供するブランクプロジェクトを用い、Webアプリケーションの実装方法を説明する。 詳細は、Macchinetta Server 1.x 開発ガイドラインの 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アプリケーションの子モジュールとする場合、 |
各種設定
pom.xmlを編集し、バッチアプリケーションをWebアプリケーションの一部に含める。
バッチアプリケーションを |
<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>
<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>
項番 | 説明 |
---|---|
(1) |
Webアプリケーションを親とし、バッチアプリケーションを子とするための設定を追記する。 |
(2) |
子モジュール化にともない、不要となる記述を削除する。 |
バッチアプリケーションをWebアプリケーションの依存ライブラリとして追加する。
<project>
<!-- omitted -->
<dependencies>
<!-- (1) -->
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>asyncbatch</artifactId>
<version>${project.version}</version>
</dependency>
<!-- omitted -->
</dependencies>
<!-- omitted -->
</project>
項番 | 説明 |
---|---|
(1) |
バッチアプリケーションをWebアプリケーションの依存ライブラリとして追加する。 |
Webアプリケーションの実装
ここではWebアプリケーションとして、以下Macchinetta Server 1.x 開発ガイドラインを参考に、RESTful Webサービスを作成する。
Webアプリケーションの設定
まず、Webアプリケーションのブランクプロジェクトから、各種設定ファイルの追加・削除・編集を行う。
説明の都合上、バッチアプリケーションの実装形態としてRESTful Web Service を用いた実装を行っている。 |
項番 | 説明 |
---|---|
(1) |
(2)を作成するため、不要となるので削除する。 |
(2) |
|
@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;
}
}
<!-- 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 -->
<!-- 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 -->
<!-- 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 -->
項番 | 説明 |
---|---|
(1) |
バッチアプリケーション内にある |
(2) |
コントローラを動的にスキャンするためのパッケージを記述する。 |
(3) |
個々のジョブBean定義ファイルをモジュラー化することにより子コンテキストとして動的ロードを行う |
(4) |
非同期で実行するジョブが用いる また、並列起動可能なスレッドの多重度を指定ことができる。 |
(5) |
(4)の |
(6) |
|
(7) |
Spring Framework のプロファイルとして、非同期バッチを表す |
asyncプロファイルの指定をしなかった場合
この場合、Webアプリケーション横断で共有すればよい |
スレッドプールのサイジング
スレッドプールの上限が過剰である場合、膨大なジョブが並走することとなり、
アプリケーション全体のスループットが劣化する恐れがある。
サイジングを行ったうえで適正な上限値を定めること。 また、スレッドプール枯渇に伴う |
REST APIで使用するリクエストの例として、 ここでは「ジョブの起動」、「ジョブの状態確認」の2つを定義する。
項番 | API | パス | HTTPメソッド | 要求/応答 | 電文形式 | 電文の説明 |
---|---|---|---|---|---|---|
(1) |
ジョブの起動 |
/api/v1/job/ジョブ名 |
POST |
リクエスト |
JSON |
ジョブパラメータ |
レスポンス |
JSON |
ジョブ実行ID |
||||
(2) |
ジョブの実行状態確認 |
/api/v1/job/ジョブ実行ID |
GET |
リクエスト |
N/A |
N/A |
レスポンス |
JSON |
ジョブ実行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());
}
}
項番 | 説明 |
---|---|
(1) |
|
(2) |
|
(3) |
|
(4) |
|
Spring Batchが提供するSimpleJobOperatorの仕様変更に伴い、
JobControllerの実装も下記のように変更している。
|
Web/バッチアプリケーションモジュール設定の統合
バッチアプリケーションモジュール(asyncbatch
)は単体で動作可能なアプリケーションとして動作する。
そのため、バッチアプリケーションモジュール(asyncbatch
)は、Webアプリケーションモジュール(asyncapp-web
)との間で競合・重複する設定が存在する。
これらは、必要に応じて統合する必要がある。
-
ログ設定ファイル
logback.xml
の統合
Web/バッチ間でLogback定義ファイルが複数定義されている場合、正常に動作しない。
asyncbatch/src/main/resources/logback.xml
の記述内容はasyncapp-env/src/main/resources/
の同ファイルに統合した上で削除する。 -
データソース、MyBatis設定ファイルは統合しない
データソース、MyBatis設定ファイルの定義はWeb/バッチ間では、以下関係によりアプリケーションコンテキストの定義が独立するため、統合しない。-
バッチの
asyncbatch
モジュールはサーブレットに閉じたコンテキストとして定義される。 -
Webの
asyncapp-domain
、asyncapp-env
モジュールはアプリケーション全体で使用されるコンテキストとして定義される。
-
Webとバッチモジュールによるデータソース、MyBatis設定の相互参照
Webとバッチモジュールによるコンテキストのスコープが異なるため、
特にWebモジュールからバッチのデータソース、MyBatis設定、Mapperインタフェースは参照できない。 |
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] ------------------------------------------------------------------------
$
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ステータスのみを確認する場合は |
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.
}
項番 | 説明 |
---|---|
(1) |
|
(2) |
|
複数起動
ここでの複数起動とは、Webコンテナを複数起動し、それぞれがジョブ要求を待ち受けることを指す。
非同期ジョブの実行管理は外部RDBMSによって行われるため、各アプリケーションの接続先となる 外部RDBMSを共有することで、同一筐体あるいは別筐体にまたがって非同期ジョブ起動を待ち受けることができる。
用途としては特定のジョブに対する負荷分散や冗長化などがあげられる。 しかし、Webアプリケーションの実装 で述べたように、Webコンテナを複数起動し並列性を高めるだけでこれらの効果が容易に得られるわけではない。 効果を得るためには、一般的なWebアプリケーションと同様の対処が求められる場合がある。 以下にその一例を示す。
-
Webアプリケーションの特性上、1リクエスト処理はステートレスに動作するが、 バッチの非同期実行はジョブの起動と結果の確認を合わせて設計しなければ、かえって障害耐性が低下する恐れもある。
たとえば、ジョブ起動用Webコンテナを冗長化した場合でもクライアント側の障害によりジョブ起動後にジョブ実行IDを ロストすることでジョブの途中経過や結果の確認は困難となる。 -
複数のWebコンテナにかかる負荷を分散させるために、クライアント側にリクエスト先を振り分ける機能を実装したり、 ロードバランサを導入したりする必要がある。
このように、複数起動の適性は一概に定めることができない。 そのため、目的と用途に応じてロードバランサの利用やWebクライアントによるリクエスト送信制御方式などを検討し、 非同期実行アプリケーションの性能や耐障害性を落とさない設計が必要となる。