nREPL on CloudHub 2.0

CPUがあるとLinuxをポートしてみる人がいるように、Clojureを使う人であればJVMがあるとnREPLを動かしてみたくなりますよね。Clojure愛好家の自分としてもやはりJVMが動くところがあるとnREPLを動かしてみたくなるものです。以前の記事ではAndroid上で動かしてみましたが、今回はいつもお仕事でつかっているMuleSoftのAnypoint Platformのランタイムの1つであるCloudHub 2.0で動かしてみることにします。

# 形式
Mule applicationはJavaのライブラリを使ってStatic methodをコールできるのですが、nREPLはそもそもListenしないといけない代物なので、カスタムモジュールにてリスナーとして作成することにしました。標準のGrizzlyを使うというのもあるかと思いますが、簡単に動かしたかったのでClojure界隈で結構使われる[http-kit](https://github.com/http-kit/http-kit)を使用しています。



```java
public class NreplListener extends Source<InputStream, HttpRequestAttributes> {

    private final Logger LOGGER = LoggerFactory.getLogger(NreplConnectionProvider.class);

    @Config
    private NreplConfiguration config;
    
    @DisplayName("Transport")
    @Parameter
    public Transport  protocol = Transport.HTTP;

    public Transport getTransport() {
	return protocol;
    }

    public void onStart(SourceCallback<InputStream, HttpRequestAttributes> sourceCallBack) {
	int port = config.getPort();
	
	LOGGER.info("onStart");
	
	IFn require = Clojure.var("clojure.core", "require");
	require.invoke(Clojure.read("mule-nrepl-connector.core"));
	IFn onStart = Clojure.var("mule-nrepl-connector.core", "on-start");
	IFn keyword = Clojure.var("clojure.core", "keyword");
	HashMap opts = new HashMap<clojure.lang.Keyword, Object>();
	opts.put(keyword.invoke("port"), port);
	if (Transport.CIDER == protocol) {
	    opts.put(keyword.invoke("cider"), true);
	} else if (Transport.TTY == protocol) {
	    opts.put(keyword.invoke("tty"), true);
	    
	}
	opts.put(keyword.invoke("callback"), sourceCallBack);
	onStart.invoke(opts);
    }

    @OnSuccess
    public void onSuccess(@Content String responseBody,
                          @Optional String responseStatusCode,
                          SourceCallbackContext callbackContext) {
	IFn require = Clojure.var("clojure.core", "require");
	require.invoke(Clojure.read("mule-nrepl-connector.core"));
	IFn nrepl = Clojure.var("mule-nrepl-connector.core", "on-success");
	nrepl.invoke(responseBody, responseStatusCode, callbackContext);	
	
    }
    
    public void onStop() {
	LOGGER.info("onStop");
	IFn require = Clojure.var("clojure.core", "require");
	require.invoke(Clojure.read("mule-nrepl-connector.core"));
	IFn nrepl = Clojure.var("mule-nrepl-connector.core", "on-stop");
	nrepl.invoke();

    }

  @OnTerminate
  public void onTerminate() {}

}
```
実装自体は単純な感じでSourceクラスを継承したListenerクラスのonStart()でnREPLの起動を行っています。nREPL自体はもともとTCPで受けるようになっているのですが、HTTPを受けられるようにするためClojureのhttp-kitライブラリを使用したWeb applicationとして実装しています。Web applicationのパスルーティングにはreititを使用して/nreplというパスで命令を送信できるようにしています。
命令自体は
[https://nrepl.org/nrepl/design/transports.html](https://nrepl.org/nrepl/design/transports.html)
に記載されているEDN形式をJSONで送信しています。

```json
{"op":"eval", "code":"(+ 2 2)"}
```
ちなみに、Listner自体は他にEmacsのCIDERからの接続と、netcatを使用してTCPで接続できるようにそれぞれCIDER, TTYというのをオプションとして選択できるようにしています。
ただし、両オプションを使用した場合はインターネットからIngress Controllerを通って接続できないため以下のマニュアルに記載のとおりTCPアプリケーションとしてデプロイし、Transit GatewayもしくはVPNを通して接続することになります。
[https://docs.mulesoft.com/cloudhub-2/ch2-deploy-api](https://docs.mulesoft.com/cloudhub-2/ch2-deploy-api)

# ビルド 
以下よりダウンロードしてmavenにてビルドします。
```
$ git clone https://github.com/myst3m/mule-nrepl-connector
$ cd mule-nrepl-connector
$ mvn clean package  -DskipTests -Dmaven.javadoc.skip
```
targetディレクトリにJarファイルが作成されるので、Mule Applicationに取りこんで使用します。

# nREPL Mule app

上記Extensionを使用したnREPLのMule applicationを以下のようにして実装してみました。単純にリスナーを配置しただけですが。TCPアプリケーションとしてCloudHub 2.0へデプロイできるようnrepl::listnerタグのprotocolフィールドはconfig.yamlに記載の値を使用できるようにしました。また、ListenするTCPのポート番号も指定可能にしました。
(ただし、protocolのフィールドはExtensionのConfigurationが択一値にするようしているのでStudioで読み込むと強制的に書きかえられるので注意。)

[https://github.com/myst3m/nrepl](https://github.com/myst3m/nrepl)

```xml
<?xml version="1.0" encoding="UTF-8"?>

<mule xmlns:ee="http://www.mulesoft.org/schema/mule/ee/core" xmlns:apikit="http://www.mulesoft.org/schema/mule/mule-apikit"
	xmlns:nrepl="http://www.mulesoft.org/schema/mule/nrepl"
	xmlns:http="http://www.mulesoft.org/schema/mule/http" xmlns="http://www.mulesoft.org/schema/mule/core" xmlns:doc="http://www.mulesoft.org/schema/mule/documentation" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mulesoft.org/schema/mule/core http://www.mulesoft.org/schema/mule/core/current/mule.xsd
http://www.mulesoft.org/schema/mule/http http://www.mulesoft.org/schema/mule/http/current/mule-http.xsd
http://www.mulesoft.org/schema/mule/nrepl http://www.mulesoft.org/schema/mule/nrepl/current/mule-nrepl.xsd
http://www.mulesoft.org/schema/mule/mule-apikit http://www.mulesoft.org/schema/mule/mule-apikit/current/mule-apikit.xsd
http://www.mulesoft.org/schema/mule/ee/core http://www.mulesoft.org/schema/mule/ee/core/current/mule-ee.xsd">
	<nrepl:config name="Nrepl_Config" doc:name="Nrepl Config" doc:id="025e0d26-fda5-4c36-ae04-c067ad26ee20" configId="main" port="${transport.port}"/>
	<configuration-properties doc:name="Configuration properties" doc:id="438c3f7b-00d9-4c72-9fb1-eba62796191a" file="config.yaml" />
	<flow name="nreplFlow" doc:id="7108b130-0005-435d-9419-48d2844e78a4" >
		<nrepl:listener doc:name="Listener" doc:id="b4a1aa20-78c5-443c-9653-ef52585ed2cf" config-ref="Nrepl_Config" protocol="${transport.type}"/>
		<logger level="INFO" doc:name="Logger" doc:id="1feec81a-352f-4fca-8bab-a09d32ff3596" message="hello"/>
	</flow>
</mule>
```

ダウンロードしてExtensionと同じmvnコマンドを実行するとMule applicationが作成されます。

```sh
$ mvn clean package  -DskipTests -Dmaven.javadoc.skip
```
これをTCPアプリケーションとしてデプロイするにはMuleSoftのプラットフォームAPIを使用する必要があるのですが、ここでは自作のCLIを使うことにします。
(実際には今回はHTTPで使用するので管理コンソールからデプロイすることも可能ですが、接続クライアントとしてもこのCLIを使いたいため使用します)
[https://github.com/myst3m/yaac](https://github.com/myst3m/yaac)
GNU/Linuxを使用している場合はdistにx86-64用のバイナリがあるのでそれを使います。c-appというコマンドのコンテキストを~/.yaac/credentialにつくりAnypoint Platformで作成したConnected Appのclient idとclient secretをいれます。yaac config credentialコマンドを実行しても作成できます。ExchangeへのアップロードおよびデプロイできるようConnected Appには適切な権限をつけておきます。
```
{"c-app":
 {"grant_type":"client_credentials",
  "client_secret":"your secret",
  "client_id":"you id"}}
```
その後以下のようにloginします。
```
$ yaac login c-app
TOKEN-TYPE  ACCESS-TOKEN                          EXPIRES-IN
bearer      a1d6246d-fead-aaaa-aaaa-d39d2643e433  3600
```
あとは、Mule applicationとして実装してパッケージングしたJarをデプロイしてきます。

# CloudHub 2.0へのデプロイ
CloudHub 2.0へのデプロイを行うにはExchangeへ一度アップロードする必要があります。これもyaacコマンドで行ってみます。ここではビジネスグループ名をT1、アセット名をnrepl-appとしてアップロードしてみます.バージョンは0.2.0とします。

```
$ yaac upload asset nrepl-0.2.0-mule-application.jar -g T1 -a nrepl-app -v 0.2.0

ORGANIZATION-ID                       GROUP-ID                              GROUP-NAME  NAME       ASSET-ID   TYPE  VERSION
fe1db8fb-9999-4b5c-a591-06fea582f980  fe1db8fb-9999-4b5c-a591-06fea582f980  T1          nrepl-app  nrepl-app  app   0.2.0
```
次にデプロイします。以下ではT1というビシネスグループのProductionという環境にnrepl-appという名前でExchange上のアセットグループT1のアセット名nrepl-appというアプリをデプロイしています。
(yaacコマンドで、デフォルトで使うビジネスグループ、環境、ターゲットは設定できますので上記GithubのREADMEをみてみてください)
```
$ yaac deploy app T1 Production nrepl-app -g T1 -a nrepl-app target=t1ps v-cores=0.1
```
targetの部分にはデプロイするターゲット名もしくはターゲットIDを指定します。私はt1psというPrivate Spaceを指定しています。使用するvCoreも指定します。
```
$ yaac get runtime-target
NAME                     TYPE            ID                                    REGION          STATUS
leibniz                  SERVER          36185685                              -               RUNNING
Cloudhub-EU-West-2       shared-space    cloudhub-eu-west-2                    eu-west-2       Active
t1ps                     private-space   738ce306-aaaa-aaaa-b35b-e11bd5ad7d6c  ap-northeast-1  Active
Cloudhub-CA-Central-1    shared-space    cloudhub-ca-central-1                 ca-central-1    Active
Cloudhub-AP-Northeast-1  shared-space    cloudhub-ap-northeast-1               ap-northeast-1  Active
Cloudhub-US-East-2       shared-space    cloudhub-us-east-2                    us-east-2       Active
Cloudhub-AP-Southeast-1  shared-space    cloudhub-ap-southeast-1               ap-southeast-1  Active
Cloudhub-US-West-2       shared-space    cloudhub-us-west-2                    us-west-2       Active
Cloudhub-US-East-1       shared-space    cloudhub-us-east-1                    us-east-1       Active
Cloudhub-AP-Southeast-2  shared-space    cloudhub-ap-southeast-2               ap-southeast-2  Active
Cloudhub-EU-West-1       shared-space    cloudhub-eu-west-1                    eu-west-1       Active
Cloudhub-US-West-1       shared-space    cloudhub-us-west-1                    us-west-1       Active
Cloudhub-EU-Central-1    shared-space    cloudhub-eu-central-1                 eu-central-1    Active```
```
デプロイできたかどうか確認します。
```
$ yaac get app T1 Production
ORG  ENV         NAME         ID                                    STATUS       TARGET
T1   Production  nrepl-app    5728c106-0d1b-9876-b31c-79fab45e639c  NOT_RUNNING  t1ps
```
logもみてみます。
```
$ yaac logs app nrepl-app
TIMESTAMP                LOG-LEVEL  MESSAGE
2024-03-20T10:46:31.977  INFO       "Starting Bean: listener"
2024-03-20T10:46:32.067  INFO       "Start collecting CorePricingStats metrics with frequency 60000 for app: nrepl-app"
2024-03-20T10:46:32.068  INFO       "Un-registering listeners for application nrepl-app"
2024-03-20T10:46:32.068  INFO       "Registering listeners for application nrepl-app"
2024-03-20T10:46:32.172  INFO       "Fast header injection enabled for app: nrepl-app"
2024-03-20T10:46:32.185  INFO       "Anypoint monitoring custom file appender disabled."
2024-03-20T10:46:32.266  WARN       "creating custom anypoint file appender"
2024-03-20T10:46:32.266  INFO       "Disable console logging"
2024-03-20T10:46:32.475  INFO       "onStart"
2024-03-20T10:46:32.476  INFO       "Started ServerConnector@a4c0d17{HTTP/1.1, (http/1.1)}{0.0.0.0:7777}"
```
logsサブコマンドは-fをつけると数秒おきにログのポーリングをします。
describeコマンドで詳細をみてみます。
```
$ yaac desc app T1 Production nrepl-app
ID                                    NAME       STATUS   POD      V-CORES  REPLICAS  PUBLIC-URL                                          INTERNAL-URL
5728c106-0d1b-40c1-b31c-79fab45e639c  nrepl-app  APPLIED  RUNNING  0.1      1         https://nrepl-app-98wz6g.qyw2z2.jpn-e1.cloudhub.io  https://nrepl-app-49wz6g.internal-qyw2z2.jpn-e1.cloudhub.io
```
RUNNINGになっていたら完了です。

#  接続
yaacにHTTP Transport用のclient機能があるのでそれで接続してみます。
```
$ yaac nrepl https://nrepl-app-98wz6g.qyw2z2.jpn-e1.cloudhub.io/nrepl
user> (+ 2 2)
4
user> 
```
userというプロンプトがでてきたら成功です。終了するときは^Dを押します。yaac nreplを使用するときは一緒にrlwrapを使って、bashのような使用感にするとコマンドラインヒストリも使えて便利です。

尚、yaacに-Xをつけるとどういうデータを送信しているのかトレースができます。これは、他のget appや、desc app、deploy appなどコントロールプレーンと通信するコマンドはすべてに有効です。
```
$ yaac nrepl https://nrepl-app-98wz6g.qyw2z2.jpn-e1.cloudhub.io/nrepl -X
===> [1262] POST https://nrepl-app-98wz6g.qyw2z2.jpn-e1.cloudhub.io/nrepl
Content-Type: application/json
Host: nrepl-app-98wz6g.qyw2z2.jpn-e1.cloudhub.io


{"op":"eval", "code":":trial"}


<=== [1262] 200 OK 442ms
connection: Keep-Alive
content-length: 88
content-type: application/json
date: Wed, 20 Mar 2024 01:53:16 GMT
server: http-kit
x-correlation-id: 1b9dbbef-2aa1-465a-bc22-11876beb9c6f


{"session":"d828922b-ec81-430b-924c-e6a49c43cd0a",
 "ns":"user",
 "value":":trial",
 "out":""}


user> (+ 2 2)
===> [7021] POST https://nrepl-app-98wz6g.qyw2z2.jpn-e1.cloudhub.io/nrepl
Content-Type: application/json
Host: nrepl-app-98wz6g.qyw2z2.jpn-e1.cloudhub.io


{"op":"eval", "ns":"user", "code":"(+ 2 2)"}


<=== [7021] 200 OK 72ms
connection: Keep-Alive
content-length: 83
content-type: application/json
date: Wed, 20 Mar 2024 01:53:28 GMT
server: http-kit
x-correlation-id: 05c14c1f-57fc-4645-8002-03e6b1495956


{"session":"67b1e294-1bf5-4aa3-82a2-0b1c6c793a70",
 "ns":"user",
 "value":"4",
 "out":""}


4
user> 
```

# 色々探ってみる
ということで、CloudHub 2.0上でnREPLを走らせてやりとりするところまでできました。
Mule ApplicationからRuntime.getRuntime()を通じてOSのコマンドを発行できるので試しにどんなCPUが使われているの/proc/cpuinfoをみてみましょう。

```
user> (require '[clojure.java.shell :refer [sh]])
nil
user> (-> (sh "cat" "/proc/cpuinfo") :out println)
processor	: 0
vendor_id	: GenuineIntel
cpu family	: 6
model		: 85
model name	: Intel(R) Xeon(R) Platinum 8175M CPU @ 2.50GHz
stepping	: 4
microcode	: 0x2007006
cpu MHz		: 3099.970
cache size	: 33792 KB
physical id	: 0
siblings	: 8
core id		: 0
cpu cores	: 4
apicid		: 0
initial apicid	: 0
fpu		: yes
fpu_exception	: yes
cpuid level	: 13
wp		: yes
flags		: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single pti fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx avx512f avx512dq rdseed adx smap clflushopt clwb avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves ida arat pku ospke
bugs		: cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs itlb_multihit mmio_stale_data retbleed gds
bogomips	: 4999.99
clflush size	: 64
cache_alignment	: 64
address sizes	: 46 bits physical, 48 bits virtual
power management:

...
```
CloudHub 2.0はコンテナなのでノードで使用されているものとなりますが、Intel(R) Xeon(R) Platinum 8175M CPU @ 2.50GHzがつかわれているのがわかります。

また、CloudHub 2.0上からインタラクティブにHTTPのリクエストを投げてみましょう。
```
user> (require '[org.httpkit.client :as http])
user> (-> @(http/get "https://httpbin.org/headers") :body println)
{
  "headers": {
    "Accept-Encoding": "gzip, deflate", 
    "Content-Length": "0", 
    "Host": "httpbin.org", 
    "User-Agent": "http-kit/2.0", 
    "X-Amzn-Trace-Id": "Root=1-65fa43d3-7a20d00b35b4b05f01841841"
  }
}

nil
user> 
```
例では、httpbin.orgで送信したヘッダをJSON形式で返答してもらってる感じになります。

ということで、nREPLでCloudHub 2.0上でのインタラクティブなプログラミングができることができました。やはりnREPL最高ですね。
今回はHTTPでやりましたが、TTY/CIDERを使用した方法をやっていきたいと思います。