GoReplayで検証環境へ本番環境のリクエストを復元する方法と懸念
やりたいこと
- (1) 検証環境で本番環境へのリクエストを再現したい
- (2) トラフィックのミラーリングではなく、任意のタイミングで任意の流量で再現したい
- (3) 本番環境の機密情報を検証環境に持ち込みたいくない
- (4) 本番環境への影響・変更は最低限に留めたい
- (5) 対象アプリケーションはECSで動作しているとする
GoReplayで実現できるか?
「やりたいこと」の(1)、(2)に対応できるか。
GoReplay(https://github.com/buger/goreplay) の gor
コマンドには 入力元と出力先を指定する必要がある。
参考: https://github.com/buger/goreplay/wiki/The-Basics
ミラーリングするだけならば入力: HTTPトラフィック、出力 HTTPトラフィック とすることですぐさまリクエストをコピーして転送できる。 HOSTヘッダ以外は同様のものをコピー先サーバで受信できていることがわかる。
今回検討したいことは任意のタイミングでのリプレイなので、入力・出力は以下のようになる。 (本番環境) 入力: HTTPトラフィック、出力: ファイル (検証環境) 入力: ファイル、出力: HTTPトラフィック アーキテクチャは以下のようなイメージ。
GoReplayのPRO版ならばS3に保存する機能がついている。これを使わない場合、他の手段でアプリケーションのコンテナからファイルをどこかに保存する必要がある(この記事では省略)。 参考: https://github.com/buger/goreplay/wiki/%5BPRO%5D-Using-S3-for-storing-and-replaying-traffic
リプレイ時の流量調整もコマンドで対応可能 (|200%
部分)。
# Replay from file on 2x speed gor --input-file "requests.gor|200%" --output-http "staging.com"
基本的な機能はこれでカバーできそうである。
データの変換、フィルタリング
「やりたいこと」の(3)に対応できるか。 ミドルウェアでビジネスロジックの注入が可能。 参考: https://github.com/buger/goreplay/wiki/Middleware ミドルウェアの実装はアプリケーション次第だが、変換ロジックに外部との通信などコストが高い処理があるとアプリケーションコンテナに負荷がかかってしまう。 ミラーリングの場合でも無視できないレベルの負荷がありそう。 GoReplay利用時のベンチマークの参考 -> https://blog.glidenote.com/blog/2016/12/16/goreplay/
この部分は機能面では実現はできるはずだが、ロジック次第ではパフォーマンスの考慮が必要。
本番環境への影響
「やりたいこと」の(4)に対応できるか。 GoReplayはコンテナ内ではアプリケーションへのリクエストをプロキシしているわけではなく、コンテナに来たHTTPリクエストを取得している。GoReplayに問題があってもアプリケーションは問題なく動く。 懸念点の一つ目は、前節でも挙げたパフォーマンスである。負荷試験を行い、必要があればインフラを増強する必要がある。 また、別の問題点として、アプリケーションのDocker Imageへの改修が必要なことである。結果として、アプリケーションが複雑化してしまったり、切り戻り処理にそのアプリケーションのデプロイが必要になる可能性がある。
この問題を完全に回避するなら AWSの VPC トラフィックミラーリングを使って、GoReplayのキャプチャ用コンテナにリクエストを流すという方法が考えられそう。 https://aws.amazon.com/jp/blogs/news/new-vpc-traffic-mirroring/
そもそも別の手段でミラーリングするならGoReplayが不要とも考えられるが、以下の理由でこの構成もありかもしれない。
- リプレイ側で使える機能がとても便利そう(流量制限、複数ファイルの結合、同じファイルをループして利用、ミドルウェアなど)
- リプレイで使うファイルが独自形式
感想
多機能でドキュメントもしっかりしていて、よく考えられたツールという印象。大規模システムで使う場合はパフォーマンスやデータの要件など細かい点の検討が必要になるとは思う。
対象データリストをファイルで受け取ってPostgreSQLのデータを更新する
やりたいこと
IDのリストを外部からもらい、自分達のサービスのDBにデータパッチ(データの更新 or 削除など)を行いたい。
このとき、IDのリストはかなり大きいもの (>100)とする。
このとき、SQLのレビューや再利用性の観点から IN句を使うのは避けたい。
避けたい例 id in (id1, id2, id3, ... , id1000)
どういう状況でそういうことが起こるか
IDのリストを自サービスで抽出できる場合、ファイルから渡さなくてもサブクエリ等で対処可能な場合が多い。 しかし、以下のようなケースではこのようなワークアラウンドが有効。 - SELECTクエリが複雑すぎて負荷が心配 - 今回のやり方なら抽出用のReader instanceからIDのリストを持ってきて、Writer instanceで更新ということができる - マイクロサービス化していてDBが分かれており、IDのリストの検索が外部サービスによって行われている - 対象データをマニュアルでレビューして確認が取れたものだけに対して更新を実行したい
今回用いるシンプルな例
外部からもらったファイルid_list
1 3 5
自サービスのtableの状況
person=# select * from person; id | name | deleted ----+------+--------- 1 | a | f 2 | b | f 3 | c | f 4 | d | f 5 | e | f (5 rows)
できたもの
id_list
に含まれているIDを持つテーブル上のレコードを論理削除する。
対象テーブル: person
変更カラム: person.deleted (false -> true)
実行
$ cat id_list 1 3 5 $ export PGPASSWORD=${pass} && sh run.sh 1 UPDATE 1 3 UPDATE 1 5 UPDATE 1
結果
$ psql -h localhost -p 5432 -U root person psql (14.6 (Homebrew), server 12.13 (Debian 12.13-1.pgdg110+1)) Type "help" for help. person=# select * from person; id | name | deleted ----+------+--------- 2 | b | f 4 | d | f 1 | a | t 3 | c | t 5 | e | t (5 rows)
対象レコード(id:1,3,5)だけが deleted: t (true)
になっていることを確認。
つかうもの
server side - postgres:12.13
client side - shell script - SQL file - psql command
スクリプト/SQL
コード: https://github.com/yoshikipom/sandbox/tree/main/postgres/task/update-target
3ファイルを同じフォルダに置く。
$ ls id_list run.sh update.sql
id_list
1 3 5
今回はheaderなしの改行区切りのファイルとする。フォーマット変更や複数変数に対応する場合、 run.sh
でハンドリング。
update.sql
\echo :target_id UPDATE person SET deleted = true WHERE id = :target_id;
:{変数名}
で変数を展開できる。SQL上で文字列に展開したい場合は:'{変数名}'
(例:'target_id'
) とする。- 今回はIDがintegerなので
:target_id
。 - ログに削除したIDを表示するために
\echo
で変数の値を表示。
run.sh
#!/bin/bash while read id do if [ -n "${id}" ]; then psql -h localhost -p 5432 -U root person -v target_id=${id} -f update.sql fi done < id_list
-v key=value
でSQLに変数の値を渡せる。< id_list
とread id
で${id}
にファイルの各行にあるIDを代入。- 通常はpsqlでパスワードを求められるため、今回は環境変数を通して先に渡した。
$ export PGPASSWORD=${pass} && sh run.sh
- if文は
id_list
に空行があった場合にスキップするため。
感想
エンジニア的には1つのSQLでなんとかしようとしがちだが、読みやすさ (+ レビューしやすさ)、拡張性、再利用性、パフォーマンスの予想しやすさなどを考えるとシェルスクリプトを通してデータパッチするのもありだと学んだ。
参考
Spring Boot外部設定(application.yml)の保守性を上げる
Overview
spring bootの設定ファイルの保守性を上げる
- spring boot 2.7.0
- 設定ファイルはyml形式を使用
保守性をあげるために設定ファイルに求めること
- 環境に応じて切替可能
- ファイル分割(コンフリクトを避ける、設定がみつけやすい)
- 外部からの値の注入(シークレット情報を扱う場合は重要)
- 起動の簡単さ(必須設定が多すぎると新規開発者が苦労する)
Spring Bootの機能
基本
設定ファイルとそれを読み込むConfigクラスを準備。(@Valueでも同様に動くが今回は省略。)
src/main/resources/application.yml
myapp: configTest: key: value env_test: ${ENV_TEST} noenv: ${NO_ENV:default_value} flag: true env: no_env
ConfigTest.java
@Data @ConfigurationProperties(prefix = "myapp.config-test") @Component public class ConfigTest { private String key; private String key2 = null; private String envTest; private String noenv; private boolean flag; private String env; }
読み込みはAutowiredして呼びたい設定のgetterを呼べばOK。 (ここではテスト結果が見やすいようにtoString()を読んでます。)
@SpringBootApplication @Slf4j public class ConfigApplication implements CommandLineRunner { @Autowired private ConfigTest configTest; public static void main(String[] args) { SpringApplication.run(ConfigApplication.class, args); System.exit(0); } @Override public void run(String... args) throws Exception { log.info(configTest.toString()); } }
実行ログ
2022-08-21 20:39:03.100 INFO 62961 --- [ main] c.y.dev.config.ConfigApplication : ConfigTest(key=value, key2=null, envTest=injected_value, noenv=default_value, flag=true, env=no_env)
profile
例
application.yml
,application-dev.yml
,application-prod.yml
を準備- dev環境:
export spring_profiles_active=dev
してからアプリ実行 ->application-dev.yml
が読まれる - prod環境:
export spring_profiles_active=prod
してからアプリ実行 ->application-prod.yml
が読まれる
src/main/resources/application-dev.yml
myapp: configTest: env: dev
環境変数spring_profiles_active=dev
を設定して実行ログ確認
2022-08-21 16:59:05.057 INFO 61874 --- [ main] c.y.dev.config.ConfigApplication : The following 1 profile is active: "dev" ... 2022-08-21 16:59:07.101 INFO 61874 --- [ main] c.y.dev.config.ConfigApplication : ConfigTest(key=overwritten_by_external, key2=null, envTest=injected_value, noenv=default_value, flag=true, env=dev)
env=dev
になっているのでうまく動作している。
参考 https://www.baeldung.com/spring-profiles https://spring.pleiades.io/spring-boot/docs/current/reference/html/features.html#features.external-config.files.profile-specific
外部ファイル読み込み
src/main/resources/application.yml (追記)
spring: config: import: - "another.yml" - "optional:file:./spring-mvc/dev/external-config.yml"
src/main/resources/another.yml
myapp: configTest: key: overwritten_by_another
spring-mvc/dev/external-config.yml
myapp: configTest: key: overwritten_by_external
実行ログ
2022-08-21 16:59:07.101 INFO 61874 --- [ main] c.y.dev.config.ConfigApplication : ConfigTest(key=overwritten_by_external, key2=null, envTest=injected_value, noenv=default_value, flag=true, env=dev)
external-config.ymlが優先されるので値はkey=overwritten_by_external
となっている。
環境変数読み込み
「基本」の env_test: ${ENV_TEST}
を参照。${}
内で指定した環境変数が自動で展開される。このプレースホルダは設定ファイル内の他の値を展開したり文字列の結合したりも可能。
https://spring.pleiades.io/spring-boot/docs/current/reference/html/features.html#features.external-config.files.property-placeholders
default値の設定
環境変数が設定されない場合
env_test: ${ENV_TEST:default-value}
のように書ける。
他の値はJava class側のコンストラクタで設定しておけばデフォルトを持てるが、設定ファイル側に入れておいたほうが見やすい。
ConfigTest.java
@Data @ConfigurationProperties(prefix = "myapp.config-test") @Component public class ConfigTest { private String key = "default-value"; ...
環境変数 vs profile
環境変数でもprofileでも環境ごとの値の設定は可能。 - 環境変数 - シークレットを扱いやすい - 値の変更が容易 - yamlのような構造がないのでリスト型や辞書型の設定は持ちづらい - profile - 構造があるので多数の設定やリスト型、辞書型の設定も扱いやすい - シークレットを使うなら結局プレースホルダーで環境変数を入れることになりそう - JARに設定ファイルを入れているなら値の変更に再度ビルドが必要 設定値が少ないうちは環境変数で環境差異を吸収するのが良さそう。
感想
Spring Bootの外部設定管理まわりは機能が多くて、同じことを複数のやり方でできることが多い。 何を使って何を使わないか決めておくのがよさそう。
参考
Spring BootでREST API, MQ consumer, CLI, 定期実行タスクを実行
Overview
Spring BootはWebアプリケーションを実行する以外にもいろいろできるのでいくつか紹介する。
REST API
application
Spring Bootで一番よくあるアプリケーション。 HTTPリクエストを送ったらJSONレスポンスを得られる。
Spring Boot Application + @RestControllerを持つクラスを実装すれば実装できる。
application class
@SpringBootApplication public class ApiApplication { public static void main(String[] args) { SpringApplication.run(ApiApplication.class, args); } }
controller example
@RestController @RequestMapping("items") public class ItemController { @GetMapping("") public List<Item> get() { return List.of(new Item("value")); } }
実行
$ curl --request GET \ --url http://localhost:8080/items \ [{"key":"value"}]%
MQ consumer (worker)
application
Message Queueのconsumer. この例はkafkaの consumer (org.springframework.kafka:spring-kafka
を利用)。
@KafkaListenerでトピックを指定。シリアライザーなど細かい設定はapplication.yamlに書ける。
application class
@SpringBootApplication @EnableKafka @Slf4j public class ConsumerApplication { public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class, args); } @KafkaListener(topics = "my_topic") public void receive(String value) { log.info("Received value: {}", value); } }
application.yaml
spring: kafka: bootstrap-servers: localhost:9092 consumer: client-id: ${spring.application.name} group-id: ${spring.application.name}-group key-deserializer: org.apache.kafka.common.serialization.StringDeserializer value-deserializer: org.apache.kafka.common.serialization.StringDeserializer auto-offset-reset: earliest
実行
kafkaに対してメッセージをproduce.
$ cat test.json {"name":"myname"} $ kcat -b localhost:9092 -t my_topic -P -J -l test.json
アプリケーションのログ
2022-08-01 00:25:38.553 INFO 54511 --- [ntainer#0-0-C-1] o.s.k.l.KafkaMessageListenerContainer : ${spring.application.name}-group: partitions assigned: [my_topic-0] 2022-08-01 00:30:04.773 INFO 54511 --- [ntainer#0-0-C-1] c.y.dev.consumer.ConsumerApplication : Received value: {"name":"myname"}
コマンドラインアプリケーション
application
単発の処理を実行してすぐ終了するアプリケーション。 CommandLineRunnerの実装に@SpringBootApplicationをつける。 runメソッドの引数でコマンドライン引数を受け取れる。
@SpringBootApplication @Slf4j public class CliApplication implements CommandLineRunner { public static void main(String[] args) { SpringApplication.run(CliApplication.class, args); System.exit(0); } @Override public void run(String... args) throws Exception { log.info("arg size: {}", args.length); for (int i = 0; i < args.length; ++i) { log.info("args[{}]: {}", i, args[i]); } } }
実行
jarに固めて実行してみる
$ java -jar spring-mvc-0.0.1-SNAPSHOT.jar arg1 arg2 . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.7.0) 2022-08-01 00:33:47.676 INFO 54895 --- [ main] com.yoshikipom.dev.cli.CliApplication : Starting CliApplication using Java 18 on yoshiki-mbp.local with PID 54895 (/Users/yoshiki/Documents/work/java/spring-mvc/build/libs/spring-mvc-0.0.1-SNAPSHOT.jar started by yoshiki in /Users/yoshiki/Documents/work/java/spring-mvc/build/libs) 2022-08-01 00:33:47.679 INFO 54895 --- [ main] com.yoshikipom.dev.cli.CliApplication : No active profile set, falling back to 1 default profile: "default" 2022-08-01 00:33:48.732 INFO 54895 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2022-08-01 00:33:48.744 INFO 54895 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2022-08-01 00:33:48.744 INFO 54895 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.63] 2022-08-01 00:33:48.834 INFO 54895 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2022-08-01 00:33:48.834 INFO 54895 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1073 ms 2022-08-01 00:33:49.420 INFO 54895 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2022-08-01 00:33:49.436 INFO 54895 --- [ main] com.yoshikipom.dev.cli.CliApplication : Started CliApplication in 2.275 seconds (JVM running for 2.708) 2022-08-01 00:33:49.441 INFO 54895 --- [ main] com.yoshikipom.dev.cli.CliApplication : arg size: 2 2022-08-01 00:33:49.446 INFO 54895 --- [ main] com.yoshikipom.dev.cli.CliApplication : args[0]: arg1 2022-08-01 00:33:49.446 INFO 54895 --- [ main] com.yoshikipom.dev.cli.CliApplication : args[1]: arg2
定期実行アプリケーション
application
定期実行する処理。 interval(fixedRate)を設定する方法やcron形式で設定する方法がある。 @EnableSchedulingをapplicationクラスにつけて、@Scheduledをつけたメソッドを実装する。 上記のコマンドラインアプリケーションを定期的に実装しても同じことができるが、その場合アプリケーションの起動処理の後にやりたい処理が実行される。 こちらの場合は定期処理の前のアプリケーション実行はないが、常にSpring Bootアプリケーションは起動しておく必要がある。
@SpringBootApplication @EnableScheduling @Slf4j public class ScheduledApplication { public static void main(String[] args) { SpringApplication.run(ScheduledApplication.class, args); } // @Scheduled(cron = "0 * 9 * * ?") @Scheduled(fixedRate = 1000) public void receive() { log.info("run scheduled task: {}", LocalDateTime.now()); } }
実行
アプリのログ
... 2022-08-01 00:36:32.601 INFO 54945 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2022-08-01 00:36:32.612 INFO 54945 --- [ scheduling-1] c.y.dev.scheduled.ScheduledApplication : run scheduled task: 2022-08-01T00:36:32.612477 2022-08-01 00:36:32.615 INFO 54945 --- [ main] c.y.dev.scheduled.ScheduledApplication : Started ScheduledApplication in 4.381 seconds (JVM running for 4.891) 2022-08-01 00:36:33.613 INFO 54945 --- [ scheduling-1] c.y.dev.scheduled.ScheduledApplication : run scheduled task: 2022-08-01T00:36:33.613535 2022-08-01 00:36:34.616 INFO 54945 --- [ scheduling-1] c.y.dev.scheduled.ScheduledApplication : run scheduled task: 2022-08-01T00:36:34.616480
まとめ
Spring Bootのさまざまな機能をWeb API実装以外にも使える。
追記
Web APIはGraghQLやgRPCにも対応できる。 Batch処理に関しては大規模な処理ではSpring Batch (https://spring.io/projects/spring-batch) なども使える。 consumerはspring cloud streamを使った方がいろいろなMQに対応できる。
Mac Setup メモ (2022)
install
command line
mac app (productivity)
- alfred (luncher)
- vanilla (tool bar management)
- rectangle (window management)
- dropover (file management)
- karabiner (key setting)
- Logi Options (mouse setting)
mac app (development)
Mac Setting
- Keyboard > Shortcuts > Mission Control で Switch to Desktop # を on
- Accessibility > Display > Reduce motion を on
- Keyboard で modifier key、repeatの設定、function keyの設定を変更
VSCode Setting
github accountで同期できる
開発ツール for http (wiremock, REST Client plugin for VSCode)
Overview
- wiremockのdocker-composeでの起動
- REST Client plugin for VSCode
Environment
- MacOS
- docker, docker-compose
wiremock
一番シンプルなmockの設定
{ "request": { "method": "GET", "url": "/items" }, "response": { "status": 200, "jsonBody": { "id": 1, "name": "item-name" }, "headers": { "Content-Type": "application/json" } } }
ディレイをつける、request bodyの一致の確認、外部ファイルの利用などは以下を参照。 https://github.com/yoshikipom/sandbox/tree/main/wiremock/dev/wiremock/mappings
docker-composeでの起動
--- version: '3' services: mockServer: image: wiremock/wiremock:latest-alpine volumes: - "./dev/wiremock/:/home/wiremock/" ports: - 9000:8080 restart: on-failure command: - --verbose - --disable-banner
run
docker-compose up
REST Client plugin for VSCode
インストール
https://marketplace.visualstudio.com/items?itemName=humao.rest-client
リクエスト
ファイルを準備
### GET http://localhost:9000/items ### POST http://localhost:9000/items Content-Type: application/json { "name": "name" }
Send Requestを押す
Refference
My Repository
Spring Bootの@Asyncで非同期処理(thread poolの設定 + 動作確認)
Overview
メインスレッドの処理に大きな影響を与えずにに何かの処理をしたいときに実装した(APIでレスポンスタイムに影響を与えずになにかDBに保存したい等)。 その処理を確実に達成したい場合はMessage Queueをを使った方が無難。
以下のような要件を達成するような実装を紹介する。 - 非同期で実行し、メインスレッドはその完了を待たない - 何らかの理由でThreadが増えすぎてメイン処理を遅くすることは避けたい
Code: https://github.com/yoshikipom/java/tree/main/spring-mvc/src/main/java/com/yoshikipom/dev/api/async Ref - https://www.baeldung.com/spring-async - https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.html
Code
非同期処理を実行するThread
- Beanとして登録する
- Defaultでも
task
という名前のものが登録されている - 個人的には新規で作りたい
- 命名により目的が明示的
- 調査時にログにスレッド名が出たときにわかりやすい
- 設定が明示的
- 複数のプールを定義可能
- ここでした設定
- corePoolSize: キープするスレッドプール数
- queueCapacity: スレッドが足りないときに待ち状態にできる数
- maxPoolSize: 最大のスレッド数
- 他の設定はドキュメントを参照 https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.html
@Configuration public class MyTaskExecutorBean { @Bean(name = "myTask") public TaskExecutor myTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(1); executor.setQueueCapacity(1); executor.setMaxPoolSize(2); return executor; } }
上記の非同期スレッドで実装したい処理
@Async("${bean名}")
をメソッドにつける (publicメソッド限定)
@Service @Slf4j public class MyAsyncService { @Async("myTask") public void executeAsyncProcess() { log.info("start async process"); log.info("sleep 1s"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } log.info("finish async process"); } }
呼び出し側
myAsyncService.executeAsyncProcess()
が非同期処理@EnableAsync
を 実行クラスにつける- thread poolの設定のため4回連続で実行
- maxPoolSize + queueCapacityは受け付けられない。そのときは
TaskRejectedException
が起こる
@SpringBootApplication @EnableAsync @Slf4j public class MyApplication implements CommandLineRunner { @Autowired private MyAsyncService myAsyncService; public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } @Override public void run(String... args) throws Exception { log.info("start app"); try { // max pool size:2, queue size:1 myAsyncService.executeAsyncProcess(); // will be executed myAsyncService.executeAsyncProcess(); // will be executed on another thread myAsyncService.executeAsyncProcess(); // will be queued myAsyncService.executeAsyncProcess(); // will not be executed due to shortage of thread pool } catch (TaskRejectedException e) { log.warn(e.getMessage()); } log.info("finish app"); } }
実行結果
- メインの処理が終わってから非同期のメソッドが動いている
myTask-1
とmyTask-2
の二つは並列で動いている- 3つ目の処理はmaxPoolSizeを超えるので上記の2つのどちらかが終わるまで待ってから実行 (今回は
myTask-1
が先に空いた) - 4つ目の処理は受け付けられないため、Errorが発生 (catchに書いたWARN logが出ている)。
2022-07-07 21:44:23.914 INFO 66165 --- [ main] c.y.dev.api.async.MyApplication : Started MyApplication in 2.479 seconds (JVM running for 2.874) 2022-07-07 21:44:23.916 INFO 66165 --- [ main] c.y.dev.api.async.MyApplication : start app 2022-07-07 21:44:23.918 WARN 66165 --- [ main] c.y.dev.api.async.MyApplication : Executor [java.util.concurrent.ThreadPoolExecutor@32eae6f2[Running, pool size = 2, active threads = 2, queued tasks = 1, completed tasks = 0]] did not accept task: org.springframework.aop.interceptor.AsyncExecutionInterceptor$$Lambda$785/0x0000000800fe2a40@550e9be6 2022-07-07 21:44:23.918 INFO 66165 --- [ main] c.y.dev.api.async.MyApplication : finish app 2022-07-07 21:44:23.924 INFO 66165 --- [ myTask-1] c.y.dev.api.async.MyAsyncService : start async process 2022-07-07 21:44:23.924 INFO 66165 --- [ myTask-1] c.y.dev.api.async.MyAsyncService : sleep 1s 2022-07-07 21:44:23.924 INFO 66165 --- [ myTask-2] c.y.dev.api.async.MyAsyncService : start async process 2022-07-07 21:44:23.924 INFO 66165 --- [ myTask-2] c.y.dev.api.async.MyAsyncService : sleep 1s 2022-07-07 21:44:24.928 INFO 66165 --- [ myTask-1] c.y.dev.api.async.MyAsyncService : finish async process 2022-07-07 21:44:24.928 INFO 66165 --- [ myTask-2] c.y.dev.api.async.MyAsyncService : finish async process 2022-07-07 21:44:24.929 INFO 66165 --- [ myTask-1] c.y.dev.api.async.MyAsyncService : start async process 2022-07-07 21:44:24.929 INFO 66165 --- [ myTask-1] c.y.dev.api.async.MyAsyncService : sleep 1s 2022-07-07 21:44:25.933 INFO 66165 --- [ myTask-1] c.y.dev.api.async.MyAsyncService : finish async process
Conclusion
Thread数(とキューイングする数)を制限した非同期処理が実装できた。
CompletableFuture
や Future
を非同期処理の戻り値に使えば非同期処理を待って戻り値を得ることも可能。