jimble

実行モデル#

リクエスト1本につき仮想スレッドが1本です。その上で、最初から最後まで同期的に走ります。

  • スレッドプールの設定はありません(Helidon に任せています)
  • CompletableFuture を返す API はありません
  • **ブロッキングして構いません。**JDBC もそのまま呼びます

1リクエストの流れ#

onRequest
  ↓
before(外側 → 内側)
  ↓
ルートの処理 / Executor を順に実行
  ↓
送信
  ↓
(例外なら)error(内側 → 外側)
  ↓
after(内側 → 外側)※必ず通る
  ↓
onComplete ※必ず通る

onRequestルートに当たらなくても呼ばれますafteronCompletefinally にあるので、例外が出ても必ず通ります。

「送ったら止まる」は1か所で見ている#

before でログイン画面を返したら、その先の before・ルート・Executor は走りません。 この判定は1か所(Stage)に閉じています。

afteronComplete は走ります。この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 自身もそうしています)
共有可変状態を守る staticHashMap は同時アクセスで壊れます。「たまたま動いていた」が起きにくいぶん、仮想スレッドでは実際に壊れます
ThreadLocal に頼らない jimble はリクエストの受け渡しに ScopedValue を使っています(Context.current()

止めるとき#

止め方は1か所(Shutdown)に預けます。

Shutdown.add("わたしのワーカー", worker::stop);
順番 後に預けたものから先に止める(立てた順の逆)。DB を先に閉じると、止まる途中のものが DB を使えないため
失敗したら **残りは止める。**止められなかったものの名前が返る
jimble が預けるもの DB プール / サーバー / スケジューラ

補足

SIGTERM を受けると、預けたものが逆順に止まりますサーバー設定)。 コンテナは SIGTERM を送って待つので、受け取らずに死ぬと 処理中のリクエストが途中で切れます。

普通に動かしているぶんには要りません。プロセスが終われば全部止まります。 要るのはプロセスを終わらせずにアプリを入れ替えるとき、つまり jimbleRun です。 止め忘れたものは入れ替え後のものと二重に動き、「同じジョブが2回走る」という形で出ます (ホットリロード)。

リクエスト1本ぶんの後始末は Shutdown ではなく context.onClose(...) です。 登録の逆順に呼ばれ、1つ失敗しても残りを畳みます。