Pedestalのコンセプト
Microservices with Clojureより。前回はマイクロサービスのためのRESTのAPI概要について述べた。今回は、Pedestalを利用したClojureによる具体的なマイクロサービスの為のREST APIの実装について述べる。
PedestalはAPIファーストのClojureフレームワークで、動的な性質を持つ信頼性の高い並行サービスを構築するためのライブラリ群を提供するデータ駆動型の拡張可能なフレームワークであり、コンポーネント間の結合を減らすためにプロトコルを使って実装されている。関数よりもデータを、マクロよりも関数を優先する。データ駆動型のルートやハンドラを作成することができ、入力されたリクエストに応じて実行時に異なる動作をさせることができる。これにより、マイクロサービスベースのアプリケーションに適した、柔軟で動的なサービスを作成することができる。また、SSE(server-sent events)やWebSocketを利用したスケーラブルな非同期サービスの構築もサポートする。

Pedestalのアーキテクチャは、InterceptorsとContext Mapという2つのメインコンセプトと、Chain ProviderとNetwork Connectorsという2つのサブコンセプトで構成されている。PedestalフレームワークのコアロジックはすべてInterceptorとして実装されているが、HTTPコネクションハンドラは分離され、実行を開始するための初期コンテキストマップとInterceptorのキューをセットアップするChain Providerのインターフェースが作成されている。Pedestal にはサーブレットチェーンプロバイダが含まれており、サーブレットを扱う全ての HTTP サーバで動作する。また、インターセプターがコントラクトに従って必要とするキーをコンテキストマップにセットアップする。PedestalアプリケーションはHTTPだけに限定されるものではない。他のアプリケーションのプロトコルをサポートするために、カスタムチェーンプロバイダを書くこともできる。また、ターゲットチェーンプロバイダとネットワークコネクタの組み合わせにより、信頼性の高いUDPのような異なるトランスポートプロトコルで動作させることも可能となる。
Pedestalは当初、PedestalアプリケーションとPedestalサーバーという2つの独立した部分を持っていた。Pedestalアプリケーションは、ClojureScriptベースのフロントエンドフレームワークであったが、現在は廃止されている。現在では、信頼性の高いサービスとAPIを構築するために、Pedestalサーバにのみ焦点が当てられている。
Interceptors
PedestalのInterceptorはインターセプターと呼ばれるソフトウェアデザインパターンをベースにしている。インターセプターとは、興味のあるイベントをフレームワークに登録し、それらのイベントが制御フロー内で発生したときにフレームワークから呼び出されるサービス拡張となる。インターセプターが呼び出されると、その機能を実行し、制御フローはフレームワークに戻る。
Pedestalのコアロジックのほとんどは、一つまたは複数のインターセプターで構成されており、それらを組み合わせてインターセプターのチェーンを構築することができる。PedestalのインターセプターはClojureのMapで定義され、以下の図のように :name, :enter, :leave, :error のキーを含んでいまる。nameキーはインターセプターの名前空間キーワードを含み、省略可能となる。:enterキーと:leaveキーはContext Mapを入力として受け取り、Context Mapを出力として返すClojureの関数を指定する。インターセプターをPedestalフレームワークに登録するには、:enterキーまたは:leaveキーのどちらかが定義されている必要がある。:enterキーで指定された関数は、インターセプターにデータが流れ込むとPedestalフレームワークから呼び出される。:leaveで指定された関数は、インターセプターからレスポンスが返されるときに呼び出される。

:error で指定された関数は、インターセプターの実行によって例外イベントが発生したときや、エラーで実行が失敗したときに呼び出される。例外イベントを処理するために、:errorキーはContext Mapとインターセプターがスローしたex-info 例外を2つの引数に取るClojure関数を指定する。これは、他のインターセプターが処理できるように例外を再接続したコンテキストマップを返すか、Pedestalフレームワークが処理できるように例外を投げる。
インターセプターチェーン
Pedestal の Interceptor は Chain of Responsibility というデザインパターンに従って Interceptor チェーンとして構成することができまる。それぞれのインターセプターは正確に一つの仕事をこなし、インターセプターチェーンとしてまとまると、一つまたは複数の仕事からなる大きな仕事を達成することができる。
Chain of Responsibility パターンは、インターセプターチェーンの複合構造をナビゲートするのに役立つ。インターセプターチェーン内の制御の流れは Context Map によって制御される。Context Map 自体が各 interceptor の入力として渡されるので、interceptor は任意にチェーン内の interceptor を追加したり削除したり、並べ替えたりすることができる。ルーティングやコンテントネゴシエーション、リクエストハンドラなど、WebフレームワークのモジュールのほとんどがPedestalのインターセプターとして実装されているのは、このような理由によるものである。
:enter と :leave のインターセプター関数は、実行フローが次のインターセプターに進むために、Context Map を値として返す必要がある。もし、nil を返した場合、Pedestalフレームワークは内部サーバエラーを報告し、実行フローは終了する。インターセプターはCore.asyncチャンネルを返すかもしれない。その場合、チャネルはコンテキストマップを将来引き渡すという約束のように扱われる。チャネルが Context Map を送出すると、チェインエクゼキュータはチャネルを閉じる。

