📚

RuboCopのServer Modeで高速lint/formatする with Docker

2022/09/12に公開

以前、rubocop-daemonをDocker環境で動かしてvscodeで高速にformat on saveする記事を書きました。
https://zenn.dev/cumet04/articles/fast-rubocop-on-docker

この記事ではサードパーティのライブラリのfork + 自前のクライアントスクリプトで実現していましたが、2022年6月末にリリースされたRuboCop 1.31より、同じことを実現するServer Modeが公式に実装されたようです。
https://docs.rubocop.org/rubocop/usage/server.html

というわけで、こちらでも同様にDocker環境 + vscodeで動かしてみました。

挙動や仕様を探ってみる

まずはServer Modeを軽く動かしてみて挙動や仕様を確認してみます。

最低限rubocopが動くRuby on Dockerな開発環境を作り、そこで動作確認をしたあとにvscodeに組み込んでいく流れを想定しています。なお完成品のリポジトリがこちらです。sinatraのサンプルコードにrubocopを入れただけのコンテナをdocker composeで起動する構成です。

https://github.com/cumet04/sbox_rubocop-server-on-docker

通常のユースケースを試す

まずはコンテナ/ホストを意識しない通常のユースケースで動かしてみます。通常の開発をしている想定でdocker composeでコンテナを起動し、そのコンテナの中に入って各種コマンドを試してみます。

ドキュメントによると、--serverオプションをつけてrubocopを実行した場合、サーバプロセスがなければ起動して実行、既にサーバがあればそれを使って実行するというような挙動になるようです。
ということで、serverなし・serverあり(一回目)・serverあり(二回目)の3パターンをtimeコマンドで時間を見ながら実行します。[1]

アプリケーションコンテナの中にbashで入った
root@b6d6d4555a18:/app# time rubocop .
Inspecting 2 files
..

2 files inspected, no offenses detected

real    0m0.677s
user    0m0.617s
sys     0m0.132s
root@b6d6d4555a18:/app# time rubocop --server
RuboCop server starting on 127.0.0.1:46801.
Inspecting 2 files
..

2 files inspected, no offenses detected

real    0m0.645s
user    0m0.616s
sys     0m0.102s
root@b6d6d4555a18:/app# time rubocop --server
Inspecting 2 files
..

2 files inspected, no offenses detected

real    0m0.242s
user    0m0.059s
sys     0m0.019s

realだけ取り出してサマリすると以下となります。

  • serverなし: 0.677s
  • serverあり(1): 0.645s
  • serverあり(2): 0.242s

serverあり(2)が早くなっており、想定通りの挙動をしているように見えます。今回は実験環境が非常にミニマルなためserverなしでもそこまで遅くありませんが、実際のプロジェクトではより遅いはずなので、serverありで得られる恩恵はより大きいでしょう。

コンテナ外からの実行を考える

以前書いた記事では、あらかじめrubocopのdaemonをコンテナとして常時起動しておき、それに対してホストマシンからシンクライアント的にコマンドを実行するという仕組みにしていました。そのため、Server Modeでも似た仕組みを想定しつつ挙動を見ていきます。

ドキュメントによると--start-server--stop-serverオプションでサーバの起動や停止だけを実行できるようなので、試してみます。

コンテナ内bash
root@b6d6d4555a18:/app# rubocop --start-server
RuboCop server starting on 127.0.0.1:41393.
root@b6d6d4555a18:/app# rubocop --start-server
RuboCop server (27) is already running.
root@b6d6d4555a18:/app# rubocop --stop-server
root@b6d6d4555a18:/app# rubocop --start-server
RuboCop server starting on 127.0.0.1:42167.

デフォルトではポート番号は特に固定されておらず、起動ごとに変更されるようです。ここは$RUBOCOP_SERVER_HOST$RUBOCOP_SERVER_PORT環境変数で指定できるとドキュメントに記載されています。

また--start-serverはサーバプロセスを起動してすぐ終了する仕様で、ドキュメントを見てもサーバをforegroundで起動するようなオプションは見当たりませんでした。サーバを単独で起動させるコンテナを立てるには少々工夫が要りそうです。

ともあれ、適当なポートをホストにbindしたコンテナでサーバを実行し、ホスト側からそこに繋いでrubocopしてみます。
docker-compose.ymlに以下のようなサービスを追加し、

docker-compose.yml
  rubocop-server:
    ...
    command: bash -c 'rubocop --start-server; sleep infinity'
    environment:
      RUBOCOP_SERVER_HOST: 0.0.0.0
      RUBOCOP_SERVER_PORT: 45678
    ports:
      - 45678:45678

これを起動した状態でホスト側でenv RUBOCOP_SERVER_HOST=localhost RUBOCOP_SERVER_PORT=45678 rubocop --serverと実行すると

