mikutter blog

mikutterのアナウンスなど

mRPC

昨年はmikutterをほとんど触っていなかったのだが、この頃また大きめの機能開発を行っているので、生存報告を兼ねて現状何をどこまで作っているかを書いておく。

今はプロトタイピングの段階で、まだそれをまとめて動かせるような状態にはなっていない。できてもいない話を書き残すのは嫌なのだが、一応コードはあるから……という言い訳をしつつ、往生際の悪いことにまだmikutterに機能追加する意欲があるということを伝えるとともに、自分の頭の整理も兼ねてこの記事を書いている。

(書いてる最中に2013年から存在していたバグを見つけてしまってリリースを打つ羽目になったので、この記事から生存報告の意味はほとんど失われてしまったのじゃ……。)

mRPCとは

Pluggaloidは、システムをプラグインという単位に切り分けて個別に開発できるようにしつつ、イベントなどの仕組みを使って柔軟にそれらを連携させることができる。

これらはPluggaloidが提供するイベントなどの通信機能を使って連携しており、依存していない。 依存していないというのは、2つのプラグインは確かに連携しているのに、もう片方のプラグインが存在していなくても問題なく動作するということだ。 同じようなイベントを受信し、同じようなイベントを送出する「何か」があればそれでいい。そして、イベントさえ到達すれば、どこにあったっていい。

mRPC(mikutter Remote Plugin Call)は、複数のmikutter間でイベントを転送し合うことで、総体としてひとつのmikutterと同等のものを表現しようという試みだ。

f:id:toshi_a:20210504112304p:plain

プロセスを分離することのメリット・デメリット

まずはじめに、当初考えていたマルチプロセスのメリット・デメリットを簡単にまとめる。

並列実行

最も簡単に思いつくのは、並列実行可能になることだ。 詳細は割愛するが、Rubyでは基本的にはThreadが同時に動くことはできない。

しかしプロセスが分かれていれば当然このような制約はなく、プロセス数ぶんだけCPUコアを使うことができる。

別のホストへのアタッチ・デタッチ

別のプロセスに乗ったPluggaloidの通信方法がUnixソケットのようなものを使うなら、当然TCPコネクションなどを用いて別の場所で動いているPluggaloidにも接続できる。 Mastodonプラグインや自動リプライプラグインはMasotodonサーバが動いているマシンで動かしておいて、UIなどの他のプラグインを手元のマシンで動かすといったことが可能になる。

異なるプログラミング言語での開発

そのプロトコルさえわかっていれば、通信する相手はRubyで書かれたコードである必要はない。

プロセス間通信のオーバーヘッド

今まで必要なかった、シリアライズ・デシリアライズの処理が入る。 イベントはプラグインが通信する手段として多用されているため、却って性能が低下する。

やりたいこと

このセクションに関してだけは夢を語るほかない。

並列処理は言うまでもなくパフォーマンス、とくに速度の向上を期待して行うものだが、今のmikutterは概ねやりたいことに対して十分なパフォーマンスを発揮できている。mRPCのような思い切った賭けに出る必要はない。

メリットとデメリットを約分すると、「別のホストへのアタッチ・デタッチ」「異なるプログラミング言語での開発」の2つが残る。これが私がmRPCを使って実現したいことだ。

mikutterクライアント

もはやmikutterは特定のSNSに依存しなくなった。数多くのWorldを束ねるmikutterは、それ自体がプラットフォームで、独自の世界観がある。 タイムラインはMastodon用のレンダラを用意せず、あくまで汎用的なものを使うのは、mikutterにとってTwitterMastodonも、mikutter同士が通信するための手段に過ぎないからだ。 あるいはストレージ(データベース)と捉えてもいい。Twitter APIMastodon APIはそれらを読み書きするクエリ言語だ。

それを突き詰めると、mikutter同士が通信する手段をmikutterが持っていないことに違和感を覚える。いくつかWorldプラグインが登場しているのに、mikutter Worldは存在しないのだ。 残念ながら、mRPCはこのような用途には使えない。他人のmikutterと通信するには、たとえばActivityPubのようなプロトコルを使ったほうが良いだろう。

「mikutterを常に起動しておけというのか?」そのとおりだ。