チェーンエグゼキュータは core.async ライブラリの go ブロックで各インターセプターを呼び出すので、あるインターセプターが次のスレッドと異なるスレッドで呼び出されても、すべてのバインドがそれぞれのインターセプターに伝達されることになる。インターセプターがリクエストの処理や外部APIの呼び出しに時間がかかるようなシナリオでは、 goブロックを使って返り値としてチャンネルを送り、 Pedestalに非同期で実行を続けさせることが推奨される。Pedestalは、出力としてチャネルを受け取ると、インターセプタースレッドを降ろし、チャネルから値が生成されるのを待つ。チャネルから消費される値は1つだけで、それはコンテキスト・マップでなければならない。
前の図に示すように、インターセプターチェーンを実行している間、すべての :enter 関数はチェーンにリストされているインターセプターの順番に呼び出されます。すべてのインターセプターの :enter 関数が呼ばれると、その結果の Context Map は:leave関数のを通して逆順に呼び出される。
チェイン内のどのインターセプターも非同期に返すことができるため、Pedestal はインターセプターの仮想コールスタックと呼ばれるものを作成する。これは :enter 関数を呼び出すべきインターセプターのキューと、 :enter 関数は呼び出されたが :leave 関数は保留されているインターセプターのスタックを保持する。スタックを保持することで、Pedestal はチェーン上のインターセプターの :enter 関数の呼び出しと逆の順序で :leave 関数を呼び出すことができる。これらのキューとスタックは両方ともコンテキストマップに保持され、インターセプターからアクセス可能となる。
Interceptor は Context Map にアクセスできるので、 チェーン内の Interceptor の順番を変更することで、 残りのリクエストの実行計画を変更することができる。彼らは追加のインターセプターをエンキューするだけでなく、 チェーンに残っているインターセプターをすべてスキップしてリクエストを終了させることもできる。
Context Mapの重要性
コンテキストとは、インターセプターが入力として受け取り、また出力として生成するClojureマップに過ぎない。インターセプター、チェーン、実行スタック、キュー、インターセプターによって生成されたり、インターセプターチェーンに残っているインターセプターによって必要とされるかもしれないコンテキスト値など、Pedestalアプリケーションを制御するすべての値を含んでいる。コンテキストマップはまた、一連の述語関数を含んでいます。述語関数のうち一つでも真を返せば、チェインは終了する。非同期機能を有効にし、プラットフォームとインターセプターの間のインタラクションを容易にするために、必要なすべての関数とシグナルキーもContext Map内に定義されている。ここに示す表は、インターセプターチェーンがContext Mapに保持するキーの一部を示したものとなる。