Address already in use - bind(2) for "localhost" port 45678 (Errno::EADDRINUSE)

新しくサーバを起動しようとしてエラーになってしまいました。どうも別ホストに明示的に起動したサーバに接続しにいくようなユースケースは想定されていないようです。

コードからこの挙動を追ってみた記録

念のため、rubocop本体のコードを確認して上記仕様を確かめてみます。

まずエントリーポイント的なファイルの該当コードがこれです。
https://github.com/rubocop/rubocop/blob/d5d2fe1b37624b43b055507e3e1329f747e7812d/exe/rubocop#L11-L12

ここから、RuboCop::Server.running?の定義を追ってみると、下記のようになっていました。
https://github.com/rubocop/rubocop/blob/d5d2fe1b37624b43b055507e3e1329f747e7812d/lib/rubocop/server.rb#L33-L37

これ以上は掘り下げていませんが、見るからにキャッシュ(ファイルシステムと思われる)やpidを参照しており、あくまでサーバもクライアントも同一ホストで動作させる想定になっているようです。

仕方ないのでこの方針は諦めます。そこで通常のユースケースに則り、rubocop用のコンテナを立てるようなことはせず、アプリケーションコンテナに対してdocker compose execrubocop --serverを実行してみます。

ホストマシン上
sbox_rubocop-server-on-docker$ time docker compose exec app rubocop --server
RuboCop server starting on 127.0.0.1:34289.
Inspecting 2 files
..

2 files inspected, no offenses detected

real    0m0.873s
user    0m0.039s
sys     0m0.030s
sbox_rubocop-server-on-docker$ time docker compose exec app rubocop --server
Inspecting 2 files
..

2 files inspected, no offenses detected

real    0m0.238s
user    0m0.043s
sys     0m0.026s

2回目が早くなっており、単にこれを実行させれば良さそうということがわかります。
こちらのほうがシンプルに実現できそうであり、かつそもそも他の方法は無いことから、この方向で進めます。

vscodeに組み込む

Server Modeの挙動や仕様とDocker環境での実現方針は決まったので、vscode上で動かせるように設定していきます。なお、vscode想定とはいえ用意するラッパースクリプトは汎用なので、vscode以外でも使えるはずです。

前提となる環境条件ですが、cop対象のコードがあるコンテナがdocker-composeで動作しており、そのプロジェクト内にあるrubocopが1.31以上のバージョンになっていればokです。特に追加のライブラリやコンテナ設定などは不要です。

まずホスト環境でrubocopコマンドとして振る舞うラッパースクリプトを用意します。ここではbin/rubocopとして下記のようなスクリプトを作成します。

bin/rubocop
#!/bin/bash

cd $(dirname $0)/.. # docker-compose.ymlがあるディレクトリに移動

OPTION=$(test -p /dev/stdin && echo '-T') # 標準入力を受ける場合もそうでない場合もいい感じに動くよう、`-T`オプションの有無を制御
docker compose exec $OPTION app rubocop --server $@ # --serverオプションを入れつつ、他の引数をそのまま渡す

これでbin/rubocopが実質コンテナ内でのrubocopコマンドとして動作します。ただしファイルパスはコンテナ内のWORKDIR基準なので注意が必要です。

次にvscode用に拡張機能と設定を用意します。rubocopを動作させる拡張としてmisogi.ruby-rubocopを導入し、また下記設定を追加します。

settings.json
  "ruby.rubocop.executePath": "./bin/",
  // formatはせずlintだけでよいなら下記設定は不要
  "[ruby]": {
    "editor.defaultFormatter": "misogi.ruby-rubocop"
  },

ここまでできれば、Server Modeなrubocopがいい感じにlintやformatを実行してくれるようになります。

導入のために追加したファイルは上記2つのみで、特にプロジェクトの既存の設定やコードに手を入れておらず、コンテナを足したりもしていません。つまり、rubocopのバージョンさえ上がっていれば既存のプロジェクトにも大きな変更を入れずに導入することができます

そのrubocopのバージョンを上げるのがどのくらい現実的かというのはプロジェクトによってまちまちかもしれませんが、追加のライブラリを足すのと違い、rubocop本体は遅かれ早かれ上げるものだと思います。せっかくなので、このタイミングで上げてみるのも良いでしょう。

まとめ

RuboCopが公式にServer Modeを実装したことにより、高速なlintやformatがかなり簡単に導入できるようになりました。

これまでの環境への変更が多い手順に抵抗があった方も、これを機に導入してみてはいかがでしょうか。

脚注
  1. シンタックスハイライトが想定と違うように見えていますが、ハイライトなしよりは見やすいと思うのでこのままにしています ↩︎

GitHubで編集を提案

Discussion