lein-ductで作成したプロジェクトのREPL環境とmain実行環境でコンポーネントの初期化範囲に差異があるというメモ。
違いを確認する
duct 0.11.0-beta1で以下のようにプロジェクトを作成。
lein new duct-beta example
以下の4コンポーネントを定義。
- :example.animal/cat
- :example.animal/dog
- :example.color/red
- :example.color/blue
resources/example/config.edn
{:duct.profile/base
 {:duct.core/project-ns example
  :example.animal/cat {:dog #ig/ref :example.animal/dog
                       :colors #ig/refset :example/color}
  :example.animal/dog {}
  :example.color/red {}
  :example.color/blue {}}
 :duct.profile/dev   #duct/include "dev"
 :duct.profile/local #duct/include "local"
 :duct.profile/prod  {}
 :duct.module/logging {}}
catは :duct/daemon 、 redとblueは :example/color に derive して is-a 関係を持たせる。
src/duct_hierarchy.edn
{:example.animal/cat [:duct/daemon]
 :example.color/red [:example/color]
 :example.color/blue [:example/color]}
REPLから起動する
REPLから起動。
lein repl
user=> (dev)
dev=> (go)
cat、dog 、red、blueの全てのコンポーネントのinit-keyが実行され、catの :dog には dogコンポーネント :colors には red と blue コンポーネントのセットが渡される。
main関数から起動する
mainから起動。
lein run
dog、catコンポーネントのinit-keyが実行され、catの :dog には dogコンポーネント。 redとblueは初期化されないため :colors には空のセットが渡される。
なぜ違いがあるのか?
単純は話でREPLとmainでintegrantの初期化方法が異なっている。
lein-ductで生成されるのはただのテンプレートなので必要に応じてカスタマイズすれば良いというだけの話なのだが、自分の場合はコードの内容を把握せずに先入観で全てのコンポーネントが初期化されるはずと思い込んでいた。
REPL
REPLから使用していた integrant-repl の go関数は以下のようになっており、
(defn go []
  (prep)
  (init))
上記の integrant.repl/init は integrant.repl/init-system を通じて 最終的に integrant.core/init を呼び出している。
(defn- init-system [config]
  (build-system
   #(ig/init config)
   #(ex-info "Config failed to init; also failed to halt failed system"
             {:init-exception %1}
             %2)))
ig/init では 第二引数のkeysを明示していないためコンフィグレーションマップに含まれる全てコンポーネントが初期化対象となる。
main関数
一方mainはと言うと、起動引数でkeysを指定できるようになっており、指定されなかった場合は :duct/daemon にderiveされたコンポーネントとその依存コンポーネントを初期化対象としている。
(defn -main [& args]
  (let [keys     (or (duct/parse-keys args) [:duct/daemon])
        profiles [:duct.profile/prod]]
    (-> (duct/resource "example/config.edn")
        (duct/read-config)
        (duct/exec-config profiles keys))))
上記の duct.core/exec-config は以下のように keys付きで integrant.core/init を呼び出している。
(defn exec-config
  "Build, prep and initiate a configuration of modules, then block the thread
  (see [[await-daemons]]). By default it only runs profiles derived from
  `:duct.profile/prod` and keys derived from `:duct/daemon`.
  This function is designed to be called from `-main` when standalone operation
  is required."
  ([config]
   (exec-config config [:duct.profile/prod]))
  ([config profiles]
   (exec-config config profiles [:duct/daemon]))
  ([config profiles keys]
   (-> config (prep-config profiles) (ig/init keys) (await-daemons))))
対応方法
基本的にはmainのようにルートのコンポーネントを指定しそこからの依存コンポーネントツリーに基づいての初期化で問題ないのだが、
今回のケースのように derive したコンポーネントの一覧を ig/refset で参照する場合、これは直接的な依存関係と見なされないようなので明示的にキーを指定し初期化対象として宣言する必要があるようだ。
(defn -main [& args]
  (let [keys     (or (duct/parse-keys args) [:duct/daemon :example/color])
        profiles [:duct.profile/prod]]
    (-> (duct/resource "example/config.edn")
        (duct/read-config)
        (duct/exec-config profiles keys))))