もしインターセプターによって :bindings map が変更され、出力のコンテキストマップに返された場合、Pedestal はチェーン内の次のインターセプターを実行する前に、新しいバインディングをスレッドローカルバインディングとしてインストールする。io.pedestal.interceptor.chain/queue コンテキストキーには、まだ実行されていないすべての interceptor が含まれている。キューの最初のインターセプターは、:enter 関数を呼び出すことで次に実行されるとみなされるインターセプターとなる。このキーはデバッグのためにのみ使用しなければならない。キューや実行の流れを変更するためには、このキーの値を変更する代わりに、インターセプターチェーンのenqueue、terminate、terminate-when を呼ばなければならない。io.pedestal.interceptor.chain/terminators のキーで指定された終了述語は :enter関数が呼ばれるたびに、真の述語があるかどうかを調べる。有効な述語が見つかった場合、Pedestalは残りのすべてのインターセプターの:enter関数をスキップして、スタック内のインターセプターの:leave関数を実行し、実行フローを終了させ始める。
io.pedestal.interceptor.chain/stack コンテキストキーには、 :enter 関数がすでに呼び出されているが :leave 関数が保留されている Interceptor が含まれる。スタックの一番上にあるインターセプターが最初に実行され、:leave 関数が :enter 関数の呼び出しと逆の順序で呼び出されるようにする。
インターセプターチェーンによって追加されたキーに加えて、コンテキストマップは他のインターセプターによって追加されたキーも含むことができる。例えば、Pedestal が標準で提供するサーブレットインターセプター は、 :servlet-request, :servlet- response, :servlet-config, :servlet など、サーブレット固有のキーをコンテキストマップに追加している。HTTPサーバーを扱う場合、Context Mapには:requestと:responseというキーもあり、それぞれrequest と responseというマップが割り当てられている。
Pedestalは、HTTPサービスだけにとどまらず、様々なサービスを拡張することができる。Kafkaのようなシステムにサービスを拡張したり、SCTP、Reliable UDP、UDTなどの異なるプロトコルを使用することもできる。pedestal.service Pedestalモジュールは、HTTPに特化したインターセプターの集合体となる。
Pedestalサービスを実装する
Pedestalは、Pedestalサービスに必要な依存関係とディレクトリレイアウトを持つ新しいプロジェクトを作成するために、pedestal-serviceという名前のleiningenテンプレートを提供している。このテンプレートを使って新しいプロジェクトを作成するには、以下のようにテンプレート名とプロジェクト名を指定して、leinコマンドを使用する。
lein new pedestal-service pedestal-play
leinコマンドは、指定されたプロジェクト名で新しいディレクトリを作成し、project.cljファイルに必要なすべての依存関係を追加する。作成されたプロジェクトのディレクトリツリーは、以下のようになる。
> tree pedestal-play
pedestal-play
├── Capstanfile
├── config
│ └── logback.xml ├── Dockerfile
├── project.clj
├── README.md
├── src
│ |_____pedestal_play
│ |_______ server.clj
│ |_______ service.clj
└── test
└── pedestal_play
└── service_test.clj
プロジェクトを実行するには、lein runコマンドを使用するだけで、テンプレートで定義されたサンプルサービスがコンパイルされ、8080番ポートで開始される。サービスをテストするには、ブラウザでhttp://localhost:8080 と http://localhost:8080/about を開き、レスポンスを観察できる。最初のURLではHello World! が、2番目のURLではClojure 1.10.0 – served from /aboutがレスポンスとして返される。
また、両エンドポイントは、以下のようにcURLからアクセスすることもできる。
>curl -v http://localhost:8080
* Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.79.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Tue, 30 Aug 2022 00:59:05 GMT
< Strict-Transport-Security: max-age=31536000; includeSubdomains
< X-Frame-Options: DENY
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< X-Download-Options: noopen
< X-Permitted-Cross-Domain-Policies: none
< Content-Security-Policy: object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;
< Content-Type: text/html;charset=utf-8
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
Hello World!%
interceptorsとhandlersを使う
pedestal-playプロジェクトでは、service.cljというソースファイルに、サービスで使用するabout-pageとhome-pageの2つのインターセプターが定義されている。これらはサービスに使用される。さらに、HTTP固有のインターセプタであるbody-params と html-bodyが使用されていて、これはpedestal-serviceモジュールによってすぐに用意されるようになっている。pedestal-play.service名前空間のスニペットは、インターセプターの宣言と一緒にここに示されている。
(ns pedestal-play.service
(:require [io.pedestal.http :as http]
[io.pedestal.http.route :as route]
[io.pedestal.http.body-params :as body-params]
[ring.util.response :as ring-resp]))
(defn about-page
[request]
(ring-resp/response (format "Clojure %s - served from %s"
(clojure-version)
(route/url-for ::about-page))))
(defn home-page
[request]
(ring-resp/response "Hello World!"))
Context Mapを入力として受け取り、Context Mapを返すのではなく、about-pageとhome-pageインターセプターはリクエストマップを引数に取り、レスポンスマップを返す。このようなインターセプターはハンドラと呼ばれ、Pedestalフレームワークでは関数として扱われる。Pedestalフレームワークにおけるハンドラは、コンテキストマップにアクセスできない。したがって、チェーン内のインターセプタの順序を変更することができない。
home-page と about-page ハンドラで使われる response 関数は、Ring フレームワークが提供するユーティリティ関数で、ステータス 200、ヘッダーなし、ボディコンテンツありの Ring レスポンスマップを作成するために使用される。body-params 関数は、リクエストボディを MIME タイプ に基づいて解析し、対応するボディパラメータを持つ関連キーをリクエストマップに追加するインターセプターを返す。例えば、この関数はapplication/x-www-form-urlencodedというコンテンツタイプのリクエストに対して、すべてのフォームパラメータを含む :form-params キーを追加する。
html- body var で指定されたインターセプターは、レスポンスにtext/html;charset=UTF-8 というタイプのContent-Type ヘッダーパラメーターを追加する。このようにhtml-body はレスポンスに対して動作するので、このインターセプタの関数は :leave キーで定義される。一方 body-params の関数は、インターセプタマップの :enter キーで定義される。
PedestalのすべてのインターセプターはIntoInterceptorプロトコルを実装している。Pedestal は IntoInterceptor プロトコルを Map, Function, List, Symbol, Var に拡張し、これらの拡張形式で Interceptor を定義できるようにしている。インターセプターを定義する最も一般的な方法は、Clojure Map で、 :enter, :leave, :error のキーを持つものとなる。また、ハンドラとして定義することもできる。
routesを作る
Pedestalのインターセプターは、クライアントがアプリケーションと対話するためのエンドポイントを定義するルートにアタッチされる。Pedestalはroutesを定義する3つの著名な方法、verbose、table、terseを提供する。3つともClojureデータ構造として定義され、verboseはMap、tableはセット、terseはベクトルとして定義されています。この3つの形式は便宜上定義されている。Pedestalは、便利な関数expand-routesを使用して処理する前に、すべてのルート定義を内部的に冗長形式に変換している。冗長構文は、ルートを定義するキーワードを持つMapのリストとなる。キーワードの一覧とその説明は、次の表のようになる。

