実行モデル#
リクエスト1本につき仮想スレッドが1本です。その上で、最初から最後まで同期的に走ります。
- スレッドプールの設定はありません(Helidon に任せています)
CompletableFutureを返す API はありません- **ブロッキングして構いません。**JDBC もそのまま呼びます
1リクエストの流れ#
onRequest
↓
before(外側 → 内側)
↓
ルートの処理 / Executor を順に実行
↓
送信
↓
(例外なら)error(内側 → 外側)
↓
after(内側 → 外側)※必ず通る
↓
onComplete ※必ず通る
onRequest はルートに当たらなくても呼ばれます。
after と onComplete は finally にあるので、例外が出ても必ず通ります。
「送ったら止まる」は1か所で見ている#
before でログイン画面を返したら、その先の before・ルート・Executor は走りません。
この判定は1か所(Stage)に閉じています。
after と onComplete は走ります。この2つは finally にあるので、
送信済みでも例外が出ても必ず通ります(上の図のとおりです)。
「送ったら止まる」のは送信までの段だけです。
補足
移送元(jooby_base)は同じ判定を7か所にコピーしていました。 1か所に集めたのは、書き足したところだけ判定を忘れるのを防ぐためです(要件 F-C-13)。
Executor#
ルートは処理を直接書くほかに、Executor をキューに積む書き方ができます。
post("/save", () -> new SaveExecutor());
- キューは実行中にも積めます(
context.addExecutor(...)) cancel()を呼んでもその場では止まりません。executeから戻ったあとに残りが捨てられ、onCancelが呼ばれますOPTIONSはプリフライト専用なので、キューを回しません(要件 F-W-19)
どこが何のスレッドで動くか#
| 仕組み | スレッド |
|---|---|
| HTTP リクエスト | Helidon の仮想スレッド(リクエストごとに1本) |
AsyncData / AsyncList の読み込み・先読み |
**呼び出しスレッドそのもの。**名前に反して別スレッドは使いません |
| SSE | そのリクエストの仮想スレッドを掴んだまま |
| WebSocket | Helidon のリスナのスレッド(接続ごと) |
| MQ のワーカー | 仮想スレッド。型ごとに mq.thread_count.<型> 本 |
| スケジューラの時計 | プラットフォームスレッド1本(jimble-scheduler) |
| スケジューラが起動するバッチ | 仮想スレッド。scheduler.execute_threads 本(0 で無制限) |
| バッチのハートビート | 仮想スレッド1本(jimble-batch-heartbeat) |
本数を絞りたいところでは VirtualThreadManager を使います。
補足
VirtualThreadManager(n) の n は「同時に走る本数」です。
セマフォを取るのは仮想スレッドを起こした後なので、待っているスレッドは作られています。
仮想スレッドは安いので実害はありませんが、「n 本しか作らない」ではありません。
詰まるのはスレッドではなく接続プール#
仮想スレッドはいくらでも増えます。先に尽きるのは DB の接続です。
落とし穴
トランザクションの中や selectListWithFetcher の途中は、接続をプールに返していません。
その状態で SSE のように長く繋ぐと、人数分だけプールを食います。
maximumPoolSize = 10 なら10人で尽き、11人目は「遅い」でも「エラー」でもなく
接続待ちで止まったままになります。
SSE で無限ループを書かないのはこのためです。 有限のぶんを送って閉じ、クライアントに繋ぎ直させてください(SSE)。
仮想スレッドで気をつけること#
synchronized を使わない |
仮想スレッドがキャリアに固定されます。ReentrantLock を使ってください(jimble 自身もそうしています) |
| 共有可変状態を守る | static な HashMap は同時アクセスで壊れます。「たまたま動いていた」が起きにくいぶん、仮想スレッドでは実際に壊れます |
ThreadLocal に頼らない |
jimble はリクエストの受け渡しに ScopedValue を使っています(Context.current()) |
止めるとき#
止め方は1か所(Shutdown)に預けます。
Shutdown.add("わたしのワーカー", worker::stop);
| 順番 | 後に預けたものから先に止める(立てた順の逆)。DB を先に閉じると、止まる途中のものが DB を使えないため |
| 失敗したら | **残りは止める。**止められなかったものの名前が返る |
| jimble が預けるもの | DB プール / サーバー / スケジューラ |
補足
SIGTERM を受けると、預けたものが逆順に止まります(サーバー設定)。 コンテナは SIGTERM を送って待つので、受け取らずに死ぬと 処理中のリクエストが途中で切れます。
普通に動かしているぶんには要りません。プロセスが終われば全部止まります。
要るのはプロセスを終わらせずにアプリを入れ替えるとき、つまり jimbleRun です。
止め忘れたものは入れ替え後のものと二重に動き、「同じジョブが2回走る」という形で出ます
(ホットリロード)。
リクエスト1本ぶんの後始末は Shutdown ではなく context.onClose(...) です。
登録の逆順に呼ばれ、1つ失敗しても残りを畳みます。