- Ruby / Rails関連
週刊Railsウォッチ(20210308前編)書籍『Ruby on Rails Performance Apocrypha』、rswag gemほか
こんにちは、hachi8833です。
🔗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_s
やto_plain_text
と同じようにもっと使いやすくするためにモデルに委譲を追加した。
同PRより大意
つっつきボイス:「Action Textで使われているTrixは、Basecampが開発したWebアプリ向けリッチテキストエディタ↓」「to_trix_html
が前からあったのでそれを使うようにしたようですね」
🔗 Arel::Crud
をArel::Table
から削除した
compile_update
とcompile_delete
は、元もArel::Table
ではまったく動かなかった(Arel::Table
に@ast
も@ctx
もないため)。
compile_insert
とcreate_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でイニシャライザがテーブルを受け取れるようになった
- PR: Allow all tree managers' initializer takes a table by kamipo · Pull Request #41579 · rails/rails
SelectManager
は80ad95bテーブルを受け取るようになったが、他のmanagerはクエリ生成で必須の場合でもそうなっていない。
SelectManager
と同様、他のtree managerもすべてテーブルを受け取っていいと思う。
同PRより大意
つっつきボイス:「tree managerって初めて見ました」「お、Arelの中にマネージャーがありますね↓」「ホントだ」「いかにもSQLのSELECT/INSERT/UPDATE/DELETEに対応する感じ」
- rails/select_manager.rb at main · rails/rails
- rails/insert_manager.rb at main · rails/rails
- rails/update_manager.rb at main · rails/rails
- rails/delete_manager.rb at main · rails/rails
「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
🔗 excluding
の修正
- PR: Make .excluding work when no arguments are passed by jorgemanrubia · Pull Request #41601 · rails/rails
- PR: Make .excluding work when a nil argument is passed by jorgemanrubia · Pull Request #41604 · rails/rails
つっつきボイス:「この間追加された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』
Announcing: The Ruby on Rails Performance Apocrypha. A book that introduces performance engineering, frontend and Ruby perf, and scaling. It's available now for just $10 on Gumroad, DRM-free: https://t.co/76q75gk5vd pic.twitter.com/6sbFxNwRiK
— Nate Berkopec (@nateberkopec) January 14, 2021
つっつきボイス:「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にも対応したそうです。
The Complete Guide to Rails Performance has been updated for Rails 6 and Ruby 2.7: https://t.co/7zcuTX1CdF
[from my GitHub sponsor]
— Yukihiro Matsumoto (@yukihiro_matz) July 22, 2020
「トピックを見ると、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
「アポクリファって?」「こんな意味でした↓」「なるほど、外典ですか」「アポクリファっていかにもゲームのタイトルとかにありそうな響き」
apocrypha: 外伝、外典(ギリシャ語のἀπόκρυφα(複数形: 隠されたもの)由来で、cryptに通じる)
後で調べると、ゲームを元にした小説のタイトルが見つかりました↓。
参考: Fate/Apocrypha - Wikipedia
🔗 Redisベースのrate limiter(Ruby Weeklyより)
- 元記事: Redis rate limiter
つっつきボイス:「ざっと見た感じでは、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を使ったんでしょうね」「ふむふむ」「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は見たことなかったけど、★は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句その後
TimeWithZoneオブジェクトが欲しいのならそれは今は無理だけどAS句で作ったカラムにDBの型情報がないのはsqlite3 adapterだからで、内部的にはadapterは結果セットの型情報を返してもよい。 / “【Rails 6.1】AS 句で作ったカラムに DB の型情報はない - esm アジャイル事業…” https://t.co/EOAJo5TmTG
— Ryuta Kamizono (@kamipo) March 1, 2021
ただし、空のcolumn_typesを返すとcolumn_types.rejectを抑制できてallocationを減らせるというボーナスがあるので、型情報があっても極力空のcolumn_typesを返すようにしている。https://t.co/ZeuGyAXMbO
— Ryuta Kamizono (@kamipo) March 1, 2021
つっつきボイス:「そうそう、この間の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に誕生日ほか
- 20210222 ActiveRecord::Relationの新メソッドload_asyncとexcluding、Active Jobのperform_laterの改善ほか
- 20210209後編 Rubyでミニ言語処理系を作る、Kernel#getsの意外な機能、CSSのcontent-visibilityほか
- 20210208前編 Rails次期リリースがバージョン7に決定、thoughtbotのアプリケーションセキュリティガイドほか](/hachi8833/2021_02_08/103801)
- 20210202後編 Ruby 3 irbのmeasureコマンド、テストを関数型言語のマインドセットで考えるほか
- 20210201前編 Webpackerのガイドがマージ、RailsはRuby 3でどのぐらい速くなったかほか
- 20210126後編 Google Cloud FunctionsがRubyをサポート、Ruby 3のパターンマッチングでポーカーゲームほか
- 20210125前編 Railsリポジトリのデフォルトブランチがmainに変更、Rails 6.1はMySQLのENUM型に対応済みほか
- 20210120後編 Ruby 3.0の新機能で遊ぶ、RubyスニペットをJSに変換するRuby2JS、rspec-parameterized gemほか
- 20210113後編 Ruby 3.0 Ractor解説記事、Vercelホスティングサービス、教育用OS xv6ほか
- 20210112前編 Active Recordの範囲指定バリデーション改善、soleとfind_sole_byメソッド、AlgoliaとRailsほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)