Rubocop Challenger 作者が教えるオススメの使い方。 〜 .rubocop_todo.yml 解消後も消さないで残しておくと良いことがあります!〜
1 ヶ月も前になってしまいましたが、クラッソーネの @yamat47 さんが拙作の rubocop_challenger を使って .rubocop_todo.yml
を解消した、というブログを書いて下さいました。
かつての自分と同じような課題を抱えていた方に自分の gem を役立てて頂けたのは本当に嬉しいです。 ブログ化までして下さった @yamat47 さん、本当にありがとうございます!
今年に入ってからちょっとずつ進めてたRuboCopのTodoとの戦いをまとめて記事にしました!こんな感じの、開発者向けの改善をするのもめちゃ好きです😀https://t.co/82F6xmAKko
— Takuya Yamaguchi (@yamat47) August 18, 2021
また、gem の README では Circle CI を使った方法しか紹介してなかったのですが、GitHub Actions を使った方法についても解説して頂いており、重ね重ね感謝感激です🙏 GitHub の README からもリンク貼らせて頂きました!
一方で、ひとつ気になったことが。。
自動で解消できるものがなくなって以降はRuboCop Challengerの仕組みが不要になったので、最後は削除してしまいました。
ありがとう、RuboCop Challenger...!!
待って! Rubocop Challenger は消さないで残しておくと良いことがあります!
Rubocop Challenger は v2.0.0 から bundle update 機能も兼ね備えています
本家 RuboCop の gem をアップデートする時、新しく追加された Cop によってエラーが出てしまった、という経験は無いでしょうか? このエラーの解消が億劫で gem のアップデートを放置してしまっている方もいるかも知れません。
Rubocop Challenger v1.0.0 のテーマは「負債の解消」だったんですが、 v2.0.0 からは「RuboCop との共生」だったりします。
誰にも話したこと無いので今始めて公表しました←
v2.0.0 以上の Rubocop Challenger では最初に RuboCop 関連の gem に対して bundle update
を実行するようになっています。
最新の RuboCop にアップデートした状態で Rubocop Challenge を実行するので、新しく追加された Cop に違反したコードがあった場合、その場で即 auto-correct
を実行し、以下のような PR を作成してくれます。
一方 dependabot も rubocop-rspec
v2.5.0 の bundle update の PR を作成してくれていますが、こちらは CI の RuboCop チェックでエラーになっています。
RuboCop の CI チェックをパスしないと merge 出来ない設定にしている場合も多いと思いますが、これを修正するのは地味に面倒です。
ちなみに違反したコードが存在しない場合は Re-generate .rubocop_todo.yml with RuboCop v1.21.0 のような PR が作成され、 bundle update
を実行しただけの PR が作成されます。
この機能は地味に便利なので、CI のスケジュール実行を使って週イチくらいで動かすのがオススメの使い方です。
v2.0.0 以降の RuboCop Challenger は Gemfile に含めない事をオススメします
v2.0.0
あたりから RuboCop Challenger を Gemfile に含めて実行すると、他の gem との互換性問題でエラーになるケースを確認しております🙏
現在では以下のように CI での実行時に gem install rubocop_challenger
を実行する方法をオススメしております。
# .circleci/config.yml version: 2 jobs: rubocop_challenge: docker: - image: circleci/ruby:3.0 working_directory: ~/repo steps: - checkout - run: name: Rubocop Challenge command: | gem install rubocop_challenger rubocop_challenger go \ --email=rubocop-challenger@example.com \ --name="Rubocop Challenger" workflows: version: 2 nightly: triggers: - schedule: cron: "30 23 * * 1,2,3" # 8:30am every Tuesday, Wednsday and Thursday (JST) filters: branches: only: - master jobs: - rubocop_challenge
こちらは完全に自分がやらかしたというか、会社で書いた以下のブログの内容が古いまま更新しなかったため、 Gemfile
に rubocop_challenger
を入れて使っている方が沢山 (?) いるかも知れません。
GitHub の README ですら間違っていたので、さっき慌てて修正しました。。
古い方法で使ってしまった皆さん、本当にすみません。。
rubocop_challenger
を Gemfile に含めるのが.rubocop_todo.yml
解消後にアンインストールしたくなる理由の一つだと思うので、これで使い続ける人が増えてくれると嬉しいです🙏
安全な auto-correct かどうかが分かります
特にアナウンスしていなかった気がするのでついでにご紹介しておくと、最新の Rubocop Challenger では「安全な auto-correct」かどうかが分かるようになっていて、 PR の本文に以下のような記述が入るようになっています。
RuboCop 公式によると「安全な auto-correct」の場合、元のソースコードが壊れる心配は無いとのことなので、 PR merge する際の判断材料にして頂ければ。
MAU, DAU も計測できるようになった redis-objects-daily-counter v0.3.0
前回のブログで redis-objects-daily-counter という gem を作ったという話をしました。
今回は v0.3.0
をリリースしたのでそちらのご紹介です。
【おさらい】redis-objects-daily-counter はどういう gem なのか
redis-objects gem の拡張として作られており、日付の変更毎に新しい counter を定義してくれる daily_counter
機能を提供する gem です。
例えば以下のような Homepage
という class で daily_counter :pv
という定義をします。
class Homepage include Redis::Objects daily_counter :pv, expireat: -> { Time.now + 2_678_400 } # about a month def id 1 end end
すると以下のように #pv
というインスタンスメソッドが使えるようになります。
これは redis-objects
gem の counter
の拡張であり、 #increment
や #decrement
というメソッドで Redis 上の値を更新することが可能です。
# 2021-04-01 homepage = Homepage.new homepage.id # 1 homepage.pv.increment homepage.pv.increment homepage.pv.increment puts homepage.pv.value # 3
daily_counter
が counter
とどのように違うかというと、日付が変わるとRedis 上の Key が変化するという点です。
# 2021-04-02 (next day) puts homepage.pv.value # 0 homepage.pv.increment homepage.pv.increment puts homepage.pv.value # 2
これは、 daily_counter
が以下のようなルールに則って Redis の Key にアクセスするように振る舞うためです。
model_name:id:field_name:yyyy-mm-dd
なお、この時のタイムゾーンは実行中の Ruby プロセスの標準タイムゾーンに従いますが、 Rails 上で実行した場合は application.rb
で指定したタイムゾーンに従います。
日付が変わった後も、前日までの Key-Value は削除していないので、 #range
や #[]
や #at
というメソッドでアクセスすることが可能です。
ちなみに #range
や #[]
では値が返却されますが、 #at
では Redis::Counter
インスタンスが返却されます。
v0.2.0
までは #[]
と #at
は同じ挙動でしたが、 v0.3.0
からそういう実装に変わりました 🙏
# 2021-04-01 homepage.pv.increment(3) # 2021-04-02 (next day) homepage.pv.increment(2) # 2021-04-03 (next day) homepage.pv.increment(5) homepage.pv[Date.new(2021, 4, 1)] # => 3 homepage.pv[Date.new(2021, 4, 1), 3] # => [3, 2, 5] homepage.pv[Date.new(2021, 4, 1)..Date.new(2021, 4, 2)] # => [3, 2] homepage.pv.delete_at(Date.new(2021, 4, 1)) homepage.pv.range(Date.new(2021, 4, 1), Date.new(2021, 4, 3)) # => [0, 2, 5] homepage.pv.at(Date.new(2021, 4, 2)) # => #<Redis::Counter key="homepage:1:pv:2021-04-02"> homepage.pv.at(Date.new(2021, 4, 2)).value # 2
v0.3.0 で追加された機能とは
daily_counter
に加えて daily_set
が追加されました。
こちらは redis-objects
gem の set
に daily-
機能を追加したものになります。
class Homepage include Redis::Objects daily_set :dau, expireat: -> { Time.now + 2_678_400 } # about a month def id 1 end end homepage.dau << 'user1' homepage.dau << 'user2' homepage.dau << 'user1' # dup ignored puts homepage.dau.members # ['user1', 'user2'] puts homepage.dau.length # 2 puts homepage.dau.count # alias of #length
set
は重複を受け付けない Array
と考えて良いと思います。
上記の例を見てもらうと分かりますが user1
を 2 回追加しても #members
による取得結果には 1 つしか含まれていません。
これを使って何が実現できるかというと、 DAU (Daily Active User) の計測です。
ログインしたユーザーの ID を daily_set
で定義した変数に追加していくことで、その日アクティブだったユーザー一覧とその数を知ることが出来ます。
# 2021-04-01 homepage.dau.merge('user1', 'user2') puts homepage.dau.members # ['user1', 'user2'] # 2021-04-02 (next day) puts homepage.dau.members # [] homepage.dau.merge('user2', 'user3') puts homepage.dau.members # ['user2', 'user3'] # 2021-04-03 (next day) homepage.dau.merge('user4') homepage.dau.at(Date.new(2021, 4, 2)) # => #<Redis::Set key="homepage:1:dau:2021-04-02"> homepage.dau.at(Date.new(2021, 4, 2)).members # => ['user2', 'user3'] homepage.dau.at(Date.new(2021, 4, 2)).length # => 2
#range
や #[]
を使う事で指定した期間内の値を結合することも出来ます。
これによって DAU 目的で記録した値から MAU を求めることも可能です。
homepage.dau[Date.new(2021, 4, 1)] # => ['user1', 'user2'] homepage.dau[Date.new(2021, 4, 1), 3] # => ['user1', 'user2', 'user3', 'user4'] homepage.dau[Date.new(2021, 4, 1)..Date.new(2021, 4, 2)] # => ['user1', 'user2', 'user3'] homepage.dau.delete_at(Date.new(2021, 4, 1)) homepage.dau.range(Date.new(2021, 4, 1), Date.new(2021, 4, 3)) # => ['user2', 'user3', 'user4']
#at
で一日ずつ値を取得して Ruby の Array#uniq
を使っても同じ結果が得られますが、 #range
や #[]
の方が内部で Redis の SUNION
を利用するため高速だと思います。
ちなみに #range
や #[]
の結果は Ruby の Array なので、 #length
を使って長さを求める際は実行中のマシンリソースを消費します。
一方 #at
では Redis::Set
を返しており、 Redis::Set#length
は内部で Redis の SCARD
を実行するため、 #length
は #at
からの方が高速だと思います。
ただし一定期間の値の結合は #range
や #[]
を使う必要があるので、結局マシンリソースの消費は避けられないですね。。
この辺りの仕様は実装していてかなり頭を悩ませました。なので今後のアップデートで破壊的な変更が入るかも知れません。
monthly_set
なども実装してあります
前回の記事でも紹介したような monthly_counter
に相当する monthly_set
なども使えます。
MAU, WAU, DAU で使い分けると良いかと思います。
annual_set
- Key format:
model_name:id:field_name:yyyy
- Redis is a highly volatile key-value store, so I don't recommend using it.
- Key format:
monthly_set
- Key format:
model_name:id:field_name:yyyy-mm
- Key format:
weekly_set
- Key format:
model_name:id:field_name:yyyyWw
- Key format:
daily_set
- Key format:
model_name:id:field_name:yyyy-mm-dd
- Key format:
hourly_set
- Key format:
model_name:id:field_name:yyyy-mm-ddThh
- Key format:
minutely_set
- Key format:
model_name:id:field_name:yyyy-mm-ddThh:mi
- Key format:
Redis::Objects を使ったサービス改善と新しい gem を作ったお話
半年ぶりくらいに会社の勉強会で発表しました。
Redis::Objects という gem を使って RDS にかかっていた IO 負荷を改善した、という話と、 Redis::Objects を拡張する gem を作ったという話(後述)です。
Redis を使って DB の負荷を下げる系のお話って 2010 年代にされ尽くした感があって、今更な気もするんですが、一周回って最近聞かない話だなーと思ってます。 フロントエンドエンジニアで最近サーバーサイドも触る機会が増えたような方々にウケが良かった感じがありますね。
一方で社内のインフラエンジニアからは Redis ってメンテナンスの手間が掛かって評判悪いですね。 使うだけなら手軽で楽なんですが、AWS のアップデートでダウンタイムが発生する時に利用者向けにアナウンスしないといけなかったり。 ダウンタイムと言っても深夜に数秒〜数分なので影響は少ないと思うんですが、エンタープライズ向けのサービスとかやってるとこの辺は厳しいですね。
SRE のエラーバジェットの概念 が日本全体に浸透すればソフトウェア開発の生産性は底上げされるんじゃないかと思うのですが、この辺は時間を掛けて少しずつ啓蒙していくしか無いですね。例えば日本の生活インフラ(電気・水道・ガスとか電車とか)ってめっちゃ高水準なので、ソフトウェアの世界でも100%稼働することが当たり前という価値観になってしまうんじゃないかな、と思うのですが、この辺の価値観は表裏一体というか、当たり前と感じているから高品質になる一方で、生産性は下がってしまうという構図だと思っています。
https://www.nic.ad.jp/ja/materials/iw/2017/proceedings/s15/s15-fujisaki.pdf
一応メンテナンスの手間、という点では夏頃に AWS から Amazon MemoryDB for Redis というフルマネージドな Redis が発表されています。 東京リージョンは未対応で、書き込み速度が遅くなっていたり、料金も若干高くなっていたりしますが、気になるサービスではありますね。
さて、この記事のメインは冒頭で触れた Redis::Objects を拡張する gem を作ったよ、という話です。
発端は Redis::Objects の counter
を使ってサービスの効果測定(メール配信数、CV 数、ログイン数など)をしたいなーと考えたことなんですが、効果測定って一定の期間毎に集計して改善した・していないの指標にしたいやつじゃないですか?
counter
でカウントアップしつつ、日次のバッチ処理で集計すれば良いんですが、それだとあまり気軽じゃないというか、測定対象となるサービス開発の後、測定ロジックとバッチ処理、保存先の DB テーブル作成までセットで実装しないと実現できないですよね。
これはサクッと実装するには counter
が自動で日次で保存先を切り替えてくれれば良さそう、ということで、 redis-objects-daily-counter
という gem は daily_counter
という日次で保存先を切り替えるカウンター機能を提供します。
Redis::Objects を拡張して作ってあるので、使い方はほぼ同じで、対象としたい Class に include Redis::Objects
を追加すると使えるようになります。
# Gemfile gem 'redis-objects-daily-counter'
class Homepage include Redis::Objects daily_counter :pv, expireat: -> { Time.now + 2_678_400 } # about a month def id 1 end end # 2021-04-01 homepage = Homepage.new homepage.id # 1 homepage.pv.increment homepage.pv.increment homepage.pv.increment puts homepage.pv.value # 3 # 2021-04-02 (next day) puts homepage.pv.value # 0 homepage.pv.increment homepage.pv.increment puts homepage.pv.value # 2 start_date = Date.new(2021, 4, 1) end_date = Date.new(2021, 4, 2) homepage.pv.range(start_date, end_date) # [3, 2]
#increment
#decrement
は counter
と同じ使い勝手です。ただ、日付が変わると自動的に保存先が切り替わります。
これは Redis で保存している Key 名が以下のフォーマットになっているためです。
model_name:id:field_name:yyyy-mm-dd
日付が変わった後も過去のレコードは削除されていないので、以下のコードでアクセス出来ます。この使い方は同じく Redis::Objects の list
と似たような使い勝手になっています。
# 2021-04-01 homepage.pv.increment(3) # 2021-04-02 (next day) homepage.pv.increment(2) # 2021-04-03 (next day) homepage.pv.increment(5) homepage.pv[Date.new(2021, 4, 1)] # => 3 homepage.pv[Date.new(2021, 4, 1), 3] # => [3, 2, 5] homepage.pv[Date.new(2021, 4, 1)..Date.new(2021, 4, 2)] # => [3, 2] homepage.pv.delete(Date.new(2021, 4, 1)) homepage.pv.range(Date.new(2021, 4, 1), Date.new(2021, 4, 3)) # => [0, 2, 5] homepage.pv.at(Date.new(2021, 4, 2)) # => 2
これで測定対象となるサービスを開発した後、 daily_counter
で測定処理を実装しておくだけで OK。
この状態でリリースすればデータは溜まっていくので、週次や月次で集計するバッチ処理を後でゆっくり実装すれば良いです。
バッチ処理実装前にサーバー内で $ bin/rails console
を実行して daily_counter
の値を確認しながらバッチ処理の実装を考える、とかも出来ますね。
具体的なデータを見ながらの方が開発難易度は下がります。
ちなみにですが、 expireat
オプションを指定しておくことをオススメします。
gem の仕様上、どんどん Redis 上のメモリを圧迫していくので、一定期間後に自動的に削除するようにしましょう。
daily_counter
があるなら週次、月次、年次カウンターがあっても良いよね、なんだったら毎時、毎分カウンターもあったって良いじゃないか、という事で v0.2.0
ではそれらが追加されています。
毎時・毎分のカウンターは API rate limit 機能の実装にも良いかもしれないですね。
annual_counter
- Key format:
model_name:id:field_name:yyyy
- Redis is a highly volatile key-value store, so I don't recommend using it.
- Key format:
monthly_counter
- Key format:
model_name:id:field_name:yyyy-mm
- Key format:
weekly_counter
- Key format:
model_name:id:field_name:yyyyWw
- Key format:
daily_counter
- Key format:
model_name:id:field_name:yyyy-mm-dd
- Key format:
hourly_counter
- Key format:
model_name:id:field_name:yyyy-mm-ddThh
- Key format:
minutely_counter
- Key format:
model_name:id:field_name:yyyy-mm-ddThh:mi
- Key format:
一応実装はしましたが、 annual_counter
のように期間の長いカウンターの利用はあまりオススメしません。
Redis ってふとした拍子にデータが飛んでしまう可能性があるので、あくまでキャッシュ的な位置付けで利用するのが良いと思います。
daily_counter
も週次・月次で RDS に集計結果を保存する運用を想定していますので。
というわけで久々のブログ更新では新しく作った redis-objects-daily-counter
という gem の紹介をさせて頂きました。
これから弊社プロダクトでも利用していく予定ですし、色々なサービスでも活用頂けると嬉しいです。