Airframe Updates 2022
AirframeはScalaでの開発を支援するOSSライブラリ集です。カラフルなLogger、REST/RPCサーバー、HTTPクライアント、オブジェクトのSerialization、JSON処理、コマンドラインプログラムの作成などが一通り用意されており、Scalaでアプリケーションを作るまでの時間が短縮できます。
2021年11月に開催されたScala Con 2021では、Airframeを使うとScalaの知識のみでFrontend/Backendの開発ができることを紹介しました:
(本当は2020年に発表する予定でしたが、コロナの影響で2021年まで伸びてしまいました。無事開催できて良かったです)
Scala 3 Support
Scala 3への対応は既に9割ほど完了しています。Scala StewardのメンテナでもあるTatsuno (exoego)さんがTreasure Dataに加入したタイミングで、Scala 3対応への様々なブロッカーを取り除いてくれました。そのおかげで多くのモジュールが既にScala 2.12/2.13/3 + Scala.jsのクロスビルドに対応しています。
最大の課題は、airframe-httpのバックエンドとして使っているFinagleのScala 3対応でしたが、開発元のTwitter社の大規模レイオフでFinagleのScala 3対応が絶望的になりました:
実際のところ、FinagleのScala 3対応には1年以上前から見切りをつけていて、Finagleの代替プロダクトを探していました。そこでFinagleやgRPCのバックエンドであるNettyを直接使ってairframe-http-nettyを実装できることがわかり、FinagleのScala 3対応を待たずにairfame-httpの移植が進みそうです。Netty版のAirframeは既にFinagle, gRPCと遜色ない性能でのRPC処理(40,000 RPC/sec. in M1 Macbook。最速のgRPC backendは50,000 RPC/sec.程度)が実現できています。
Scala 3への対応では、Scala 2と互換性のなくなった新しいマクロへの対応が課題でした。基本的には、マクロを使っているコードを親traitに押し込み、src/main/scala-2、src/main/scala-3とScala versionごとに違うソースフォルダに格納して、Scala 3への対応は無理をせず、ゆっくりと時間のあるときに進めていました。
以前、Scalaプロジェクトをメンテする3つのコツ、という記事で紹介したテクニックは、以下の通りです。
- Scalaのアップグレードを学習の機会としてとらえる
- Mono repo(単一レポジトリ)にすることで管理・テストの手間を抑える
- (細かいアップデートをしてくれる)Scala Ninjaを見つける
今回、Scala 3のマクロやdottyコンパイラの中身にも触れる機会があり、新しいマクロ、inline機能の手軽さを学べました。また、Mono repoにしていたおかげでAirSpecがScala 3に対応しないと他のモジュールも対応できないなど依存関係が明確になり、Scala 3に向けて取捨選択すべき機能、互換性を壊すなどの判断も1つのレポジトリ内で行うことができました。最後のScala Ninjaに関しては、@exoegoさんと、彼が開発しているScala Stewardが強力でした。これまでに500以上のpull requestがmergeされています。おかげで常に最新版のライブラリ群に追従できているScala OSSになりました。最新リリースの情報が自動的にやってきて、さらに自分のプロダクトと合わせたテストまでしてくれます。情報の流れがこちら側に変わるので、毎日ニュースを読むような感覚でOSS界隈のアップデートについて知ることができるようになりました。
2年前ほどは、Scala 3に追従できずAirframeのコード資産を破棄しなくてはいけない可能性について怯えていましたが、もはやその心配はなくなっています。逆に、Sparkなど他の巨大プロジェクトがScala 3に追いつくのを楽しみに待てる状況です。
Airframe HTTP
HTTPクライアントのインターフェースを、より簡単に使えるように刷新しました。以下のように手軽に使い始めることができます。
import wvlet.airframe.http.Http
val client = Http.newClient.newSyncClient("https://wvlet.org")
val req= client.request(Http.get("/"))
このHttpクライアントはairframe-json/codecによるserializationを内蔵していて、client.readAs[X](…) とすることで、JSONレスポンスをcase classなどのオブジェクト読み込むこともできます。
また、Httpクライアントのバックエンドに、Java11から正式に導入されたPure JavaのHttpクライアントをデフォルトで使うようになりました。これで他のHTTPクライアントライブラリ(OkHttp, Jetty, Finagleなど)に依存せずにHTTPクライアントが使えます。Java8用にはURLConnnectionBackendが使えます。こちらはsbt-sonatypeなどまだJava8をサポートする必要があるプラグインで使われています。
airframe-httpのhttpクライアントには、HttpステータスコードやExceptionの種類に応じた複雑なリトライパターンが実装されています。5xx status codeではリトライし、4xxではリトライしない(ただし、408 request timetout, 410 gone, 429 too many request, 499 client closedは例外)、SSL接続でのエラー時のリトライ処理が標準で実装されています。CircuitBreakerも導入されており、過剰なアクセスを抑えたり、Jitterを使ってリトライ間隔をバラつかせる対応もなされています。参考:Timeouts, retries, and backoff with jitter (AWS Builders’ Library):
Airframe RPCの標準化
2022年の目標はAirframe RPCの規格の標準化でした。エラー処理、RPCサーバー・クライアントのログ、バックエンドごとに別々だったRequest/Responseオブジェクトの共通化などです。
Airframe RPCの仕組み
Airframe RPCは、Scalaでメソッド定義を書くと、そのままScala/Scala.jsで使えるサーバー、クライアントのRPCインターフェースになるという、フレームワークです。
内部では、MessagePack(あるいはJSON)でデータを通信し、プログラマはローカルで関数呼びだしをしているのと同等の感覚で、リモートサーバーにあるコードを実行できます。
RPCStatus
Airframe RPCを利用しやすくするため、まずRPCで使うエラーコードの標準化を進め、RPCStatusを定義しました。HTTPのステータスコード(4xx, 5xx)やgRPCのステータスへの変換表も用意しています。RPCStatusを起点としてエラーを報告することで、HTTPステータスコードの深い知識(例えばUnauthenticated errorは401 or 403? 僕は毎回Webで調べています)がなくても適切なHTTP/RPCステータスコードを返せるようになりました。基本的にはgRPCのエラーコードを元にしています。
また、RPCStatusから作成されるRPCExceptionに対応するレスポンスにはエラーメッセージなどが自動的にJSON形式で含まれるようになっています。アプリケーション特有のエラーコード、メッセージ、スタックトレースなども標準化したフォーマットで入るので開発が非常に楽になります。sbt-airframeで作成されたRPCクライアントを使うと、サーバーで発生したRPCExceptionを、クライアント側でRPCExceptionとしてそのまま受け取ることができます。例えば、try … catch { case e: RPCException => … } というコードで、サーバー・クライアント間でのエラーの受け渡しが確実に行えるようになりました。
まだいくつかScala 3への対応の課題が残っていますが、NettyバックエンドのHTTPサーバーができてきたことで、2023年にはScala 3に対応したHTTP/1, HTTP/2の双方に対応したRPCサービスが実現できそうです。
Aiframe DI
Airframe DIは、アプリケーションに必要な複雑な依存関係にあるクラスの構築を助けてくれるライブラリです。Scala 3の対応とともに、in-trait injectionを廃止し、constructor injectionのみをサポートするようにしました。
In-trait injection (コード中にbind[X]と記述する)記法はコードにAirframe DIの依存関係が必要だったのですが、Treasure Data社内の利用例を調査しているうちにconstructor injectionのみでも問題ないことが見えてきました。またbind[X]記法をなくすことで、アプリケーションコードと、DIによるオブジェクト構築を完全に分離することができ、DIの使い方がよりシンプルになりました。
DIを使った開発中にエンジニアが考えることは、必要なサービスをコンストラクタの引数に型として並べるだけでよく、サービスの構築(オブジェクトの作成)はデザイン定義を見て、DIフレームワーク側が処理してくれます。
また、AutoCloseableのインターフェースを実装したクラスに関しては、DIのセッション終了後、自動的にclose()を呼ぶようになっています。DIでインジェクトされたクラスのリソースが自動的に解放されるので重宝しています。
AirSpecテストフレームワークではAirframe DIがfirst-classでサポートされており、テスト用に確保したリソースがテスト終了後に自動解放されます。before/after, setUp/tearDownなどを定義してリソース確保、解放のためのコードを書く手間がなくなっています:
他に隠れた機能として、DIのtracing機能がサポートされています:
val d = newDesign
.withTracer(ChromeTracer.newTracer("target/trace.json"))
ここで出力されたJSONファイルをChromeに読ませるとDIの様子が可視化できます:
Airframe Rx
WebアプリケーションのUIを実装する際に、Reactive Streamの考え方が使えることはご存知でしょうか?
Reactive Streamsのインターフェースは、Scala Collectionライブラリを日常的に使っている方には見慣れたもので、唯一の違いが、ユーザーのマウスクリックなどのイベントの受信を起点に、map, filterなどの一連のコレクション操作が実行される点です。サーバーからRPCのレスポンスを受け取ったタイミングで、任意のコードを実行して、DOMを書き換えることができるようになっています。
Airframe RxではReactive Streamインターフェースを実装し、Scala.jsによるDOMレンダリングをサポートしています。以下にAirfram Rx, Scala.js, Airframe RPCで実装したアプリケーションの例を紹介します。
VS Codeで使われているMonacoエディタを活用して、SQLエディタを作成:
Airframe Rxの技術的な特徴は、Streamイベントの受信を適切にcancelできる点です。Webページではページの書き換えなどが頻繁に行われますが、ページが書き換えられたタイミングで、書き換え前のRxストリームのsubscriptionを自動的にキャンセルし、ブラウザのメモリを食い潰さないように管理できますし、安全です。また、イベントに応じて必要なDOM部分のみを書き換えるので、React.jsのような全DOMをメモリで構築してから差分を計算して描画するVirtual DOM方式よりも、高速化するのが簡単です。
Rxのインターフェースは、ウェブUIだけではなく、Streaming RPCの実装、HTTPクライアントのインターフェースとしても有用なことが見えてきました。2023年にはScalaのFuture(Execution Contextがアプリケーションロジックに紛れ込んで使い勝手が悪い)から脱却し、Rxベースのインターフェースを標準として採用していこうと考えています。
Airframe SQL
Airframeには実は、TPC-H/TPC-DSのSQLクエリを完全にparseできるSQL lexer/parser/analyzerが実装されています。このモジュールがどう使われるかは今後紹介する機会があると思われますが、1つの応用例として、カラムナのParquetファイルを手軽に読み書きするairframe-parquetモジュールで使われています。Parquetファイルに対して簡単なSQLを実行することもできます。
Airframe ULID
ランダムなIDにタイムスタンプをつけることで時系列順にソートできるUUID v6などの話題が盛り上がっていますが、Airframe ULIDを使うと同等の機能が既に利用できます。詳細はこちらをどうぞ:
2023年に向けて
2021- 2022年だけでもここでは紹介しきれないくらい多くの更新がなされています。詳細はrelease noteにありますが、今後のアップデートはGitHubのリリースページで紹介していきます。
2023年には、
- Scala 3対応の完了
- Airframeを使ったアプリケーション開発手法を通して学べるドキュメント
を中心に開発を進めていく予定です。また、airframe-rxやairframe-sql周りから派生して面白いプロダクトもできるのではないかと思っています。
それではみなさま、良いお年を。