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を使用しています。
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
に記載されているEDN形式を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
ビルド
以下よりダウンロードして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
<?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が作成されます。
$ mvn clean package -DskipTests -Dmaven.javadoc.skip
これをTCPアプリケーションとしてデプロイするにはMuleSoftのプラットフォームAPIを使用する必要があるのですが、ここでは自作のCLIを使うことにします。
(実際には今回はHTTPで使用するので管理コンソールからデプロイすることも可能ですが、接続クライアントとしてもこのCLIを使いたいため使用します)
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を使用した方法をやっていきたいと思います。