Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ(20210308前編)書籍『Ruby on Rails Performance Apocrypha』、rswag gemほか

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

以下のコミットリストのChangelogを中心に見繕いました。今週はやや少なめです。

🔗 Active Storageで画像を生成できない場合にPreviewErrorをraiseするようになった

プレビューを生成できない場合、キャプチャされるIOストリームが空になり、0バイトのプレビューファイルが生成されてActive Storageサービスに保存されてしまう。
これが発生した理由は、PopplerがいくつかのPDFプレビュー生成に失敗して0バイトのファイルになってしまったことによるもの。これらの"preview"をリサイズするとMiniMagickでエラーが発生していた。MiniMagickのこのエラーは、0バイトのファイルに対して試みられた場合には正しい結果に思えるが、プレビューをキャプチャしようとした子プロセスが失敗して終了した場合にPreviewerが通常どおり処理を進めるのは正しいとは考えにくい。
そこで、previewerの子プロセスが0以外のステータスで終了した場合は例外を発生するようにした。
同コミットより大意


つっつきボイス:「今までは画像リサイズ時にMiniMagickのエラーが出ていたのに、そのまま空のプレビューファイルをストレージに置いてしまってたんですね」「プレビュー生成はいろんな理由で失敗する可能性があるので、Active Storageとして明示的にエラーを出すようになったのはいいですね👍」

「ライブラリが返す例外の詳細メッセージに生のコマンドが含まれていたりすると、攻撃者がそれを元にライブラリのバージョンなどを推測して手がかりにする可能性がなくもないので、そういう意味でもライブラリの例外をそのまま出さないようにしておくのは一見地味ですが結構重要だと思います」「なるほど」

🔗 Action Textのリッチテキストでto_trix_htmlが使われるようになった

このプルリクの目的は、rich_text_area_tagを使う場合はAction Textのto_sではなくto_trix_htmlを使うようにすること。to_trix_htmlメソッドをto_sto_plain_textと同じようにもっと使いやすくするためにモデルに委譲を追加した。
同PRより大意


つっつきボイス:「Action Textで使われているTrixは、Basecampが開発したWebアプリ向けリッチテキストエディタ↓」「to_trix_htmlが前からあったのでそれを使うようにしたようですね」

basecamp/trix - GitHub

🔗 Arel::CrudArel::Tableから削除した

compile_updatecompile_deleteは、元もArel::Tableではまったく動かなかった(Arel::Table@ast@ctxもないため)。
compile_insertcreate_insertは動くが、レシーバーの情報をまったく使っていないので、代わりにArel::InsertManager.new(arel_table)を使うこと。
同PRより大意


つっつきボイス:「これは文字通り、実は動かないメソッドを削除したのか」

# activerecord/lib/arel/table.rb#L3
module Arel # :nodoc: all
  class Table
-   include Arel::Crud
    include Arel::FactoryMethods
    include Arel::AliasPredication

🔗 すべてのtree managerでイニシャライザがテーブルを受け取れるようになった

SelectManager80ad95bテーブルを受け取るようになったが、他のmanagerはクエリ生成で必須の場合でもそうなっていない。
SelectManagerと同様、他のtree managerもすべてテーブルを受け取っていいと思う。
同PRより大意


つっつきボイス:「tree managerって初めて見ました」「お、Arelの中にマネージャーがありますね↓」「ホントだ」「いかにもSQLのSELECT/INSERT/UPDATE/DELETEに対応する感じ」

80ad95bからたどっていくとvisitorsがあった↓」「名前や処理からしてVisitorパターンで実装されているようですね」

参考: rails/visitors.rb at main · rails/rails

    def to_dot
      collector = Arel::Collectors::PlainString.new
      collector = Visitors::Dot.new.accept @ast, collector
      collector.value
    end

    def to_sql(engine = Table.engine)
      collector = Arel::Collectors::SQLString.new
      collector = engine.connection.visitor.accept @ast, collector
      collector.value
    end

参考: Visitor パターン - Wikipedia

[保存版]人間が読んで理解できるデザインパターン解説#3: 振舞い系(翻訳)

🔗 excludingの修正


つっつきボイス:「この間追加されたexcludingに修正が入ったそうです(ウォッチ20210222)」「そういえばwithoutというエイリアスもexcludingに追加されてましたね」

「なるほど、1つ目は引数が1個以上必要だったのをなしでもよいことにしたのか↓: プルリクメッセージにも『Active Recordは引数がなくても1=1(つまりtrue)を追加するようになっている』とあるので、それなら引数なしでもよいと判断したんでしょうね」「なるほど」「引数があるかどうかをチェックする分岐を書かなくて済むのはうれしい👍」

