Lambda Prelude は常駐バックエンドサービス向け。デプロイの基本方針は、単一ネイティブバイナリを生成し、それを Linux の init システム / コンテナ / オーケストレータで動かすことにある。

ビルド

lambda-prelude build src/main.lp --out my-service

出力は ocamlopt がリンクした本物のネイティブバイナリ。実行時にインタプリタは不要。レシーバを型検査器が特定できる送信は直接 OCaml 呼び出しに落ち、それ以外はバイナリが link するランタイムディスパッチャを通る。

クロスプラットフォーム

機能が完成扱いとなる前に両ターゲットを通す。片方でしか動かない変更は未完成扱い。

プラグイン

PostgreSQL / MariaDB / MySQL、Redis / Valkey、SMTP、TORM (dao: / entity: キーワード) はプラグインとして本体から分かれている。プログラムがプラグインを取り込む形は 2 つあり、AOT バイナリでの扱いが違う。

imports: で取り込む — imports: (Torm) のように、同名の .lp モジュールが無い名前はプラグインに解決される。インタプリタはプログラムを読み込む時点でそれを OCaml の Dynlink で開くので、プラグインの DSL キーワード (dao: など) がソースを読んでいる間から使える。AOT バイナリでは、こうして解決されたプラグインはビルド時にリンクされてバイナリの中に入る。出荷物は 1 ファイルのままで、そのプラグインを実行時に探すことも、実行ファイルの横に置くことも、ビルドプロファイルを配備側で合わせることも無い。

Plugin load: 'name' で取り込む — 実行時の送信なので、インタプリタでも AOT バイナリでも、その時点で .cmxs を探して Dynlink で開く。stdlib のプール (PostgresPool / MariadbPool / RedisPool) と Smtp モジュールはこの形で、対応するプラグインを実行時の探索パス上に置く必要がある。

実行時に開く場合の探索順 (最初に見つかったものを使う):

  1. $LAMBDA_PRELUDE_PLUGINS が設定されていればその 1 ディレクトリ (override)
  2. ワーキングディレクトリ直下の ./plugins/
  3. 実行中バイナリ横の <exe_dir>/plugins/ (インストール配置)
  4. <exe_dir>/../plugins/ (Linux パッケージ配置)
  5. _build/default/plugins/ (開発ワークフロー)

インタプリタと AOT バイナリで使えるプラグイン機能に差は無い。違うのは imports: 経由のものがリンク済みになるか否かだけ。

サービスレイアウト

小規模サービスの典型構成は次のとおり。

my-service/
  main.lp
  config.lp
  service/
    XA001Save.lp
    XA001GetAll.lp
  tests/
    save_test.lp
    getall_test.lp

lambda-prelude new my-service がエントリポイントとテストディレクトリを生成する。

lambda-prelude new my-service
cd my-service
lambda-prelude run main.lp
lambda-prelude test tests
lambda-prelude build main.lp --out my-service

init 統合

バイナリは普通の常駐プロセス。ホスト OS へは他のデーモンと同じように繋ぎ込む。

随伴 VM もアップロードするランタイムイメージも JIT ウォームアップ窓もない。

クラッシュ復旧

バイナリ内部ではアクターレベルの失敗をスーパーバイザツリーが捌く。バイナリ間ではプロセスレベルの失敗をホスト OS の再起動ポリシーが捌く。

スーパーバイザツリーを内層 (ミリ秒オーダーのプロセス内再起動)、OS init を外層 (秒オーダーのプロセス再起動) と見なす。Erlang の supervisor と heart の分担と同じ分け方。