[連載]Clojureで作るAPIの4記事目です。
前回の記事はこちらです。
この記事ではREPLを再起動することなくWebサーバーを再起動できるようにしていきます。
なぜ再起動できる必要があるのか
そもそもなんでそんなことが必要なのかというと、コードを書き換えた後で、REPLに持たせている状態を今のコードに合わせて即座に置き換えたいからです。
前回の記事で (jetty/run-jetty ring-handler {:port 8000})
という式を使ってWebサーバーを起動しました。
引数に与えている関数 ring-handler
と マップ {:port 8000}
は立ち上がったサーバーが保持していますから、サーバーを再起動しないことには置き換えることはできません。しかしそのためにいちいちREPLを再起動していては時間がかかってしまって生産的ではありません。
また、このような状態を持つものが複数あり、かつその起動が順序通りに行われる必要がある場合に注意しながら手動で起動していくなんてことはやりたくありません。
そこでそういったことを可能にするライブラリを使って宣言的に再起動できるようにしていきます。
使用するライブラリを追加
ライブラリComponentを追加します。
Componentは、
'Component' is a tiny Clojure framework for managing the lifecycle and dependencies of software components which have runtime state.
とあるようにまさに今回の目的のために作られたライブラリです。 frameworkとありますが、フレームワークと聞いたときにイメージする例えばRuby on Railsのようなものではなく、シンプルで学習コストも小さいのでご安心ください。
deps.ednを次のようにします。
;; ./deps.edn {:paths ["src"] :deps {org.clojure/clojure {:mvn/version "1.11.1"} info.sunng/ring-jetty9-adapter {:mvn/version "0.17.6" :exclusions [org.slf4j/slf4j-api]} spootnik/unilog {:mvn/version "0.7.30"} ;; 追加 com.stuartsierra/component {:mvn/version "1.1.0"}}}
Componentを実装
ライブラリComponentにおいてはLifecycleというプロトコルを実装しているレコードなどをコンポーネントと呼びます。
(defprotocol Lifecycle :extend-via-metadata true (start [component] "Begins operation of this component. Synchronous, does not return until the component is started. Returns an updated version of this component.") (stop [component] "Ceases operation of this component. Synchronous, does not return until the component is stopped. Returns an updated version of this component."))
startに起動時の処理を書き、stopに停止時の処理を書きます。
コンポーネントという言葉が繰り返し出てきて紛らわしいですが、本記事では次の2つの意味で使っています。
- ライブラリのComponent
- ライブラリのComponentの要求に従って作ったコンポーネント
まずはRing handlerを提供するコンポーネントを実装します。 今は単純な関数なのでわざわざコンポーネントにする意味が感じられないですが、例えばテスト環境と本番環境で挙動を変えたい時などでコンポーネントにしておくと切り替えができて便利です。
;; ./src/cljapi/component/handler.clj (ns cljapi.component.handler (:require [com.stuartsierra.component :as component])) (defn- ring-handler [_req] {:status 200 :body "Hello, Clojure API"}) (defrecord Handler [handler] component/Lifecycle (start [this] (assoc this :handler ring-handler)) (stop [this] (assoc this :handler nil)))
次にサーバーを起動停止するコンポーネントを実装してみます。
;; ./src/cljapi/component/server.clj (ns cljapi.component.server (:require [com.stuartsierra.component :as component] [ring.adapter.jetty9 :as jetty])) (defrecord Jetty9Server [handler opts server] ;; handlerは :handler をキーにもつマップ(= コンポーネント)であることを期待している component/Lifecycle (start [this] (if server this (assoc this :server (jetty/run-jetty (:handler handler) opts)))) (stop [this] (when server (jetty/stop-server server)) (assoc this :server nil)))
Systemを構築する
ライブラリComponentではシステムというマップを定義して、作成したコンポーネントの依存関係を定義し、まとめて起動停止できるようにします。
依存関係を component/using
を使って定義しておくと、ライブラリ側でよしなに起動順序(startを呼ぶ順序)を制御してくれます。
;; ./src/cljapi/system.clj (ns cljapi.system (:require [cljapi.component.handler :as c.handler] [cljapi.component.server :as c.server] [com.stuartsierra.component :as component])) (defn- new-system [] (component/system-map :handler (c.handler/map->Handler {}) :server (component/using (c.server/map->Jetty9Server {:opts {:join? false :port 8000}}) ;; component/usingの第二引数で依存しているコンポーネントを宣言している [:handler]))) (defn start [] (let [system (new-system)] (component/start system))) (defn stop [system] (component/stop system)) (defonce system (atom nil)) (defn go [] (when @system (stop @system) (reset! system nil)) (reset! system (start)))
component/start
を実行すると起動したコンポーネントが返ってくるので、それをatomで保持しておくことで後から停止できるようにしています。
また、go
という関数で繰り返し起動させることができるようにしています。
namespaceを移動して go
を実行してみます。
※namespaceを評価しておくのを忘れないようにしてください。
user> (in-ns 'cljapi.system) #namespace[cljapi.system] cljapi.system> (go) 12:51:40.387 [nREPL-session-e6ae6d15-5885-4f3b-912b-741243dab0a0] DEBUG org.eclipse.jetty.util.component.ContainerLifeCycle - QueuedThreadPool[qtp773034985]@2e1393e9{STOPPED,8<=0<=50,i=0,r=-1,q=0}[NO_TRY] added {org.eclipse.jetty.util.thread.ThreadPoolBudget@5eabf21,POJO} 12:51:40.390 [nREPL-session-e6ae6d15-5885-4f3b-912b-741243dab0a0] DEBUG org.eclipse.jetty.util.component.ContainerLifeCycle - Server@4f689547{STOPPED}[10.0.9,sto=0] added {QueuedThreadPool[qtp773034985]@2e1393e9{STOPPED,8<=0<=50,i=0,r=-1,q=0}[NO_TRY],AUTO} # 出力が続く... {:handler {:handler #function[cljapi.component.handler/ring-handler]}, :server {:handler {:handler #function[cljapi.component.handler/ring-handler]}, :opts {:join? false, :port 8000}, :server #object[org.eclipse.jetty.server.Server 0x4f689547 "Server@4f689547{STARTED}[10.0.9,sto=0]"]}}
最後に表示されているのがgoの戻り値、つまりstartしたシステムマップです。
:server
に起動した( :handler
が生成されている)handlerコンポーネントが注入されていることがわかります。
curlで確かめてみます。
$ curl http://localhost:8000 Hello, Clojure API
最後にmain関数も置き換えておきましょう。
;; ./src/cljapi/core.clj (ns cljapi.core (:require [cljapi.system :as system])) (defn -main [& _args] (system/start))
main関数はサーバー起動時に使う関数で、停止する必要がないので、goではなくstartを使っています。
おわりに
これでREPLを起動したまま、サーバーのように状態を持つものを再起動できるようになりました。
コードはGitHubのリポジトリに上げてあります。 04_ライフサイクルと依存関係を管理できるようにするというタグからご覧ください。
次は開発時に使う関数を分離して、より開発を進めやすくしていきます。
トヨクモでは一緒に働いてくれる技術が好きなエンジニアを募集しております。