# activerecord/lib/active_record/relation/query_methods.rb#L1131
    def excluding(*records)
      records.flatten!(1)

-     raise ArgumentError, "You must pass at least one #{klass.name} object to #excluding." if records.empty?
-
      if records.any? { |record| !record.is_a?(klass) }
        raise ArgumentError, "You must only pass a single or collection of #{klass.name} objects to #excluding."
      end
      spawn.excluding!(records)
    end

「2つ目は引数がnilの場合に対応したとありますね」「これも実質上と同じ話ですね: Active Recordはnilが渡された場合に1=1(つまりtrue)を追加するようになっているとプルリクに書かれてます」

🔗Rails

🔗 電子書籍『Ruby on Rails Performance Apocrypha』


つっつきボイス:「TechRacho記事でもお世話になっているNate Berkopecさんは"The Complete Guide to Rails Performance"という本↓を以前から出していますが、先ごろ新たに"Ruby on Rails Performance Apocrypha"という別冊的な本を出したそうです」

以下はつっつき後に見つけたツイートです。"The Complete Guide to Rails Performance"はRails 6とRuby 2.7にも対応したそうです。

「トピックを見ると、GVLとかworker killerのような、Railsを長く触っているとどこかで遭遇しそうなパフォーマンス周りの話題を扱ってますね↓」「この本は面白そうな予感がします👍」

  • Benchmarks for Rails Apps
  • Reading Flamegraphs
  • Microservices and Trends
  • Why is Ruby Slow?
  • Popularity
  • Page Weights and Frontend Load Times
  • What is the GVL?
  • Reproducing Issues Locally
  • Worker Killers
  • Multi-threading
  • Read Replicas

Rubyのスケール時にGVLの特性を効果的に活用する(翻訳)


「アポクリファって?」「こんな意味でした↓」「なるほど、外典ですか」「アポクリファっていかにもゲームのタイトルとかにありそうな響き」

apocrypha: 外伝、外典(ギリシャ語のἀπόκρυφα(複数形: 隠されたもの)由来で、cryptに通じる)

後で調べると、ゲームを元にした小説のタイトルが見つかりました↓。

参考: Fate/Apocrypha - Wikipedia

🔗 Redisベースのrate limiter(Ruby Weeklyより)


つっつきボイス:「ざっと見た感じでは、rate control用のパラメータをRedisに保存して更新・参照することで、APIサーバーのクライアントTokenごとのrate制御を実現しているようですね」

# 同記事より
class RateLimiter
  TimedOut = ::Class.new(::StandardError)

  REDIS_KEY = "harmonogram_#{Rails.env}_rate_limiter_lock".freeze

  def initialize(redis = Redis.current)
    @redis = redis
    @interval = 1 # seconds between subsequent calls
    @timeout = 15 # amount of time to wait for a time slot
  end

  def with_limited_rate
    started_at = Time.now
    retries = 0

    until claim_time_slot!
      if Time.now - timeout > started_at
        raise TimedOut, "Started at: #{started_at}, timeout: #{timeout}, retries: #{retries}"
      end

      sleep seconds_until_next_slot(retries += 1)
    end

    yield
  end

  private

  attr_reader :redis, :interval, :timeout

redis/redis - GitHub

「こういった処理はRedisでなくてもできますが、マルチスレッドな環境でもアトミックにアクセスできる高速な共有ストレージとしてRedisを使ったんでしょうね」「ふむふむ」「Redisのアトミックアクセス用命令を使うのかなと思ったら、RedisのPTTLというms単位で減っていくTTLカウンタを使うことで、clientへの割り当てタイムスロットの残時間をRedisで管理させているみたい」

## 同記事より
def seconds_until_next_slot(retries)
  ttl = redis.pttl(REDIS_KEY)
  ttl = ttl.negative? ? interval * 1000 : ttl
  ttl += calculate_next_slot_offset(retries)
  ttl / 1000.0
end

# Calculates an offset between 10ms and 50ms to avoid hitting the key right before it expires.
# As the number of retries grows, the offset gets smaller to prioritize earlier requests.
def calculate_next_slot_offset(retries)
  [10, 50 - [retries, 50].min].max
end

参考: PTTL – Redis

「RDBだけだとこういうときに困るのでRedisが欲しくなってきますね」

「あと、こういう処理をRDBで行うとDB接続のコネクションプールを使ってしまうので、Redisのような別のストレージを使う方がありがたい面はあります」「なるほど」

🔗 rswag: RSpecからSwagger JSONを生成(Ruby Weeklyより)

rswag/rswag - GitHub