自分用のmikutterをサーバなどで常時稼働させておいて、PCやスマートフォンのmikutterクライアントで、自分のmikutterに接続するのだ。 この考え方はMastodonの登場によって、そんなに奇妙な考え方ではなくなった。 おひとりさまサーバ(自分一人のためだけのMastodonサーバ)を構築して運用している人もそれなりにいるし、そのくらい分散していたほうが良いと考える人もいる。

この、mikutterサーバとmikutterクライアントが通信するプロトコルこそが、今開発しているmRPCだ。

異なるプログラミング言語での開発

Rubyはなんやかんや気に入っているものの、残念ながらどうしても向いていないことがある。 最たる例はクライアントアプリケーションだ。

UIツールキットのバインディングは惨憺たるもので、唯一Rabbitなどに使われているRuby-GNOMEが積極的にメンテナンスされている。 しかしGtk2はmikutterの高い要求を満たせるほどのものではなく、結局mikutterのタイムラインを表現するためにMiracle Painterを開発した。これは画像としてMessageを描画するという、とんでもないものだ。それを現在も使っている。

現在Gtk3の移行をやってくれているブランチもあるのだが、私は面白いと思ったことしかやれず、(非常に重要な作業なんだけど)停滞してしまっている。 本当に残念なことだが、私はこのスタンスで今までやってきたわけで、申し訳無さよりも10年間やりたいこととやるべきことが噛み合ってきた偶然に対する驚きのほうが大きい。これに関しては、もともとmikutterユーザはみんなこれが泥舟だとわかって乗り込んでいると思うので、これを読んで怒る人はいないと思っているのだが。

GUIに関しては、WindowsMacでmikutterを使う人は更に苦しんでいるらしい。そもそもああいったOSはWMなどもひっくるめてしっかりと作り込まれており、Gtkなどの汎用的なUIツールキットとなじまない。 ユーザ体験の話だけなら慣れの問題といえるかもしれないが、場合によってはIMEが有効にならないとか、クラッシュするといった無視できない問題も起こっているようだ。

スマートフォン用のアプリを作るのは、Rubyではそもそも不可能だ。これには反論の余地があると思うが、俺はやりたくね〜

そういった、異なる言語で実装されたmikutterクライアントと通信するためのプロトコルこそが、今開発しているmRPCだ。 これはもう少し将来の話になると思うけれど、このような用途も見据えて開発を行っている。

できてること

ここまで遠い目標の話をした。ここからは、今できている範囲で、もう少し突っ込んだ技術的な話を書いてく。

mRPCを実装する場所として、mRPCというプラグインを作ることにした。 Pluggaloidの機能として追加することも検討したが、それよりはプラグインとして通信機能をつけたほうがmikutterらしいと思ったからだ。

Pluggaloidからは外部プロセスはすべて単一のmRPCというプラグインのように見えていることになるため、Pluggaloidに変更を加える必要はない。 プロトコルもmRPCプラグインの中で完結しているため、Pluggaloid未満のレイヤーのプロトコルは交換可能になっている。

gRPC

gRPC (IDLとしてProtocol Buffer)を採用してプロトタイプを作っている。

目的を達成できれば何でも良いのだが、以下の点でgRPCを採用するに至った。

  1. 有名であり、それなりに枯れている(ググれば誰でも使える)
  2. Ruby以外の言語で簡単に実装できる
  3. シリアライズフォーマットが規定されていて、それが効率的である

1に関しては当然だろう。こんなところで奇をてらっても、将来私以外の人間がmRPCクライアントやサーバを作るときに困るだけでなく、私も困るからだ。 2についても、前の節で述べたとおりだ。 3は、REST APIJSONを選ばなかった理由だ。Protocol Bufferは様々な言語のサービスインターフェイスを自動生成できるだけでなく、JSON Schemaなどに比べればはるかに読みやすい。それだけでAPI仕様を理解できるし、ドキュメントの生成にも使うことができる。それらがひとつのファイルから作られるから、差異が生じないのもポイントだ。

mRPCプロトコルのことだけ考えることにした

gRPCとは関係ないのだが、ある問題で数ヶ月間も悩んだ。