さらに、verbose構文の:pathパラメータから派生するキーとして、以下のようなものがある。

pedestal.route Pedestalモジュールには、RouteとRouterの両方の実装が含まれている。Routerは特別なインターセプターで、Routesを冗長な形式で入力として受け取り、実装に基づいてRoutesに入力されるリクエストを処理する。Routerはリクエストがインターセプターチェーンに従ってインターセプターを経由して処理されることを確認する。インターセプターによるインターセプターチェーンの変更は、Routerによって効率的に処理される。pedestal-playプロジェクトでは、service.cljのソースファイルにGET /とGET /aboutという二つのRouteが定義されており、以下のコードのように、それぞれ末尾にhome-pageとabout-pageハンドラを持つインターセプターチェーンで束ねられている。
(def routes #{["/" :get (conj common-interceptors `home-page)]
["/about" :get (conj common-interceptors `about-page)]})
app-name、:host、:port、および :scheme の各キーは、次のコードに示すように、ルー ト一覧に指定されたすべての経路に適用されるマップとして指定することもできる。
(def routes #{{:app-name "Pedestal Play" :host "localhost" :port 8080
:scheme :http}
["/" :get (conj common-interceptors `home-page)]
["/about" :get (conj common-interceptors `about-page)]})
routersを宣言する
routersは、定義されたRoutesに基づいてリクエストを分析するために、インターセプターとしてチェーンに追加される関数となる。Pedestalは、ルータを作成する際に、ルータをサービスマップで指定された :io.pedestal.http/routesと :io.pedestal.http/routerのキーの値に基づいて作成する。サービスマップには、ルーター、ルート、チェーンプロバイダのプロパティなど、Pedestalがサービスを作成するためのすべての詳細が含まれている。これは、Pedestalサービスのビルダーとして機能する。
Pedestalには3つの組み込みroutersがあり、:map-tree, :prefix-tree, :prefix-treeを使用して指定できる。 :map-treeはデフォルトのルーターで、静的なすべてのルートに適用された場合、一定の時間複雑性を持つ。パスパラメーターやワイルドカードを持つルートがある場合は、prefix-tree にフォールバックする。
io.pedestal.http/router の値を関数として指定すると、その関数を使用してルータを構築する。この関数は、1つの引数、すなわち冗長形式のルートコレクションを取り、ルータプロトコルを満たすルータを返さなければならない。
リクエストパラメーターにアクセスする
Servlet Chain Providerは、リクエストマップをコンテキストマップにアタッチする。リクエストマップには、APIのクライアントが指定したすべてのフォーム、クエリ、URL パラメータが含まれる。これらのパラメータは、キーと値のペアのマップとして定義され、各キーはクライアントによって指定されたパラメータを表す。すべてのパラメータはオプションであり、クライアントがリクエストで指定した場合のみ存在する。以下は、リクエスト・マップに含まれる可能性のあるキーのリストとなる。

パラメータとは別に、リクエストマップには常に存在するこれらのキーもあり、チェーン内のインターセプターが使用することができる。


リクエスト マップを確認するには、次のコードに示すように、新しい debug-page ハンドラを pedestal-play プロジェクトに追加して、ルート /debug にマッピングする。この ハンドラ debug-page は、関心のあるパラメータ キーのみを含むリクエスト マップをレスポンスで返す。また、このコードはCheshireライブラリを使用して、JSON 文字列に変換している。
(defn debug-page
[request]
(ring-resp/response
(cheshire.core/generate-string
(select-keys request
[:params :path-params :query-params :form-params]))))
;; Common Interceptors used for all routes
(def common-interceptors [(body-params/body-params) http/html-body])
(def routes #{{:app-name "Pedestal Play" :host "localhost" :port 8080
:scheme :http}
["/" :get (conj common-interceptors `home-page)]
["/about" :get (conj common-interceptors `about-page)]
["/debug/:id" :post (conj common-interceptors `debug-page)]})
次の例に示すように、/debug ルートへの cURL リクエストは、path、query、formのパラメータを検査することができるリクエストマップ全体を提供する。
>curl -XPOST -d "formparam=1" "http://localhost:8080/debug/1?qparam=1"
{
"params": {
"qparam": "1",
"formparam": "1"
},
"path-params": {
"id": "1" },
"query-params": {
"qparam": "1"
},
"form-params": {
"formparam": "1"
}
}
interceptorsを作る
Pedestalのインターセプターは :name, :enter, :leave, :error をキーとするマップとして定義することができる。例えば、msg-play というインターセプターを定義して、 pedestal-play プロジェクトの /hello ルートに対して、クエリパラメータ名を大文字し、:leave 関数を用いて終了時に挨拶を追加するように変更することができる。
;; Handler for /hello route
(defn hello-page
[request]
(ring-resp/response
(let [resp (clojure.string/trim (get-in request [:query-params :name]))]
(if (empty? resp) "Hello World!" (str "Hello " resp "!")))))
(def msg-play
{:name ::msg-play
:enter
(fn [context]
(update-in context [:request :query-params :name]
clojure.string/upper-case))
:leave
(fn [context] (update-in context [:response :body]
#(str % "Good to see you!")))})
;; Common Interceptors used for all routes
(def common-interceptors [(body-params/body-params) http/html-body])
(def routes #{{:app-name "Pedestal Play" :host "localhost" :port 8080
:scheme :http}
["/" :get (conj common-interceptors `home-page)]
["/about" :get (conj common-interceptors `about-page)]
["/debug/:id" :post (conj common-interceptors `debug-page)]
["/hello" :get
(conj common-interceptors `msg-play `hello-page)]})
cURL から /hello ルートへ name クエリパラメータでリクエストすると、 msg- の :enter と :leave イベントが発生し、期待通りの結果が得られる。
>curl "http://localhost:8080/hello?name=clojure"
Hello CLOJURE!Good to see you!
クエリパラメータが指定されていない場合は、hello-page インターセプタの実装に従った Hello World グリーティングを返すものとなる。
>curl "http://localhost:8080/hello?name="
Hello World!Good to see you!
次回は次世帯DB Datomicを利用したマイクロサービスの為のデータベース構築について述べる。

Chinoba
Intelligence as Relationship
Research Platform
founded by
Masao Watanabe
AI Systems Architecture
Decision Trace
Human–AI Coordination
Algorithmic Governance
Related Research
この記事は Chinoba Knowledge Base の一部です。

コメント
[…] Pedestalを利用したClojureによるマイクロサービスの為のREST APIの実装 […]
[…] Pedestalを利用したClojureによるマイクロサービスの為のREST APIの実装 […]