つっつきボイス:「rswagは見たことなかったけど、★は1000件超えていますね」「rspecのrequest spec風DSLを書いて、Swaggerに対応した定義ファイルの出力やAPIテストができるgemだそうです」

参考: API Documentation & Design Tools for Teams | Swagger

「こんなふうにRSpecにSwagger的なspecを書ける↓、そしてそれを元にrake rswag:specs:swaggerizeでSwagger用のJSONを生成できるのか、へ〜!」

# 同リポジトリより
# spec/integration/blogs_spec.rb
require 'swagger_helper'

describe 'Blogs API' do

  path '/blogs' do

    post 'Creates a blog' do
      tags 'Blogs'
      consumes 'application/json'
      parameter name: :blog, in: :body, schema: {
        type: :object,
        properties: {
          title: { type: :string },
          content: { type: :string }
        },
        required: [ 'title', 'content' ]
      }

      response '201', 'blog created' do
        let(:blog) { { title: 'foo', content: 'bar' } }
        run_test!
      end

      response '422', 'invalid request' do
        let(:blog) { { title: 'foo' } }
        run_test!
      end
    end
  end

  path '/blogs/{id}' do

    get 'Retrieves a blog' do
      tags 'Blogs'
      produces 'application/json', 'application/xml'
      parameter name: :id, in: :path, type: :string

      response '200', 'blog found' do
        schema type: :object,
          properties: {
            id: { type: :integer },
            title: { type: :string },
            content: { type: :string }
          },
          required: [ 'id', 'title', 'content' ]

        let(:id) { Blog.create(title: 'foo', content: 'bar').id }
        run_test!
      end

      response '404', 'blog not found' do
        let(:id) { 'invalid' }
        run_test!
      end

      response '406', 'unsupported accept header' do
        let(:'Accept') { 'application/foo' }
        run_test!
      end
    end
  end
end

「RSpecで振る舞いを記述してそこからJSONを生成するというのは、TDD(Test Driven Development)的な、いやむしろBDD(Behavior Driven Development)的なアプローチを感じますね」「なるほど!」「書いたRSpecはそのままテストコードになるんでしょうね」

参考: テスト駆動開発 - Wikipedia
参考: ビヘイビア駆動開発 - Wikipedia

「実際に使ってみないとわかりませんが、小規模なSwagger+Rails案件をRailsエンジニアだけで開発するなら、このrswagを使ってみてもいいかもと思いました👍」「お〜」「この種の特殊なライブラリは、大規模な案件や複雑な案件でいきなり使うと途中で機能が足りなくなる可能性も考えられるので、最初は小規模な案件で試すのがいいでしょうね」

参考: とにかくRails6でrswagを動かす - Qiita

🔗 AS句その後


つっつきボイス:「そうそう、この間のAS句(ウォッチ20210301)について@kamipoさんがツイートしてましたね」

「興味深いのは、DBの型情報を付けているのがDBのアダプタだということ: 仮にもっと賢いアダプタを作ってそこで型情報を付与できれば、テーブルがなくてもAS句で作ったクエリでActive Recordの型付きオブジェクトを取ることが原理的には可能ということになりますね」「なるほど」「ただ、型情報の付与を厳密にやろうとすると複雑さが増して処理が重くなる可能性はありそう」

🔗 Active Recordクエリをビューに書いてもいいと思う場合(Hacklinesより)


つっつきボイス:「冒頭で『Active Recordクエリをビューに書いてはいけない』とよく言われていることを踏まえて、でもたとえばCategory.allぐらいだったらビューに直接書いてもいいのではという記事でした」「プロジェクトの方針にもよりますが、セレクトボックスの項目をCategory.allで取るぐらいならビューに書いても構わないだろうという気持ちはわかりますね: その一方で『Active Recordクエリをビューに書いてはいけない』と言われる理由もわかります」「なるほど」

「どこまで潔癖にやるかは最終的にプロジェクトの方針次第でしょうね」「開発速度重視のプロトタイプ開発なら、短期間で開発できるRailsのメリットを活かすためにもビューに書くのをありにしてもいいかもと思いました」


同記事の末尾でも「ビューにロジックを置かないようにすれば確かにコードも変更しやすくなるし理解しやすくなる」「でも他の部分にまず影響しない場合なら置いてももいいのでは」と締めくくられています。

🔗 その他Rails


前編は以上です。

バックナンバー(2021年度第1四半期)

週刊Railsウォッチ(20210303後編)Bundlerのセキュリティ修正、Rubyのガベージコレクション記事、Rubyが2/24に誕生日ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly

Hacklines

Hacklines


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。