イベントやフィルタはプロトタイプ宣言といって型情報を宣言できることは知っているだろうか。 あるいは、Diva::Modelのサブクラス(ツイートとかTootを表現しているアレ)にフィールドの型情報があるのは見たことがあるかもしれない。 当初、その型情報をIDLとして使おうとしたのだ。

実際にはこれは現実的ではなくて、そもそもイベントのプロトタイプ宣言による型定義はReactive APIの登場まで特に利用されておらず、宣言されることが稀だった。 現在でも、必要がなければ宣言しないと思う。 もしそれを徹底したとしても、そのようなmikutterサーバにクライアントが接続するときに、事前にそのスキーマを得ておく必要がある。

そして、mikutterはロードされるプラグインによってイベントやDiva::Modelが増えていくので(すべてがプラグインなので当然)、Protocol Bufferのファイルを事前に生成してクライアントを開発するのではなく、刻々と変わるスキーマにあわせてクライアントも動的に変化していく必要がある。 一応リフレクションという、動的にサービスのメソッドを得る仕組みはあるものの、クライアント側に求める実装が難しくなりすぎると本末転倒だ。 そのような経緯から、一度採用を見送った。

しかし、この問題はPluggaloid側の問題なので何を採用したところであまり変わらず、代案と比べてもgRPCは前述の3点でそこそこ優れていた。 結局この問題は、Protocol Bufferで型を記述するのはmRPCプロトコルの部分だけで、Pluggaloidについては一切面倒を見ないことにした。

分散オブジェクトシステムの再発明

くわえて、Diva::Modelは以下のことも考慮しなければならない。

  • mutableである
  • フィールドの型としてDiva::Modelを持つことができる
    • 深さに制限はない
    • 循環構造もとりうる

たわわベンチマーク

循環構造はリプライの先と元で相互に参照するようになっているModelがあると思う(Twitterではchildrenはメモ化されたメソッドだったが、フィールドでも表現できる)。リプライの親は連結リストのようになっているため、会話が弾めば、メール会話の引用ネストのようにMessageがどんどん膨れ上がっていく。最悪の例としては、Twitterの「月曜日のたわわ」がある。このアカウントは毎週乳のでかい女の絵を投稿するのだが、本当にでかいのは会話ツリーのほうだ。

  • このアカウントは、新たな絵を投稿するときに先週の絵の投稿へのリプライとするため、連結リストのような構造になる。私が知る限りでも100週まではそれを継続しており、今確認したところ300週を越えていた
  • そのチェインのツイートに画像が添付されている
  • 人気があり、各ツイートには大量の異なるアカウントからのリプライがつく

今週の「月曜日のたわわ」のツイートに対して「会話スレッドを表示」コマンドを呼び出されることを考えると、悪夢そのものだ。これは極端な例だと思うかもしれないが、Twitterクライアントを使うようなツイのオタクは、おっぱいがでかいと見ると絶対にクリックする。そのためだけにTwitterを使っている人も珍しくない。

後期Twitterクライアント時代にはこの「たわわベンチマーク」はクライアント開発者の間では常識となっていた。mikutterでもリプライチェーンの遅延ロード処理の確認や、Deferred再実装のさいのベンチマークとして利用した。

これは過去の話ではない。人類がTwitterMastodonを手放したとしても、おっぱいを手放すことはないからだ。 当然mRPCを開発するときにも、常に「たわわではどうなる?」と問いかけ続けなければならない。

Pluggaloid::Mirage

現在の実装では、Diva::Modelを送る場合、Model slug(Modelクラス毎に固有の値)とURIのペアを送っている。

受け取った側は、このModelのメソッドを呼び出したい場合、専用のgRPCメソッドにメソッド名と上記のペアを送ることで、結果を受け取れる。これをクエリと呼んでいる。

ここまでDiva::Modelの問題として取り扱ってきたが、実際のところmikutterでは行儀の悪いことにイベントでDiva::Modelではないオブジェクトも多くやり取りされている。 しかし実はPluggaloidはDivaに全く依存していない。Diva::Modelなら良いというのはmikutterが勝手に言っていることだ。そしてそのような不文律が存在していた理由は、将来mRPCと呼ばれるものが実装されたときのためだった。

