ClojureのProtojureでgRPC API開発してみた

Clojurianの[lagénorhynque](https://twitter.com/lagenorhynque) (a.k.a. カマイルカ🐬)です。

最近の私は仕事ではちょうど[ClojureのLaciniaでGraphQL API開発してみた](https://qiita.com/lagenorhynque/items/eebb9a36859789520dbf)の記事の例のような技術スタック(Lacinia + Pedestal + Duct)でGraphQL APIを開発しています。

マイクロサービスアーキテクチャのバックエンドサービス間の連携方式といえば[gRPC](https://grpc.io/)が有力ですが、個人的にまだClojureに限らず本格的に試したことがありませんでした。

そこで今回は、Clojure向けにProtocol BuffersとgRPCをサポートするライブラリ[Protojure](https://github.com/protojure/lib)を利用してgRPCベースのAPIを試作してみることにします(最終的に[gRPC公式チュートリアル(Python版)](https://grpc.io/docs/languages/python/basics/)のコードを移植します)。

ちなみに、Protojureにはサーバ側だけでなくクライアント側の機能もあり(クライアント用のClojureコードも生成できる)、もちろん任意の言語でクライアント側を実装することができますが、本記事ではサーバ(API)機能のみを扱い、動作確認にはGUIクライアント[Insomnia](https://insomnia.rest/)を利用することにしました。

最終的なサンプルAPIはこちら: [lagenorhynque/route-guide](https://github.com/lagenorhynque/route-guide)

# 事前準備

まずはClojureの基本的な開発環境(ビルドツール[Leiningen](https://leiningen.org/))に加えてProtocol BuffersとgRPCを扱うために必要なツールを用意します。

## 1. protoc

[Protocol Buffers](https://github.com/protocolbuffers/protobuf) (protobuf)の `.proto` ファイルをコンパイルするための `protoc` (protocol compiler)をインストールします。

macOSの場合はHomebrewからインストールするのが簡単です。

```sh
$ brew install protobuf
# 動作確認
$ protoc --version
libprotoc 25.2
```

## 2. protocのClojureプラグイン

`protoc` コマンドに対して各種言語用のプラグインが存在しますが、Clojureの場合にはProtojureの[protojure/protoc-plugin](https://github.com/protojure/protoc-plugin)が利用可能です。

[Installation](https://protojure.readthedocs.io/en/latest/install/#installation)の手順に従って最新版をインストールしましょう。

```sh
$ sudo curl -L https://github.com/protojure/protoc-plugin/releases/latest/download/protoc-gen-clojure --output /usr/local/bin/protoc-gen-clojure
$ sudo chmod +x /usr/local/bin/protoc-gen-clojure
# 動作確認
$ protoc-gen-clojure --version
protoc-gen-clojure version: v2.1.2
```

# ProtojureのLeiningenテンプレートを試してみる

開発の準備ができたら、まずはProtojureが提供しているLeiningenテンプレート[protojure/lein-template](https://github.com/protojure/lein-template)で全体の動作とコードのイメージを確認してみます。

- サンプルコードリポジトリ: [lagenorhynque/hello-grpc](https://github.com/lagenorhynque/hello-grpc)

## 1. Protojureプロジェクトの生成

```sh
$ lein new protojure hello-grpc
Generating fresh 'lein new' protojure project.
$ tree hello-grpc/
hello-grpc/
├── Makefile
├── README.md
├── project.clj
├── resources
│   └── addressbook.proto
└── src
    └── hello_grpc
        ├── server.clj
        └── service.clj

3 directories, 6 files
```

初期状態ではこのような構成でプロジェクトが生成されます。

## 2. Clojureコードの生成

[Quick Start](https://protojure.readthedocs.io/en/latest/quick-start/)の手順を参考に `make` (= `make all`)を実行してみると、

```sh
$ cd hello-grpc/
$ make
protoc --clojure_out=grpc-client,grpc-server:src --proto_path=resources resources/addressbook.proto
$ tree src/
src/
├── com
│   └── example
│       ├── addressbook
│       │   └── Greeter
│       │       ├── client.cljc
│       │       └── server.cljc
│       └── addressbook.cljc
└── hello_grpc
    ├── server.clj
    └── service.clj

6 directories, 5 files
```

`protoc --clojure_out=grpc-client,grpc-server:src --proto_path=resources resources/addressbook.proto` というコマンドにより[resources/addressbook.proto](https://github.com/lagenorhynque/hello-grpc/blob/master/resources/addressbook.proto)の定義に基づいて3種類の `.cljc` ファイルが生成されるようです。

- [src/com/example/addressbook/Greeter/client.cljc](https://github.com/lagenorhynque/hello-grpc/blob/master/src/com/example/addressbook/Greeter/client.cljc): gRPCクライアント
- [src/com/example/addressbook/Greeter/server.cljc](https://github.com/lagenorhynque/hello-grpc/blob/master/src/com/example/addressbook/Greeter/server.cljc): gRPCサーバ
- [src/com/example/addressbook.cljc](https://github.com/lagenorhynque/hello-grpc/blob/master/src/com/example/addressbook.cljc): Protocol Buffersに対応するClojureデータ(レコード)の定義とその相互変換のためのユーティリティ

## 3. 依存ライブラリの最新化

ここで、今回利用しているLeiningenテンプレートはProtojure関連ライブラリのバージョンが古くそのままでは動作しないことがあるようなので、[lein-ancient](https://github.com/xsc/lein-ancient)や[antq](https://github.com/liquidz/antq)などを活用して、依存ライブラリを適宜更新しておきましょう。

Clojure CLIでantqを `outdated` というエイリアスで導入している場合、以下のコマンドが便利です。

```sh
$ clojure -M:outdated --upgrade
```

## 4. APIサーバの動作確認

この状態で `lein run` でサーバを起動する、

```sh
$ lein run
Reflection warning, protojure/grpc/codec/lpm.clj:184:5 - reference to field close can't be resolved.

Creating your server...
14:45:57.132 [main] DEBUG org.jboss.logging -- Logging Provider: org.jboss.logging.Slf4jLoggerProvider
14:45:57.134 [main] INFO io.undertow -- starting server: Undertow - 2.3.8.Final
14:45:57.138 [main] INFO org.xnio -- XNIO version 3.8.8.Final
14:45:57.141 [main] INFO org.xnio.nio -- XNIO NIO Implementation Version 3.8.8.Final
14:45:57.148 [main] DEBUG org.xnio -- Creating worker:null, pool size:64, max pool size:64, keep alive:60000, io threads:8, stack size:0
14:45:57.153 [main] INFO org.jboss.threads -- JBoss Threads version 3.5.0.Final
14:45:57.156 [XNIO-1 I/O-1] DEBUG org.xnio.nio -- Started channel thread 'XNIO-1 I/O-1', selector sun.nio.ch.KQueueSelectorImpl@73b7819c
14:45:57.156 [XNIO-1 I/O-2] DEBUG org.xnio.nio -- Started channel thread 'XNIO-1 I/O-2', selector sun.nio.ch.KQueueSelectorImpl@150858c5
14:45:57.156 [XNIO-1 I/O-3] DEBUG org.xnio.nio -- Started channel thread 'XNIO-1 I/O-3', selector sun.nio.ch.KQueueSelectorImpl@568c22f5
14:45:57.156 [XNIO-1 I/O-4] DEBUG org.xnio.nio -- Started channel thread 'XNIO-1 I/O-4', selector sun.nio.ch.KQueueSelectorImpl@2efe0d32
14:45:57.156 [XNIO-1 I/O-5] DEBUG org.xnio.nio -- Started channel thread 'XNIO-1 I/O-5', selector sun.nio.ch.KQueueSelectorImpl@a6d1676
14:45:57.156 [XNIO-1 I/O-6] DEBUG org.xnio.nio -- Started channel thread 'XNIO-1 I/O-6', selector sun.nio.ch.KQueueSelectorImpl@5fa464f6
14:45:57.157 [XNIO-1 I/O-7] DEBUG org.xnio.nio -- Started channel thread 'XNIO-1 I/O-7', selector sun.nio.ch.KQueueSelectorImpl@2e002c01
14:45:57.157 [XNIO-1 I/O-8] DEBUG org.xnio.nio -- Started channel thread 'XNIO-1 I/O-8', selector sun.nio.ch.KQueueSelectorImpl@4376e4e7
14:45:57.157 [XNIO-1 Accept] DEBUG org.xnio.nio -- Started channel thread 'XNIO-1 Accept', selector sun.nio.ch.KQueueSelectorImpl@267b1df9
14:45:57.158 [main] DEBUG io.undertow -- Configuring listener with protocol HTTP for interface localhost and port 8080
```

もしくは `lein uberjar` でfat jarに固めて `java -jar` で実行すると、

```sh
$ lein uberjar
Compiling hello-grpc.server
Reflection warning, protojure/grpc/codec/lpm.clj:184:5 - reference to field close can't be resolved.
Created /Users/k.ohashi/repo/github.com/lagenorhynque/hello-grpc/target/hello-grpc-0.0.1-SNAPSHOT.jar
Created /Users/k.ohashi/repo/github.com/lagenorhynque/hello-grpc/target/hello-grpc-0.0.1-SNAPSHOT-standalone.jar
$ java -jar target/hello-grpc-0.0.1-SNAPSHOT-standalone.jar

Creating your server...
14:47:39.649 [main] DEBUG org.jboss.logging -- Logging Provider: org.jboss.logging.Slf4jLoggerProvider
14:47:39.651 [main] INFO io.undertow -- starting server: Undertow - 2.3.8.Final
14:47:39.657 [main] INFO org.xnio -- XNIO version 3.8.8.Final
14:47:39.661 [main] INFO org.xnio.nio -- XNIO NIO Implementation Version 3.8.8.Final
14:47:39.668 [main] DEBUG org.xnio -- Creating worker:null, pool size:64, max pool size:64, keep alive:60000, io threads:8, stack size:0
14:47:39.674 [main] INFO org.jboss.threads -- JBoss Threads version 3.5.0.Final
14:47:39.678 [XNIO-1 I/O-1] DEBUG org.xnio.nio -- Started channel thread 'XNIO-1 I/O-1', selector sun.nio.ch.KQueueSelectorImpl@6cd01003
14:47:39.678 [XNIO-1 I/O-5] DEBUG org.xnio.nio -- Started channel thread 'XNIO-1 I/O-5', selector sun.nio.ch.KQueueSelectorImpl@1c1d961a
14:47:39.678 [XNIO-1 I/O-2] DEBUG org.xnio.nio -- Started channel thread 'XNIO-1 I/O-2', selector sun.nio.ch.KQueueSelectorImpl@5986dab2
14:47:39.678 [XNIO-1 I/O-4] DEBUG org.xnio.nio -- Started channel thread 'XNIO-1 I/O-4', selector sun.nio.ch.KQueueSelectorImpl@2c8c1438
14:47:39.678 [XNIO-1 I/O-3] DEBUG org.xnio.nio -- Started channel thread 'XNIO-1 I/O-3', selector sun.nio.ch.KQueueSelectorImpl@6a861d8
14:47:39.678 [XNIO-1 I/O-6] DEBUG org.xnio.nio -- Started channel thread 'XNIO-1 I/O-6', selector sun.nio.ch.KQueueSelectorImpl@402f382a
14:47:39.678 [XNIO-1 I/O-7] DEBUG org.xnio.nio -- Started channel thread 'XNIO-1 I/O-7', selector sun.nio.ch.KQueueSelectorImpl@7fbee37a
14:47:39.678 [XNIO-1 I/O-8] DEBUG org.xnio.nio -- Started channel thread 'XNIO-1 I/O-8', selector sun.nio.ch.KQueueSelectorImpl@436460ad
14:47:39.678 [XNIO-1 Accept] DEBUG org.xnio.nio -- Started channel thread 'XNIO-1 Accept', selector sun.nio.ch.KQueueSelectorImpl@4a32c322
14:47:39.680 [main] DEBUG io.undertow -- Configuring listener with protocol HTTP for interface localhost and port 8080
```

GUIクライアントInsomniaで `resources/addressbook.proto` を指定して `localhost:8080` に対して[gRPCリクエスト](https://docs.insomnia.rest/insomnia/grpc)を実行すると、例えば以下のようにアクセスすることができました。

![Insomniaでの動作確認例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33430/d13f3876-2595-b2d3-a026-904d06fbc6d0.png)

## 4. API実装の検討

それでは、ここで動作しているAPIサーバの実装を簡単に調べてみましょう。

`service.clj` でサービスマップを定義し、 `server.clj` でサーバを起動する、というのは[Pedestal](https://github.com/pedestal/pedestal)のサンプルコードを見たことがあればお馴染みの構成です。

cf. [Clojureサービス開発ライブラリPedestal入門](https://qiita.com/lagenorhynque/items/fbd66ebaa0352ec4253d)

- [src/hello_grpc/service.clj](https://github.com/lagenorhynque/hello-grpc/blob/master/src/hello_grpc/service.clj)

```clj:src/hello_grpc/service.clj
(ns hello-grpc.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]

            ;; -- PROTOC-GEN-CLOJURE --
            [protojure.pedestal.core :as protojure.pedestal]
            [protojure.pedestal.routes :as proutes]
            [com.example.addressbook.Greeter.server :as greeter]
            [com.example.addressbook :as addressbook]))

(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 from hello-grpc, backed by Protojure Template!"))

;; -- PROTOC-GEN-CLOJURE --
;; Implement our "Greeter" service interface.  The compiler generates
;; a defprotocol (greeter/Service, in this case), and it is our job
;; to define an implementation of every function within it.  These will be
;; invoked whenever a request arrives, similarly to if we had defined
;; these functions as pedestal defhandlers.  The main difference is that
;; the :body returned in the response should correlate to the protobuf
;; return-type declared in the Service definition within the .proto
;;
;; Note that our GRPC parameters are associated with the request-map
;; as :grpc-params, similar to how the pedestal body-param module
;; injects other types, like :json-params, :edn-params, etc.
;;
;; see http://pedestal.io/reference/request-map


(deftype Greeter []
  greeter/Service
  (Hello
    [this {{:keys [name]} :grpc-params :as request}]
    {:status 200
     :body {:message (str "Hello, " name)}}))

;; Defines "/" and "/about" routes with their associated :get handlers.
;; The interceptors defined after the verb map (e.g., {:get home-page}
;; apply to / and its children (/about).
(def common-interceptors [(body-params/body-params) http/html-body])

;; Tabular routes
(def routes #{["/" :get (conj common-interceptors `home-page)]
              ["/about" :get (conj common-interceptors `about-page)]})

;; -- PROTOC-GEN-CLOJURE --
;; Add the routes produced by Greeter->routes
(def grpc-routes (reduce conj routes (proutes/->tablesyntax {:rpc-metadata greeter/rpc-metadata :interceptors common-interceptors :callback-context (Greeter.)})))

(def service {:env :prod
              ::http/routes grpc-routes

              ;; -- PROTOC-GEN-CLOJURE --
              ;; We override the chain-provider with one provided by protojure.protobuf
              ;; and based on the Undertow webserver.  This provides the proper support
              ;; for HTTP/2 trailers, which GRPCs rely on.  A future version of pedestal
              ;; may provide this support, in which case we can go back to using
              ;; chain-providers from pedestal.
              ::http/type protojure.pedestal/config
              ::http/chain-provider protojure.pedestal/provider

              ;;::http/host "localhost"
              ::http/port 8080})
```

注目すべきは `deftype Greeter` によるgRPCサービスの実装と `def grpc-routes` によるgRPC API用のルーティング定義でしょう。

`greeter/Service` は `.proto` ファイルから生成されたClojureのプロトコルで、gRPCのサービス定義に対応するメソッドを持っています。

`deftype` でそのプロトコルを実装した型を定義しておき、そのインスタンス `(Greeter.)` とメタデータ `greeter/rpc-metadata` 、共通のインターセプタ `common-interceptors` から `proutes/->tablesyntax` 関数でPedestalのルーティングデータに変換しているようです。

- [src/hello_grpc/server.clj](https://github.com/lagenorhynque/hello-grpc/blob/master/src/hello_grpc/server.clj)

```clj:src/hello_grpc/server.clj
(ns hello-grpc.server
  (:gen-class) ; for -main method in uberjar
  (:require [io.pedestal.http :as server]
            [io.pedestal.http.route :as route]
            [hello-grpc.service :as service]))

;; This is an adapted service map, that can be started and stopped
;; From the REPL you can call server/start and server/stop on this service
(defonce runnable-service (server/create-server service/service))

(defn run-dev
  "The entry-point for 'lein run-dev'"
  [& args]
  (println "\nCreating your [DEV] server...")
  (-> service/service ;; start with production configuration
      (merge {:env :dev
              ;; do not block thread that starts web server
              ::server/join? false
              ;; Routes can be a function that resolve routes,
              ;;  we can use this to set the routes to be reloadable
              ::server/routes #(route/expand-routes (deref #'service/grpc-routes)) ;; -- PROTOC-GEN-CLOJURE -- update route
              ;; all origins are allowed in dev mode
              ::server/allowed-origins {:creds true :allowed-origins (constantly true)}
              ;; Content Security Policy (CSP) is mostly turned off in dev mode
              ::server/secure-headers {:content-security-policy-settings {:object-src "none"}}})
      ;; Wire up interceptor chains
      server/default-interceptors
      server/dev-interceptors
      server/create-server
      server/start))

(defn -main
  "The entry-point for 'lein run'"
  [& args]
  (println "\nCreating your server...")
  (server/start runnable-service))

;; If you package the service up as a WAR,
;; some form of the following function sections is required (for io.pedestal.servlet.ClojureVarServlet).

;;(defonce servlet  (atom nil))
;;
;;(defn servlet-init
;;  [_ config]
;;  ;; Initialize your app here.
;;  (reset! servlet  (server/servlet-init service/service nil)))
;;
;;(defn servlet-service
;;  [_ request response]
;;  (server/servlet-service @servlet request response))
;;
;;(defn servlet-destroy
;;  [_]
;;  (server/servlet-destroy @servlet)
;;  (reset! servlet nil))
```

こちらには特別な実装はなく、Pedestalで開発/本番環境向けにAPIサーバを起動するための素直なコードのようです。

`service.clj` で定義したサービスマップ `service/service` とルーティングデータ `service/grpc-routes` を利用してAPIサーバを起動するコードが書かれています。

# Ductに載せてみる

ProtojureでgRPC APIを開発するための基本的な仕組みが把握できたので、アプリケーション構成を整えつつREPL駆動開発を快適にするためにいつも通り[Duct](https://github.com/duct-framework/duct)に載せてみましょう。

cf. [ClojureサーバサイドフレームワークDuctガイド](https://qiita.com/lagenorhynque/items/8201c116c87b40eb9c22)

- サンプルコードリポジトリ: [lagenorhynque/route-guide](https://github.com/lagenorhynque/route-guide)

## 1. Ductプロジェクトの生成

まずはDuctのLeiningenテンプレートでプロジェクトを生成します。

```sh
$ lein new duct route-guide
```

ここでは特にオプションは指定せず、ミニマルなDuctプロジェクトとしました。

## 2. 依存ライブラリの追加/更新

ProtojureのLeiningenテンプレートで生成された[project.clj](https://github.com/lagenorhynque/hello-grpc/blob/master/project.clj)を参考に必要な依存ライブラリを追加し、既存のライブラリも適宜更新します。

- [project.clj](https://github.com/lagenorhynque/route-guide/blob/master/project.clj)

```clj:project.clj
(defproject route-guide "0.1.0"
  :description "Route Guide, an example gRPC API"
  :url "https://github.com/lagenorhynque/route-guide"
  :min-lein-version "2.8.1"
  :dependencies [[duct.module.cambium "1.3.1" :exclusions [cheshire
                                                           org.clojure/tools.logging]]
                 [duct.module.pedestal "2.2.0"]
                 [duct/core "0.8.1"]
                 [integrant "0.8.0"]
                 [io.github.protojure/google.protobuf "2.0.1" :exclusions [io.github.protojure/core]]
                 [io.github.protojure/grpc-server "2.8.1" :exclusions [io.pedestal/pedestal.log]]
                 [org.clojure/clojure "1.11.1"]
                 [org.slf4j/slf4j-api "2.0.11"]]
  :plugins [[duct/lein-duct "0.12.3"]]
  :main ^:skip-aot route-guide.main
  :resource-paths ["resources" "target/resources"]
  :prep-tasks     ["javac" "compile" ["run" ":duct/compiler"]]
  :middleware     [lein-duct.plugin/middleware]
  :profiles
  {:repl {:prep-tasks   ^:replace ["javac" "compile"]
          :repl-options {:init-ns user}}
   :dev  [:shared :project/dev :profiles/dev]
   :test [:shared :project/dev :project/test :profiles/test]
   :uberjar [:shared :project/uberjar]

   :shared {}
   :project/dev  {:source-paths   ["dev/src"]
                  :resource-paths ["dev/resources"]
                  :dependencies   [[clj-http "3.12.3" :exclusions [commons-io]]
                                   [com.bhauman/rebel-readline "0.1.4"]
                                   [eftest "0.6.0" :exclusions [org.clojure/tools.logging
                                                                org.clojure/tools.namespace]]
                                   [fipp "0.6.26"]
                                   [hawk "0.2.11"]
                                   [integrant/repl "0.3.3" :exclusions [integrant]]
                                   [orchestra "2021.01.01-1"]
                                   [pjstadig/humane-test-output "0.11.0"]]
                  :plugins [[jonase/eastwood "1.4.2"]
                            [lein-ancient "0.7.0"]
                            [lein-cloverage "1.2.4"]
                            [lein-codox "0.10.8"]]
                  :aliases {"rebel" ^{:doc "Run REPL with rebel-readline."}
                            ["trampoline" "run" "-m" "rebel-readline.main"]
                            "test-coverage" ^{:doc "Execute cloverage."}
                            ["cloverage" "--ns-exclude-regex" "^(:?dev|user)$" "--codecov" "--junit"]
                            "lint" ^{:doc "Execute eastwood."}
                            ["eastwood" "{:config-files [\"dev/resources/eastwood_config.clj\"]
                                          :source-paths [\"src\"]
                                          :test-paths []}"]}
                  :injections [(require 'pjstadig.humane-test-output)
                               (pjstadig.humane-test-output/activate!)]
                  :codox {:output-path "target/codox"
                          :source-uri "https://github.com/lagenorhynque/route-guide/blob/master/{filepath}#L{line}"
                          :metadata {:doc/format :markdown}}}
   :project/test {}
   :project/uberjar {:aot :all
                     :uberjar-name "route-guide.jar"}
   :profiles/dev {}
   :profiles/test {}})
```

ProtojureでgRPC APIを開発するには次の2種類のライブラリが必要です。

- [io.github.protojure/grpc-server](https://github.com/protojure/lib): Protojureのサーバ実装用ライブラリ
- [io.github.protojure/google.protobuf](https://github.com/protojure/google.protobuf): Protocol Buffersの型のClojure実装

また、以下のDuctモジュールも導入しています。

- [duct.module.pedestal](https://github.com/lagenorhynque/duct.module.pedestal): Pedestalの組み込み
- [duct.module.cambium](https://github.com/lagenorhynque/duct.module.cambium): JSON形式でのロギングライブラリ[Cambium](https://github.com/cambium-clojure/cambium.core)の組み込み

## 3. Clojureコードの生成

Protojureのテンプレートの場合と同様に、 `.proto` ファイルからClojureコードを生成します。

ただし、今回はgRPCサーバ用のコードのみ生成したい(gRPCクライアント用のコードは使わない)ので、以下のようなコマンドをMakefileに追加して `make gen-clj` で生成できるようにしてみました。

- [Makefile](https://github.com/lagenorhynque/route-guide/blob/master/Makefile) (※ 抜粋)

```make:Makefile
.PHONY: gen-clj
gen-clj:
	protoc --clojure_out=grpc-server:src --proto_path=resources resources/route_guide/*.proto
```

さらに、ClojurianとしてはREPLからも手軽に実行できたほうが楽なので、 `dev` 名前空間に `gen-clj` という関数を定義して

- [dev/src/dev.clj](https://github.com/lagenorhynque/route-guide/blob/master/dev/src/dev.clj) (※ 抜粋)

```clj:dev/src/dev.clj
(defn gen-clj []
  (let [{:keys [exit out err]} (shell/sh "make" "gen-clj")]
    (print out)
    (when-not (zero? exit)
      (print err))))
```

`(gen-clj)` でコード生成できるようにしました。

```clj
dev> (gen-clj)
protoc --clojure_out=grpc-server:src --proto_path=resources resources/route_guide/*.proto
nil
```

さらに進んでDuct (Integrant)システムのライフサイクルの中でコード生成/更新を行うこともできそうですが、ここでは単体で実行できるようにするのにとどめました。

システム起動/再起動のたびに毎回コード生成する必要はないので、 `.proto` ファイルに更新があった時だけClojureコードを自動で再生成するような工夫ができると便利かもしれません(Gitのコミット時にフックすれば十分かも?)。

## 4. gRPCサービスの実装

Ductのコンポーネント設定を設定マップに書き、対応する初期化処理 `integrant.core/init-key` の実装を与えることでAPIサーバとしてProtojureのテンプレートと同様に動作するようにします。

### 4.1. Ductの設定マップ

- [resources/route_guide/config.edn](https://github.com/lagenorhynque/route-guide/blob/c113e9360b544966259c8b7d342dd46fcf1efdf3/resources/route_guide/config.edn)

```clj:resources/route_guide/config.edn
{:duct.profile/base
 {:duct.core/project-ns route-guide

  :duct.server/pedestal
  {:base-service #ig/ref :route-guide.grpc/service
   :service #:io.pedestal.http{:routes #ig/ref :route-guide.grpc/routes
                               :join? true
                               :host #duct/env "SERVER_HOST"
                               :port #duct/env ["SERVER_PORT" Int :or 8080]}}

  :route-guide.grpc/routes {}

  :route-guide.grpc/service
  {:options {:env :prod}}}

 :duct.profile/dev   #duct/include "dev"
 :duct.profile/test  #duct/include "test"
 :duct.profile/local #duct/include "local"
 :duct.profile/prod  {}

 :duct.module/cambium {}

 :duct.module/pedestal {}}
```

Ductの `config.edn` の構成はコンポーネントの設計方針次第で変わりますが、今回はDuctモジュール[duct.module.pedestal](https://github.com/lagenorhynque/duct.module.pedestal)を利用しているので、Pedestalのサービスマップの静的な設定は `:duct.server/pedestal` に与え、Protojureのルーティング定義とサービス設定をそれぞれ `:route-guide.grpc/routes`, `:route-guide.grpc/service` というアプリケーション独自のコンポーネントとして注入できるようにしてみました。

- [dev/resources/dev.edn](https://github.com/lagenorhynque/route-guide/blob/master/dev/resources/dev.edn)

```clj:dev/resources/dev.edn
{:duct.server/pedestal
 {:service #:io.pedestal.http{:join? false}}

 :route-guide.grpc/service
 {:options {:env :dev}}}
```

- [dev/resources/test.edn](https://github.com/lagenorhynque/route-guide/blob/master/dev/resources/test.edn)

```clj:dev/resources/test.edn
{:duct.server/pedestal
 {:service #:io.pedestal.http{:port 8081}}}
```

`dev.edn`, `test.edn` にあるのは `config.edn` からの差分の設定だけです。

### 4.2. gRPCサービス

ここが本題のgRPCのサービス実装です。

関心の分離の観点からPedestalのルーティングやサービスマップなどとは分けておきたいので、 `Greeter` サービスのための独立した名前空間 `route-guide.service.greeter` を用意しました。

- [src/route_guide/service/greeter.clj](https://github.com/lagenorhynque/route-guide/blob/c113e9360b544966259c8b7d342dd46fcf1efdf3/src/route_guide/service/greeter.clj)

```clj:src/route_guide/service/greeter.clj
(ns route-guide.service.greeter
  (:require
   [com.example.addressbook.Greeter.server :as greeter]
   [ring.util.response :refer [response]]))

(def service
  {:rpc-metadata greeter/rpc-metadata
   :callback-context
   (reify greeter/Service
     (Hello [_ {{:keys [name]} :grpc-params}]
       (response {:message (str "Hello, " name)})))})
```

Protojureのテンプレートではプロトコル `greeter/Service` に対する実装型を `deftype` で定義してインスタンス化したものを利用していましたが、繰り返し利用するものでもないので `reify` で実装してメタデータとともに `service` と名付けたマップに収めてみました。

### 4.3. コンポーネントの初期化処理

最後に、Pedestalのルーティング定義と設定マップの動的な部分のためのコンポーネント `:route-guide.grpc/routes`, `:route-guide.grpc/service` に対する初期化処理を実装します。

- [src/route_guide/grpc.clj](https://github.com/lagenorhynque/route-guide/blob/c113e9360b544966259c8b7d342dd46fcf1efdf3/src/route_guide/grpc.clj)

```clj:src/route_guide/grpc.clj
(ns route-guide.grpc
  (:require
   [integrant.core :as ig]
   [io.pedestal.http :as http]
   [io.pedestal.http.body-params :as body-params]
   [protojure.pedestal.core :as protojure.pedestal]
   [protojure.pedestal.routes :as proutes]
   [route-guide.service.greeter :as greeter]))

(defmethod ig/init-key ::routes
  [_ _]
  (let [common-interceptors [(body-params/body-params)]]
    (-> greeter/service
        (assoc :interceptors common-interceptors)
        proutes/->tablesyntax
        set)))

(defmethod ig/init-key ::service
  [_ {:keys [options]}]
  (assoc options
         ::http/type protojure.pedestal/config
         ::http/chain-provider protojure.pedestal/provider))
```

ここまでで、Protojureテンプレートと機能的に同等のgRPCサーバが実装できたはずです。

## 5. APIサーバの動作確認

REPLからAPIサーバを起動して、動作を確かめてみましょう。

```clj
user> (dev)
:loaded
dev> (reset)
:reloading (com.example.addressbook com.example.addressbook.Greeter.server route-guide.service.greeter route-guide.grpc dev route-guide.main user)
Reflection warning, protojure/grpc/codec/lpm.clj:184:5 - reference to field close can't be resolved.

Creating your [DEV] server...
:resumed
```

Protojureのテンプレートの場合と同様にGUIクライアントInsomniaからアクセスすることができます。

![Insomniaでの動作確認例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33430/d13f3876-2595-b2d3-a026-904d06fbc6d0.png)

もちろん `lein run` での起動や `lein uberjar` で生成したfat jarからの起動でも同様に動作します。

# gRPC公式チュートリアル(Python版)を移植してみる

Ductプロジェクトとして快適な開発環境が得られたので、実例を通してgRPCの基本的な機能を一通り試すべく、gRPC公式ドキュメントにあるチュートリアル(Python版)のコードをProtojureによるClojure実装として移植してみましょう。

- チュートリアル: [Basics tutorial | Python | gRPC](https://grpc.io/docs/languages/python/basics/)
- Python版サンプルコード: https://github.com/grpc/grpc/blob/v1.34.0/examples/python/route_guide/route_guide_server.py
- `.proto` ファイル: https://github.com/grpc/grpc/blob/v1.34.0/examples/protos/route_guide.proto

Protojureでの実装方法はProtojure公式ドキュメントやテストコードを参考にしました。

- Protojureのドキュメント: [Quick Start - Protojure](https://protojure.readthedocs.io/en/latest/quick-start/)
- Protojureのテストコード: https://github.com/protojure/lib/blob/master/test/protojure/grpc_test.clj
- Clojure版の最終形: [lagenorhynque/route-guide](https://github.com/lagenorhynque/route-guide)

## 1. Clojureコードの生成

まずは `.proto` ファイルをダウンロードして[resources/route_guide/route_guide.proto](https://github.com/lagenorhynque/route-guide/blob/master/resources/route_guide/route_guide.proto)に配置し、先ほど用意した `gen-clj` 関数を利用してClojureコードを生成します。

```clj
dev> (gen-clj)
protoc --clojure_out=grpc-server:src --proto_path=resources resources/route_guide/*.proto
nil
```

## 2. gRPCサービスの実装

Python版のサンプルコード[examples/python/route_guide/route_guide_server.py](https://github.com/grpc/grpc/blob/v1.34.0/examples/python/route_guide/route_guide_server.py)の実装をProtojureの仕組みを使ってClojureコードに書き換えていきます。

最終的には全体として以下のようなコードになりました。

- [src/route_guide/service/route_guide.clj](https://github.com/lagenorhynque/route-guide/blob/master/src/route_guide/service/route_guide.clj)

```clj:src/route_guide/service/route_guide.clj
(ns route-guide.service.route-guide
  (:require
   [clojure.core.async :as async]
   [io.grpc.examples.routeguide.RouteGuide.server :as route-guide]
   [ring.util.response :refer [response]]
   [route-guide.boundary.db.feature :as db.feature])
  (:import
   (java.time
    Duration
    Instant)))

(defn calculate-dinstance [start end]
  (let [coord-factor 10000000.0
        lat-1 (/ (:latitude start) coord-factor)
        lat-2 (/ (:latitude end) coord-factor)
        lon-1 (/ (:longitude start) coord-factor)
        lon-2 (/ (:longitude end) coord-factor)
        lat-rad-1 (Math/toRadians lat-1)
        lat-rad-2 (Math/toRadians lat-2)
        delta-lat-rad (Math/toRadians (- lat-2 lat-1))
        delta-lon-rad (Math/toRadians (- lon-2 lon-1))
        ;; Formula is based on http://mathforum.org/library/drmath/view/51879.html
        a (+ (Math/pow (Math/sin (/ delta-lat-rad 2)) 2)
             (* (Math/cos lat-rad-1)
                (Math/cos lat-rad-2)
                (Math/pow (Math/sin (/ delta-lon-rad 2)) 2)))
        c (* 2 (Math/atan2 (Math/sqrt a) (Math/sqrt (- 1 a))))
        r 6371000]
    (* r c)))

(defn summarize-points [db point-ch]
  (async/go-loop [point (async/<! point-ch)
                  prev-point nil
                  summary {:point-count 0
                           :feature-count 0
                           :distance 0.0}]
    (if point
      (recur (async/<! point-ch)
             point
             (cond-> summary
               true (update :point-count inc)
               (db.feature/find-feature-by-point db point) (update :feature-count inc)
               prev-point (update :distance + (calculate-dinstance prev-point point))))
      summary)))

(def service
  {:rpc-metadata (map #(assoc % :pkg "routeguide")
                      route-guide/rpc-metadata)
   :callback-context
   (reify route-guide/Service
     (GetFeature [_ {:keys [db]
                     point :grpc-params}]
       (if-let [feature (db.feature/find-feature-by-point db point)]
         (response feature)
         (response {:name ""
                    :location point})))
     (ListFeatures [_ {:keys [db]
                       rectangle :grpc-params
                       out-ch :grpc-out}]
       (db.feature/find-features-within-rectangle db out-ch rectangle)
       (response out-ch))
     (RecordRoute [_ {:keys [db]
                      point-ch :grpc-params}]
       (let [start-time (Instant/now)
             summary (async/<!! (summarize-points db point-ch))
             elapsed-time (Duration/between start-time (Instant/now))]
         (response (-> summary
                       (update :distance long)
                       (assoc :elapsed-time (.getSeconds elapsed-time))))))
     (RouteChat [_ {route-note-ch :grpc-params
                    out-ch :grpc-out}]
       (async/go-loop [new-note (async/<! route-note-ch)
                       prev-notes []]
         (if new-note
           (do (doseq [prev-note prev-notes
                       :when (= (:location prev-note)
                                (:location new-note))]
                 (async/>! out-ch prev-note))
               (recur (async/<! route-note-ch)
                      (conj prev-notes new-note)))
           (async/close! out-ch)))
       (response out-ch)))})
```

`route-guide/rpc-metadata` に対して `map` 関数で各要素の `:pkg` の値を `"routeguide"` に置き換えています。

これは、今回扱っているチュートリアルの `.proto` ファイルの定義では `package routeguide;` と `option java_package = "io.grpc.examples.routeguide";` の設定があり、Protojureのデフォルト挙動では後者に基づいて自動生成Clojureコードの名前空間が定義されるだけではなくAPIエンドポイントのパスも決まってしまうためで、ここでは前者のパッケージ名に基づいたパスにしたいのでこのような変換を入れています(例えば `GetFeature` メソッドに対して `/io.grpc.examples.routeguide.RouteGuide/GetFeature` ではなく `/routeguide.RouteGuide/GetFeature` というエンドポイントになるようにしたい)。

サービスのプロトコル `route-guide/Service` の実装の詳細についてはこのあと個別に説明します(4種類のメソッドがちょうどgRPCの4種類のメソッドパターンに対応しています)。

また、本記事では詳細に立ち入りませんが、DB (今回の場合はデータソースがJSONファイル)のような外部リソースに対するアクセスは疎結合に実装したいので、Ductらしく"boundary"のプロトコルを定義し、実装した型のデータをPedestalのインターセプタ(interceptor)を介して注入するようにしてみました(詳細は[リポジトリ](https://github.com/lagenorhynque/route-guide)のコード参照)。

- [src/route_guide/boundary/db/core.clj](https://github.com/lagenorhynque/route-guide/blob/master/src/route_guide/boundary/db/core.clj)

```clj:src/route_guide/boundary/db/core.clj
(ns route-guide.boundary.db.core
  (:require
   [cheshire.core :as cheshire]
   [clojure.java.io :as io]
   [integrant.core :as ig]))

(defrecord Boundary [data])

(defmethod ig/init-key ::db
  [_ {:keys [path]}]
  (-> (io/resource path)
      io/reader
      (cheshire.core/parse-stream-strict true)
      ->Boundary))
```

- [src/route_guide/boundary/db/feature.clj](https://github.com/lagenorhynque/route-guide/blob/master/src/route_guide/boundary/db/feature.clj)

```clj:src/route_guide/boundary/db/feature.clj
(ns route-guide.boundary.db.feature
  (:require
   [clojure.core.async :as async]
   [route-guide.boundary.db.core]))

(defprotocol Feature
  (find-feature-by-point [db point])
  (find-features-within-rectangle [db out-ch rectangle]))

(extend-protocol Feature
  route_guide.boundary.db.core.Boundary
  (find-feature-by-point [{:keys [data]} {:keys [latitude longitude]}]
    (some (fn [{:keys [location]
                :as feature}]
            (when (and (= (:latitude location) latitude)
                       (= (:longitude location) longitude))
              feature))
          data))
  (find-features-within-rectangle [{:keys [data]} out-ch {:keys [lo hi]}]
    (let [left (min (:longitude lo) (:longitude hi))
          right (max (:longitude lo) (:longitude hi))
          top (max (:latitude lo) (:latitude hi))
          bottom (min (:latitude lo) (:latitude hi))]
      (async/go
        (doseq [{:keys [location]
                 :as feature} data
                :when (and (<= left (:longitude location) right)
                           (<= bottom (:latitude location) top))]
          (async/>! out-ch feature))
        (async/close! out-ch)))))
```

ここからは、サービス定義の4種類のメソッドに対する実装をそれぞれ見てみましょう。

### 2.1. `GetFeature` メソッドの実装: simple RPC

まずは単純にリクエストを受け取ってレスポンスを返すパターンです。

```proto
  // A simple RPC.
  //
  // Obtains the feature at a given position.
  //
  // A feature with an empty name is returned if there's no feature at the given
  // position.
  rpc GetFeature(Point) returns (Feature) {}
```

Python版では、メソッドの(レシーバを含めて)第2引数で入力データ `Point` を受け取り、戻り値として `Feature` を返しているようです。

```py
    def GetFeature(self, request, context):
        feature = get_feature(self.db, request)
        if feature is None:
            return route_guide_pb2.Feature(name="", location=request)
        else:
            return feature
```

Protojureでは、第2引数(リクエストマップ)の `:grpc-params` の値として入力データ `Point` を受け取り、 `Feature` マップをボディとするレスポンスマップを返すことで同等の振る舞いになります。

Clojureのプロトコルのメソッドは第1引数が `this` になる(ちょうどPythonのメソッドと同じ形式)ので引数が1個ズレていますが、Protojureにおけるサービス(プロトコル)のメソッドはリクエストマップ(request map)を受け取ってレスポンスマップ(response map)を返す[Ring](https://github.com/ring-clojure/ring)のハンドラ関数(handler function)と同じようなものであることが分かります。

Ringのハンドラ関数とまったく同じように、Pedestalのインターセプタであらかじめリクエストマップに追加のデータ(ここではDBデータ `:db`)を詰めておくと、メソッドの第2引数のマップから取り出すことができました。

```clj
     (GetFeature [_ {:keys [db]
                     point :grpc-params}]
       (if-let [feature (db.feature/find-feature-by-point db point)]
         (response feature)
         (response {:name ""
                    :location point})))
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33430/89817d6c-c777-55ab-8b2e-169447ff8220.png)

### 2.2. `ListFeatures` メソッドの実装: response-streaming RPC

次はレスポンスをストリーミングで返すパターンです。

```proto
  // A server-to-client streaming RPC.
  //
  // Obtains the Features available within the given Rectangle.  Results are
  // streamed rather than returned at once (e.g. in a response message with a
  // repeated field), as the rectangle may cover a large area and contain a
  // huge number of features.
  rpc ListFeatures(Rectangle) returns (stream Feature) {}
```

Python版では、ジェネレータ(for文内の `yield feature`)によってレスポンスとして `Feature` を繰り返し送ることができるようです。

```py
    def ListFeatures(self, request, context):
        left = min(request.lo.longitude, request.hi.longitude)
        right = max(request.lo.longitude, request.hi.longitude)
        top = max(request.lo.latitude, request.hi.latitude)
        bottom = min(request.lo.latitude, request.hi.latitude)
        for feature in self.db:
            if (feature.location.longitude >= left and
                    feature.location.longitude <= right and
                    feature.location.latitude >= bottom and
                    feature.location.latitude <= top):
                yield feature
```

Protojureでは、第2引数(リクエストマップ)の `:grpc-out` の値として[core.async](https://github.com/clojure/core.async)のチャネル(channel)を受け取れるので、返したい `Feature` の値を非同期的に流し込む(`find-features-within-rectangle` 関数の `(async/>! out-ch feature)`)ようにしておき、レスポンスマップのボディとしてそのチャネルを返せば同等のことができます。

```clj
     (ListFeatures [_ {:keys [db]
                       rectangle :grpc-params
                       out-ch :grpc-out}]
       (db.feature/find-features-within-rectangle db out-ch rectangle)
       (response out-ch))
```

```clj
  (find-features-within-rectangle [{:keys [data]} out-ch {:keys [lo hi]}]
    (let [left (min (:longitude lo) (:longitude hi))
          right (max (:longitude lo) (:longitude hi))
          top (max (:latitude lo) (:latitude hi))
          bottom (min (:latitude lo) (:latitude hi))]
      (async/go
        (doseq [{:keys [location]
                 :as feature} data
                :when (and (<= left (:longitude location) right)
                           (<= bottom (:latitude location) top))]
          (async/>! out-ch feature))
        (async/close! out-ch))))
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33430/322d1fc5-5898-86bc-71c5-5fa5af6ecc8e.png)

### 2.3. `RecordRoute` メソッドの実装: request-streaming RPC

今度は反対にリクエストをストリーミングで受け取るパターンです。

```proto
  // A client-to-server streaming RPC.
  //
  // Accepts a stream of Points on a route being traversed, returning a
  // RouteSummary when traversal is completed.
  rpc RecordRoute(stream Point) returns (RouteSummary) {}
```

Python版では、第2引数で入力データとしてイテレータを受け取ってそこから `Point` の値を順に取り出すことができるようです。

```py
    def RecordRoute(self, request_iterator, context):
        point_count = 0
        feature_count = 0
        distance = 0.0
        prev_point = None

        start_time = time.time()
        for point in request_iterator:
            point_count += 1
            if get_feature(self.db, point):
                feature_count += 1
            if prev_point:
                distance += get_distance(prev_point, point)
            prev_point = point

        elapsed_time = time.time() - start_time
        return route_guide_pb2.RouteSummary(point_count=point_count,
                                            feature_count=feature_count,
                                            distance=int(distance),
                                            elapsed_time=int(elapsed_time))
```

Protojureでは、第2引数(リクエストマップ)の `:grpc-params` の値としてcore.asyncのチャネルを受け取り、そこから `Point` の値を順に取り出す(`summarize-points` 関数の `(async/<! point-ch)`)ことができます。

```clj
(defn summarize-points [db point-ch]
  (async/go-loop [point (async/<! point-ch)
                  prev-point nil
                  summary {:point-count 0
                           :feature-count 0
                           :distance 0.0}]
    (if point
      (recur (async/<! point-ch)
             point
             (cond-> summary
               true (update :point-count inc)
               (db.feature/find-feature-by-point db point) (update :feature-count inc)
               prev-point (update :distance + (calculate-dinstance prev-point point))))
      summary)))

;; 途中省略

     (RecordRoute [_ {:keys [db]
                      point-ch :grpc-params}]
       (let [start-time (Instant/now)
             summary (async/<!! (summarize-points db point-ch))
             elapsed-time (Duration/between start-time (Instant/now))]
         (response (-> summary
                       (update :distance long)
                       (assoc :elapsed-time (.getSeconds elapsed-time))))))
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33430/00c4844d-3835-781b-5730-d298398c5022.png)

### 2.4. `RouteChat` メソッドの実装: bidirectional streaming RPC

最後はリクエストもレスポンスもストリーミングで扱うパターンです。

```proto
  // A Bidirectional streaming RPC.
  //
  // Accepts a stream of RouteNotes sent while a route is being traversed,
  // while receiving other RouteNotes (e.g. from other users).
  rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
```

これはリクエストをストリーミングで受け取るパターンとレスポンスをストリーミングで返すパターンの複合形だといえます。

Python版でもProtojureでも、両パターンの実装方法の単純な組み合わせで実現できます(Python版ではイテレータから取り出してジェネレータで返し、Protojureではcore.asyncのチャネルから取り出して別のチャネルに流し込む)。

```py
    def RouteChat(self, request_iterator, context):
        prev_notes = []
        for new_note in request_iterator:
            for prev_note in prev_notes:
                if prev_note.location == new_note.location:
                    yield prev_note
            prev_notes.append(new_note)
```

```clj
     (RouteChat [_ {route-note-ch :grpc-params
                    out-ch :grpc-out}]
       (async/go-loop [new-note (async/<! route-note-ch)
                       prev-notes []]
         (if new-note
           (do (doseq [prev-note prev-notes
                       :when (= (:location prev-note)
                                (:location new-note))]
                 (async/>! out-ch prev-note))
               (recur (async/<! route-note-ch)
                      (conj prev-notes new-note)))
           (async/close! out-ch)))
       (response out-ch))
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33430/4cbff8cd-1b13-b830-fa40-589e864ea6e8.png)

# まとめ

- Protojureを利用すると、Clojureとしてとても自然な形でgRPC APIを実装することができた
- Duct, Pedestal, Protojureという組み合わせでクリーンなアプリケーション構成と快適な開発環境を得ることができた
- ProtojureでgRPCのストリーミングはcore.asyncのチャネルを介して実現できると分かった
- ClojureでgRPCのAPIを開発するならProtojureはかなり良い選択肢になりそう(実用に向けてさらに詳細を探ってみたい)
- やはりClojureは超楽しい＞ω＜/

# Further Reading

[Clojure/ClojureScript関連リンク集 > Webサーバサイド (Clojure)](https://qiita.com/lagenorhynque/items/68c314c288b75a9492ba#web%E3%82%B5%E3%83%BC%E3%83%90%E3%82%B5%E3%82%A4%E3%83%89-clojure)