逃げる8回で会心の一撃

Web エンジニアのサトウリョウスケが開発とか色々書くブログです

Rubocop Challenger 作者が教えるオススメの使い方。 〜 .rubocop_todo.yml 解消後も消さないで残しておくと良いことがあります!〜

1 ヶ月も前になってしまいましたが、クラッソーネの @yamat47 さんが拙作の rubocop_challenger を使って .rubocop_todo.yml を解消した、というブログを書いて下さいました。

かつての自分と同じような課題を抱えていた方に自分の gem を役立てて頂けたのは本当に嬉しいです。 ブログ化までして下さった @yamat47 さん、本当にありがとうございます!

また、gem の README では Circle CI を使った方法しか紹介してなかったのですが、GitHub Actions を使った方法についても解説して頂いており、重ね重ね感謝感激です🙏 GitHub の README からもリンク貼らせて頂きました!

zenn.dev

一方で、ひとつ気になったことが。。

自動で解消できるものがなくなって以降は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 を作成してくれます。

f:id:ryz310:20210923205915p:plain
RSpec/ExcessiveDocstringSpacing を修正した PR が作成されました
f:id:ryz310:20210923210041p:plain
同 PR で rubocop-rspec が v2.4.0 から v2.5.0 にアップデートされています

一方 dependabot も rubocop-rspec v2.5.0 の bundle update の PR を作成してくれていますが、こちらは CI の RuboCop チェックでエラーになっています。 RuboCop の CI チェックをパスしないと merge 出来ない設定にしている場合も多いと思いますが、これを修正するのは地味に面倒です。

f:id:ryz310:20210923210548p:plainf:id:ryz310:20210923210545p:plain
RuboCop の CI チェックでエラー

ちなみに違反したコードが存在しない場合は 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

こちらは完全に自分がやらかしたというか、会社で書いた以下のブログの内容が古いまま更新しなかったため、 Gemfilerubocop_challenger を入れて使っている方が沢山 (?) いるかも知れません。 GitHub の README ですら間違っていたので、さっき慌てて修正しました。。

developer.feedforce.jp

古い方法で使ってしまった皆さん、本当にすみません。。

rubocop_challenger を Gemfile に含めるのが.rubocop_todo.yml 解消後にアンインストールしたくなる理由の一つだと思うので、これで使い続ける人が増えてくれると嬉しいです🙏

安全な auto-correct かどうかが分かります

特にアナウンスしていなかった気がするのでついでにご紹介しておくと、最新の Rubocop Challenger では「安全な auto-correct」かどうかが分かるようになっていて、 PR の本文に以下のような記述が入るようになっています。

f:id:ryz310:20210923215918p:plain
✅ 安全な auto-correct

f:id:ryz310:20210923220846p:plain
⚠️ 安全でない auto-correct

RuboCop 公式によると「安全な auto-correct」の場合、元のソースコードが壊れる心配は無いとのことなので、 PR merge する際の判断材料にして頂ければ。

MAU, DAU も計測できるようになった redis-objects-daily-counter v0.3.0

前回のブログで redis-objects-daily-counter という gem を作ったという話をしました。

ryz310.hateblo.jp

今回は 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_countercounter とどのように違うかというと、日付が変わると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 の setdaily- 機能を追加したものになります。

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 で一日ずつ値を取得して RubyArray#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.
  • monthly_set
    • Key format: model_name:id:field_name:yyyy-mm
  • weekly_set
    • Key format: model_name:id:field_name:yyyyWw
  • daily_set
    • Key format: model_name:id:field_name:yyyy-mm-dd
  • hourly_set
    • Key format: model_name:id:field_name:yyyy-mm-ddThh
  • minutely_set
    • Key format: model_name:id:field_name:yyyy-mm-ddThh:mi

Redis::Objects を使ったサービス改善と新しい gem を作ったお話

半年ぶりくらいに会社の勉強会で発表しました。

Redis::Objects という gem を使って RDS にかかっていた IO 負荷を改善した、という話と、 Redis::Objects を拡張する gem を作ったという話(後述)です。

github.com

Redis を使って DB の負荷を下げる系のお話って 2010 年代にされ尽くした感があって、今更な気もするんですが、一周回って最近聞かない話だなーと思ってます。 フロントエンドエンジニアで最近サーバーサイドも触る機会が増えたような方々にウケが良かった感じがありますね。


一方で社内のインフラエンジニアからは Redis ってメンテナンスの手間が掛かって評判悪いですね。 使うだけなら手軽で楽なんですが、AWS のアップデートでダウンタイムが発生する時に利用者向けにアナウンスしないといけなかったり。 ダウンタイムと言っても深夜に数秒〜数分なので影響は少ないと思うんですが、エンタープライズ向けのサービスとかやってるとこの辺は厳しいですね。

SRE のエラーバジェットの概念 が日本全体に浸透すればソフトウェア開発の生産性は底上げされるんじゃないかと思うのですが、この辺は時間を掛けて少しずつ啓蒙していくしか無いですね。例えば日本の生活インフラ(電気・水道・ガスとか電車とか)ってめっちゃ高水準なので、ソフトウェアの世界でも100%稼働することが当たり前という価値観になってしまうんじゃないかな、と思うのですが、この辺の価値観は表裏一体というか、当たり前と感じているから高品質になる一方で、生産性は下がってしまうという構図だと思っています。

https://www.nic.ad.jp/ja/materials/iw/2017/proceedings/s15/s15-fujisaki.pdf

f:id:ryz310:20210920165304p:plain
SREの信条 (Google)

一応メンテナンスの手間、という点では夏頃に AWS から Amazon MemoryDB for Redis というフルマネージドな Redis が発表されています。 東京リージョンは未対応で、書き込み速度が遅くなっていたり、料金も若干高くなっていたりしますが、気になるサービスではありますね。

dev.classmethod.jp


さて、この記事のメインは冒頭で触れた Redis::Objects を拡張する gem を作ったよ、という話です。

github.com

発端は 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 #decrementcounter と同じ使い勝手です。ただ、日付が変わると自動的に保存先が切り替わります。 これは 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.
  • monthly_counter
    • Key format: model_name:id:field_name:yyyy-mm
  • weekly_counter
    • Key format: model_name:id:field_name:yyyyWw
  • daily_counter
    • Key format: model_name:id:field_name:yyyy-mm-dd
  • hourly_counter
    • Key format: model_name:id:field_name:yyyy-mm-ddThh
  • minutely_counter
    • Key format: model_name:id:field_name:yyyy-mm-ddThh:mi

一応実装はしましたが、 annual_counter のように期間の長いカウンターの利用はあまりオススメしません。 Redis ってふとした拍子にデータが飛んでしまう可能性があるので、あくまでキャッシュ的な位置付けで利用するのが良いと思います。 daily_counter も週次・月次で RDS に集計結果を保存する運用を想定していますので。


というわけで久々のブログ更新では新しく作った redis-objects-daily-counter という gem の紹介をさせて頂きました。 これから弊社プロダクトでも利用していく予定ですし、色々なサービスでも活用頂けると嬉しいです。