実際にはPluggaloidはDivaに依存しないため、Diva::Modelを特別扱いすることはできない。そこでPluggaloidに Pluggaloid::Mirage というModuleを追加した。 mPRCは、このmoduleをincludeしたクラスを特別扱いし、mRPCで公開可能な(クエリ可能な)オブジェクトとして扱う。

これからは、Pluggaloidで受け渡すオブジェクトとして、Pluggaloid::Mirageをincludeしているものを推奨することになるだろう。 今のところDiva::Modelの他に、Plugin::GUI::Widgetがある。 サードパーティプラグインがPluggaloid::Mirageをincludeすることも可能だ。

gRPCメソッド

イベント、フィルタ、Pluggaloid::Mirageのクエリの実装が必要となる。

defdslを用いた連携は、mikutterプラグインでは依存関係があると見做され、同時にロードされることが要求されるため、この通信に含める必要はない。 例外として、Spellをこの中に加えている。今のところはSpellだけだが、あといくつか専用のgRPCメソッドを用意するかもしれない。

あと、mikutter 4.1のReactive APIは、実はmRPCの通信を効率化することも狙いのひとつだったので、専用のメソッドをそれぞれ用意するかもしれない。

イベント

f:id:toshi_a:20210504112519p:plain

イベントを購読するメソッド。接続時に購読するイベントを1つ指定する。 Server Streamであり、イベントが発生する度にサーバからデータが送られてくる。

フィルタ

f:id:toshi_a:20210504112541p:plain

フィルタを登録するメソッド。接続時にフィルタ名を1つ指定する。

これはBidirectional Streamで相当複雑に見えるが、イベントに2つほどやることを追加しただけだ。

  1. (loop [Filtering])イベントと違って戻り値がある。フィルタした結果をクライアントからサーバに返す必要がある。このためにBidirectional Streamにする必要があった。
  2. (loop [ProxyObject query])サーバはフィルタされた結果にプロキシオブジェクトが含まれていた場合、サーバ→クライアントの方向にクエリを投げるかもしれない。

特に二番目が厄介だ。gRPCクライアントはサーバに対してサービスを提供できないようなので、サーバからクライアントのサービスのメソッドをリクエストできない。要するにフィルタの結果を受け取ったサーバが、後でそれに含まれるプロキシオブジェクトに対してクエリできないのだ。

そこで今の実装では、フィルタのコネクションの中でプロキシオブジェクトのクエリを行えるようにした。これを発展させて、クライアントがフィルタ結果をサーバに返すと同時にProxyValueを投機的に送るといった工夫ができるかもしれない。

Spell

f:id:toshi_a:20210504112611p:plain

こちらはクライアントがサーバのspellを呼び出すだけ。前2つで紹介したもののように、「spellのパターンを追加する」といったものではない(それはそれで用意する必要がありそうだが)。

紹介した理由は、リクエスト-レスポンス型のメソッドではなく、Bidirectional Streamを使う必要があったからだ。

理由はフィルタと全く同じで、サーバ→クライアントの方向にクエリを行う必要があるため。

まとめ

先に実現可能性を棚に上げて色々と夢を語ったが、複数のmikutterをmRPCで通信させるのはまだやろうとは思っていない。Mastodonプラグインの入ったサーバとGtkプラグインが入ったクライアントが通信しても、今とできることは大して変わらないからだ。

直近では、Spellを叩いてターミナルからmikutter経由で投稿できるような単純なものしか出てこないだろう。 調子よくいけば、簡易的なmikutterクライアントを作れると思っている。プラットフォームとしてはFlutterを使いたいと思っている。

Flutterは、iOS/Androidアプリのほか、最近だとWindows/Mac/Linuxのデスクトップアプリケーション、更にはWebの対応が順調に進んでいて、それぞれのプラットフォームのUIコンポーネントは使わずにすべて独自に画像としてレンダリングした上で、各プラットフォームのUIコンポーネントそっくりのテーマを適用できる。

独自にレンダリングして大丈夫なのか?とmikutterユーザなら疑問に思うことはないだろう。なぜなら私達はmikutterがすでにMiracle Painterでそれをやっているのを知っているからだ。 mikutterクライアントには当面本腰を入れるつもりはないし、Gtk3移行の代わりにできるほどのクオリティではない。しかし、mRPCによってこのような話が少々現実味を帯びてきているのは愉快なことだ。