こんにちは、hachi8833です。こちらはオンラインイベントや無料コンテンツのまとめサイトだそうです↓。
- サイト: オンラインフェスジャパン
つっつきボイス:「音楽ライブ系の情報多いですね」「フェスだからいろいろあるかも」「漫画や書籍が無料で読める系も」「毎週のようにライブ見に行ってる知り合いも、ここ最近イベント中止が相次いで週末やることなくなったって言ってますし」「毎週のようにってスゴい」「自分の回りのJリーグファンもみんな気落ちしてますし」
- 各記事冒頭にはでパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
- 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です
- 毎月第一木曜日に「公開つっつき会」を開催しています: お気軽にご応募ください
Rails: 先週の改修(Rails公式ニュースより)
今回はコミットリストから見繕いました。
perform_enqueued_jobs
がリトライしないよう修正
ActiveJob::TestCase#perform_enqueued_jobs
は今後リトライしないようになる。
perform_enqueued_jobs
をブロックなしで呼び出すと、アダプタは既にキューに入っているジョブを実行するようになった。キューの中にある、後に完了することが決まっているジョブは実行されなくなる。
この変更が影響するのは、perform_enqueued_jobs
にブロックを渡さなかった場合に限られる。
Changelogより大意
# activejob/lib/active_job/test_helper.rb#L602
def jobs_with(jobs, only: nil, except: nil, queue: nil, at: nil)
validate_option(only: only, except: except)
- jobs.count do |job|
+ jobs.dup.count do |job|
job_class = job.fetch(:job)
if only
next false unless filter_as_proc(only).call(job)
elsif except
next false if filter_as_proc(except).call(job)
end
if queue
next false unless queue.to_s == job.fetch(:queue, job_class.queue_name)
end
if at && job[:at]
next false if job[:at] > at.to_f
end
yield job if block_given?
true
end
end
つっつきボイス:「プルリクのreeenqueueってeが一個多い気がする」「ホントだ」「そういえばGitHubにはスペルチェッカーありませんね」
問題
perform_enqueued_jobs
をブロック無しで実行すると、リトライメカニズムで自分自身を再度キューに乗せるジョブが即座に動き出してしまう。
この動作はperform_enqueued_jobs
にブロックを渡すのであれば理解できる。
しかし自分が期待するのは、perform_enqueued_jobs
にブロックを渡さない場合は、後でキューに乗るジョブを実行するのではなく、既にキューに乗っているジョブを実行することである。
解決法
ジョブの配列をdupして今後改変されないようにする。
同PRより大意
「あーよくある操作かも: ジョブを実行した後にそのジョブリストを取り出すのか、実行する前にジョブリストを取り出すのかというタイミングをちゃんと使い分けできるようにしたのかな」「ジョブをdup
して解決したってありますね」「dup
しないとジョブのタイミングによってうまくいかないことがあったのかも」
これは#33626の意図にも合致する。
しかしこれは重要な変更ではあるが微妙な変更でもある。元のメソッドは「そのブロックを実行中に有効になったジョブが実行対象となる」というような意味だが、メソッド名にブロックを渡さない場合の文字どおりの意味としては「それまでにキューに乗っていた特定のジョブを実行する」ということになる。そしておそらく「既存のジョブだけではなく後続のジョブも実行してしまう」というバグが存在していた。
これらを別のメソッドに分割して振る舞いを区別できるようにする手もある。つまり「マッチするジョブを、ブロックの実行中に実行する」と「キューで待ち状態のジョブだけを実行する」を区別する。
ジョブのリトライをテストする場合、後続のジョブをジョブ階層的にキューに乗せ、スケジュールされたジョブは第2のブロック形式呼び出しでラップする必要があるだろうか?
同PRコメントより
# 同PRコメントより
# キューに乗ったジョブAを実行する。ジョブAはジョブBもキューに乗せる。
A.perform_later
# ここで実行すればアサーションで期待どおりの出力が得られるはず
perform_enqueued_jobs only: [ A, B ]
# あれ、ジョブAがキューに乗せたジョブBが動かなかったぞ
assert some_b_thing # => failed
# キューに乗ったジョブを実行し、その実行中にキューに乗ったジョブを実行する?
perform_enqueued_jobs do
perform_enqueued_jobs
end
「perform_enqueued_jobs
が入れ子になってる」「変な書き方」「コメントを見た感じではブロック回りの話のようだ」
「ちなみに現場レベルだと1回実行するとうまくいかないのにもう1回実行するとなぜかうまくいくバグってあったりしますよね」「あるある」「実行するとなぜか1個余っちゃって、念のためもう1回実行すると消えるみたいな」「理由わからないけど現場はそれで解決しようみたいな」「# こうするとちゃんと動く
とかコメントが付くヤツ」「# たまにうまくいかないけど2回動かせば大丈夫
とか」「冪等になってればいいという考え方もありますけど」
IPAddr
をlocalhostと比較したときに毎回例外をrescueしていたのをやめた
- PR: Fix exceptions raised/rescued in dev by IPAddr by nateberkopec · Pull Request #38694 · rails/rails
# railties/lib/rails/application/configuration.rb#L36
@hosts = Array(([IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0"), ".localhost"] if Rails.env.development?))
@hosts = Array(([".localhost", IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0")] if Rails.env.development?))
つっつきボイス:「IPAddr
をlocalhostと比較すると例外が発生していたんですね」「localhostはIPアドレスじゃないしな〜」
「比較の順序を変えただけ?」「IPAddr
構造体と比較するより先にlocalhostと比較するようにしたようだけど」
「これ見ると↓一番よく使うのがlocalhostなのにlocalhostの比較がトップに来ないのは遅いって(コメント)」「あ〜そういうことね」「リクエストのたんびに例外をrescueするのをやめてパフォーマンスを改善しようってことだと思いました」「HostAuthorizationミドルウェアで毎回ここを通ってたのか、わかる」
MRI以外のプラットフォームでは例外のraiseがかなり遅いので、例外を制御フローに使うのは可能な限り許すべきではないと個人的には思う。
同コメントより大意
localhostで使ったファビコンが残ってしまう問題
「Rackミドルウェアは必ず通るし」「まあ最近は開発環境でlocalhostを指定することあんまりありませんけど」「それな」「localhostを複数の案件で使っちゃうと、ブラウザ表示で他の案件のファビコンが表示されちゃったりしてやりづらいんですよ」「そうそう、スクショとか撮りづらくなる」「だからなんちゃら.lvh.me
とかにして、他の案件のファビコンが出てこないようにしてます」
「それ気になってました: localhostを指定してブラウザ表示すると、普段よく表示しているオレオレアプリのファビコンが他のアプリにも表示されちゃうことが多いのはなぜなんだろうって」「もともとブラウザはファビコンについてはかなりアグレッシブにキャッシュしちゃうんですよ: 本来はメタタグにファビコンパスの定義を書けるんですけど、そういう定義がなくてもブラウザが『ここにファビコンがあるはず』って勝手に取りに行っちゃう変な文化があります」「えぇ〜、そうだったんですか」
参考: 正しいfaviconの設定方法を対応ブラウザ別にまとめる | ブログ | Glatch(グラッチ) - 夫婦で活動するフリーランスWeb制作ユニット
参考: Google Chromeでブックマークのfaviconがおかしいのを直す方法 | N★Typeブログ
「おすすめの対処法は、localhostの代わりに上で言ってるなんちゃら.lvh.me
みたいに案件ごとにドメインを明示的に 分けることでしょうね」「なるほど!」「IPアドレス直打ちとかすると、ごくまれにうまく行かなかったりすることもありますので、lvh.meみたいなループバックドメインのサブドメインにしておく方が余分な面倒を減らせます」「自社アプリだけ開発しているなら気にしなくていいんでしょうけど」「うちみたいな受託ソルジャーでは必要ですね」
参考: 【Rails】ローカル環境の開発でサブドメインがある場合「localhost」ではなく「lvh.me」を使う - FujiYasuの日記
RubyのIPアドレス処理
「そういえばRubyのデフォルトのIPアドレス機能って、CIDR構成ネットワークのinclusionチェックはできるけど人間が読めるstring形式にさっと戻せないとか、結構融通効かないんですよね」「たしかにイマイチ」「サイダー?」「IPアドレスに続けて/26
とか書くアレですね」「任意の箇所でIPアドレスを区切るヤツ」「ああ思い出しました」「Rubyのデフォルト機能には、サブネットマスクを取ってコネコネするみたいなのがないんですよね〜」
参考: Classless Inter-Domain Routing - Wikipedia — CIDR
参考: class IPAddr
(Ruby 2.7.0 リファレンスマニュアル)
「かといって今どきIPアドレス比較のビット演算処理を手作りしたくありませんし」「そんな生々しい処理」「現代の俺たちがやるべきことじゃない気がする」
「いつだったかkamipoさんも『今の時代にIPアドレス処理するはめになるとは』みたいなことをつぶやいてましたね」「そうそう、ipaddress
とかいうもっと高機能なgemを入れてました」「引数で渡されたものがサブネットの中にあるかどうかみたいなコードを今どき自分で書きたくない」「同意」
以下のgemがそれかどうかわかりませんが参考までに。
stringのアロケーションを削減
# actionpack/lib/abstract_controller/helpers.rb#L60
def helper_method(*methods)
methods.flatten!
self._helper_methods += methods
location = caller_locations(1, 1).first
file, line = location.path, location.lineno
methods.each do |method|
_helpers.class_eval <<-ruby_eval, file, line
- def #{method}(*args, &blk) # def current_user(*args, &blk)
- controller.send(%(#{method}), *args, &blk) # controller.send(:current_user, *args, &blk)
- end # end
- ruby2_keywords(%(#{method})) if respond_to?(:ruby2_keywords, true)
+ def #{method}(*args, &block) # def current_user(*args, &block)
+ controller.send(:'#{method}', *args, &block) # controller.send(:'current_user', *args, &block)
+ end # end
+ ruby2_keywords(:'#{method}') if respond_to?(:ruby2_keywords, true)
ruby_eval
end
end
つっつきボイス:「なるほど、stringをシンボルに変えたと」「ブロック変数も&blk
から&block
に」
Rails.envでStringInquirer
のサブクラスを最適化して使うようにした
# activesupport/lib/active_support/core_ext/string/inquiry.rb#L3
require "active_support/string_inquirer"
+require "active_support/environment_inquirer"
# activesupport/lib/active_support/environment_inquirer.rb
module ActiveSupport
# StringInquirerの特殊用途
# 環境文字列に基づいてコンストラクション時にデフォルトの3つの環境を定義する
class EnvironmentInquirer < StringInquirer
DEFAULT_ENVIRONMENTS = ["development", "test", "production"]
def initialize(env)
super(env)
DEFAULT_ENVIRONMENTS.each do |default_env|
singleton_class.define_method(:"#{env}?", (env == default_env).method(:itself))
end
end
end
end
つっつきボイス:「すとりんぐいんくわいあら〜?」「初めて見ました」「何をするヤツなんだろう?」「Active Supportなのか」
「ああなるほど: Rails.env.production?
みたいなbooleanなカラムをチェックする述語メソッドを生やすヤツか」「なるほど」「StringInquirer
からenvチェックを切り離してる」
「たぶんenvのdevelopmentとtestとproductionみたいによく使うものをいちいちmethod_missing
で呼んでたら非効率だから、シングルトンメソッドをあらかじめ定義しちゃおうぜということかなと」「ははぁ、なるほど」「stagingみたいにデフォルトにないenvはmethod_missing
で遅い呼び出しでもええやろと」「パフォーマンスも10倍ぐらい速くなってるぞと」「method_missing
の実装は重いからな〜」
参考: BasicObject#method_missing
(Ruby 2.7.0 リファレンスマニュアル)
Warming up --------------------------------------
StringInquirer 145.978k i/100ms
EnvironmentInquirer 367.087k i/100ms
Calculating -------------------------------------
StringInquirer 2.332M (±10.8%) i/s - 11.532M in 5.019755s
EnvironmentInquirer 27.333M (± 3.4%) i/s - 136.556M in 5.001554s
ドキュメント修正
つっつきボイス:「お、またindex_by
(ウォッチ20200309)」「今度はAPIドキュメントが追加されてました」「ああなるほど」「修正後はindex_byとindex_withの説明が相補的になってる」「index_by
はともかくindex_with
の説明は欲しい」「サンプルとともに」
index_by: enumerableをハッシュに変換する。ブロックの結果をハッシュのキーとし、そのときのelementをハッシュの値とする。
index_with: enumerableをハッシュに変換する。そのときのelementをハッシュのキーとし、ブロックの結果をハッシュの値とする。
同PRより大意
つっつきボイス:「こちらはGetting Startedガイドの修正で、RailsInstallerの記述が削除されました」「RailsInstaller、懐かしい〜: 随分前に更新されなくなってますよねこれ」「更新後のドキュメントにもありますけど、今はWindowsだとRubyInstaller for Windows使って、さらにSQLiteもインストールする」「まさに先週した話ですね(ウォッチ20200310)」「つらい作業」
- サイト: RailsInstaller — 更新なし
- サイト: Downloads: RubyInstaller for Windows
- サイト: SQLite Home Page
Rails
Simpacker: Webpackerのオルタナティブ
https://t.co/o5ZyElt5PD 見ての感想ですが、クックパッドマートは内部でSimpacker使ってます。 (今のところ特に問題なし)https://t.co/wghVSYDEF5
— ryo katsuma / cookpad mart (@ryo_katsuma) March 9, 2020
Webpackerというプロダクトは、Webpackのことを知らなくても簡単に使えるし、他にも便利な機能を提供しているところがよい。Webpackerのつらみは、WebpackをWebpacker固有のDSLやwebpacker.ymlでコンフィグしないといけない点だ。既にWebpackのコンフィグ方法を知っている人は、それをWebpackerのコンフィグに置き換える必要がある。自分はwebpack.config.jsを直接設定したいの!
Simpackerは、webpackのmanifest.json出力を参照してjavascript_pack_tag経由でscriptタグを作成する最小限の機能のみを提供する。その分Webpackの機能を知る必要があるが、Simpackerについてはほとんど何も知らなくてよい。
ただし、Webpackerにあるyarn統合やリクエスト編集のようなお便利機能はSimpackerでは利用できない。
同リポジトリより大意
つっつきボイス:「ああクックパッドさんのSimpacker」「こんなのあったんだ〜」「考えてみたら、WebpackerってRailsエンジニア側の発想ですし」「」「Webpackでやりたいフロントエンジニア側のことはあんまり考えてない感」「WebpackerではWebpackに触らせないんでしたっけ?」「というより、あくまでWebpackerからWebpackを制御するという感覚ですね: Webpackを直接触りたいフロントエンジニアはSimpackを使うと直接やれるようになるということらしい」「ふ〜む」
参考: Webpackerはもう要らない〜 Simpacker - Qiita
「ほほぅ、SimpackerではWebpackとwebpacker-cliは入るけど、webpacker-dev-serverは自分で入れないといけないのか」「webpacker-dev-serverは入ってないと結構つらそう」「このQiita記事を見た感じでは、普通にWebpackとReactを設定してる雰囲気」「使う機会があったらやってみてもいいかも」
N+1と戦うツール2点
- リポジトリ: k0kubun/activerecord-precounter: Yet Another N+1 COUNT Query Killer for ActiveRecord
- リポジトリ: flyerhzm/eager_group: fix n+1 aggregate sql functions for rails
つっつきボイス:「この間公開した翻訳記事↓でこの2つのgemが紹介されていたので」
「activerecord-precounterはk0kubunさんのgemか」「これもk0kubunさんのactiverecord-precountより設計のきれいな、counter cacheのオルタナティブ」「わかる: Active Recordにパッチを当てるgemって結構コワイし」
# 同リポジトリより
tweets = Tweet.all
ActiveRecord::Precounter.new(tweets).precount(:favorites)
tweets.each do |tweet|
p tweet.favorites_count
end
# SELECT `tweets`.* FROM `tweets`
# SELECT COUNT(`favorites`.`tweet_id`), `favorites`.`tweet_id` FROM `favorites` WHERE `favorites`.`tweet_id` IN (1, 2, 3, 4, 5) GROUP BY `favorites`.`tweet_id`
「もうひとつのeager_group gemは集計関数を扱うヤツみたいです」
-- 同リポジトリより
SELECT "posts".* FROM "posts";
SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = 1 AND "comments"."status" = 'approved'
SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = 2 AND "comments"."status" = 'approved'
SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = 3 AND "comments"."status" = 'approved'
-- ↓
SELECT "posts".* FROM "posts";
SELECT COUNT(*) AS count_all, post_id AS post_id FROM "comments" WHERE "comments"."post_id" IN (1, 2, 3) AND "comments"."status" = 'approved' GROUP BY post_id;
「なるほど、1つのSQLに集約してくれるのか」「きれいに動いてくれてればいいけど、scopedとかでおかしなことになったりしないかな」
# 同リポジトリより
class Post < ActiveRecord::Base
has_many :comments
define_eager_group :comments_average_rating, :comments, :average, :rating
define_eager_group :approved_comments_count, :comments, :count, :*, -> { approved }
end
class Comment < ActiveRecord::Base
belongs_to :post
scope :approved, -> { where(status: 'approved') }
end
「こういうふうに↑define_eager_group
と書かなければ勝手に動き出さないということらしい↓: 自分はこういうときに生SQL書くことが多いけど、同じようなコードが何度も出てくるような状況ならこういうのを使ってもいいかな」「なるほど」「生SQLも似たようなものをいくつも書くことになるの多いですし」「たしかに」「使うならscopedでは避けたい気がしますね: joinしてscopedなものにこのgemをかけると変なことになったりして」
「このgemの作者としては、こうやってオブジェクトの属性の1個になるように書けるのがいいということなんでしょうね」「たしかにシンプルですね」「ハッシュが登場してハッシュをループで回したりするのは気持ちよくないでしょうし」「それにQuery Objectにするにしても何個も似たようなQuery Objectができるのはあんまりうれしくないし」「こう書きたい気持ちはワカル」
Rails 6に入るstrict_loading
「N+1といえば、count系とは違いますけどstrict_loading
っていうN+1を殺す場所を探すのに便利な機能がRailsでデフォルトで入るってこないだのウォッチに載ってましたね(ウォッチ20200302)」「おぉそういえば」「includes
みたいなeager loadingメソッドをを叩かない状態でhas_manyリレーションにアクセスすると落ちてくれるので、N+1を絶対殺したいときに使える標準機能」「エグい: strict_loadingモードで調べられるってことなのね」「lazy loadさせないための機能」
「strict_loading
って名前がイマイチな気がしますけど: force_eager_loading
とかすればいいのに」「たしかに」「#37400にいいねが多いのもわかる気がしました」「みんなN+1に苦しめられてますし」「strict_loading
使うのはちょいめんどくさいですけど」「N+1が出るときはあっても実用上問題ないレベルなら使えるのがRailsのよさという考えもあるので、悩ましい」「まあ業務アプリではこういう機能がある方が確実にいいですね」
Hanami::API
は速度をフィーチャー(Ruby Weeklyより)
つっつきボイス:「この間取り上げようと思って漏れてしまいましたが、HanamiがAPIはじめました〼ということだそうです」「うん、HanamiがAPIサーバーの方向に向かうのは正しい気がしますね」「言われてみればAPIの方が向いてるかも?」「Railsは昔から完全にフルスタックな方向に向かっていますし、Railsと棲み分けるのであれば、Hanamiはビューやりませんぐらいのスタンスにしてみるのもありだと思います」「自分もHanamiがAPIに向かうのは全然ありだと思います」
「ところでこのベンチマークのSinatraの遅さにビビったんですけど: Railsより遅いのかと」「」「ああ、下の方にルーティング10,000とか書いてますけど、Sinatraはルーティング増えるとてきめんに遅くなるんですよ」「そういえばそんな話ありましたね」
「メモリーフットプリントはそこまで差は開いてませんね↓」
「そもそもルーティングが10,000もあるようなアプリって作りませんよね」「」「とりあえずこの比較はSinatraに不利すぎてうのみにできないけど、HanamiがAPIの高速化に専念するのは方向性として有望だと思いますし、何ならうちらで使ってみてもいいかも」
chaskiq: オープンソースのキャンペーンプラットフォーム(Ruby Weeklyより)
- リポジトリ: chaskiq/chaskiq: Open Source conversational Marketing alternative to Intercom, Drift, etc..
- サイト: Chaskiq - opensource conversational marketing | Chaskiq · Conversational marketing made open source. An Intercom, Zendesk, Drift alternative
最近conversational marketingという言い方が流行ってるんでしょうか。
つっつきボイス:「IntercomやZendeskやDriftのオルタナティブか: Driftは聞いたことある気がするけど」「それっぽいキャンペーンサイトをRailsで立ち上げられる全部盛りキットみたいな感じでした」「ああ、そのまますぐ使えるようなヤツね: ノリとしてはRedmineみたいな感じかな」「このchaskiq.ioみたいなLP(ランディングページ)的なサイトが作れるんでしょうね」「だと思います」
「それがRailsである必要がどこまであるかというのは思いますけど」「」「まあWordPressみたいに、つよつよのエンジニアなしでもとりあえず立ち上げられるところまでやれるなら価値あるかも?」「どうだろう、Railsという時点で割とハードル高かったりして」「一応Dockerとか使ってるみたいだし、うまいことパッケージングしてすっと使えるようにしてるかも」「Rails 6.0.2だから最新ですね」「金もリソースもこれからみたいなベンチャーや個人が週末までにRailsでサイト立ち上げたいみたいな用途に使えそうですね」「そういうときはスピードが一番貴重だったりしますし、ダッシュボード的なものも付いててやってます感アピールできますし、スタートアップにはそういうのが本当に大事」「やらずに済むことをやらずに済ませるというのが大事」
前編は以上です。
バックナンバー(2020年度第1四半期)
週刊Railsウォッチ(20200310後編)Flutter+Firebaseでモバイルアプリ開発、命名7つの鉄則、Rubyバージョンを切り替えるchruby gemほか
- 20200309前編 Webpackerに乗り換えるべき理由25、Railsのindex_byとindex_withは有能、GCPはやっぱりスゴいほか
- 20200303後編 Ruby 2.7で引数のruby2_keywordsフラグを確認する、fake_apiでAPIプロトタイプ、groupdateで日付をグルーピングほか
- 20200302前編 RubyKaigi 2020は9月に延期、Railsのセキュリティパッチバージョニングが変更、dry-monadsほか
- 20200226後編 dry-rbを使うべき理由、最近のRubyオンライン教材、AWSから乗り換えた話ほか
- 20200212後編 Rubyistが解説するUnicodeとUTF-8、Sorbetが速い理由、CSSの歴史、2019年の脆弱性まとめほか
- 20200210前編 Railsのベンチマークジェネレータ、長いバックグラウンドジョブと戦う、Timestamp切り詰めの謎、Open APIツールほか
- 20200204後編 Ruby3.0の他のbreaking change、Rubyのシリアライザ、GitHubのcode ownersほか
- 20200203前編 Railsの各種高速化コミット、OpenAPIの使い所、パンくずリストgem loaf、Railsビュー最適化ほか
- 20200128後編 もう一つのgemマネージャgel、”Did you mean”の仕組みを追う、DXOpalでブラウザゲームほか
- 20200127前編 Railsでキーワード引数warning退治始まる、ライブラリとフレームワークの違い、ShopifyのRails高速化記事ほか
- 20200121後編 RubyKaigi 2020受付開始、RubyGemsとBundlerの今後、ファイル同期ツールMutagenほか
- 20200120前編 福岡でも公開つっつき会、Railsのconnection_specification_nameでprimaryという名前が非推奨に、structure.sqlとschema.rbほか
- 20200115後編 Ruby 2.7関連情報、Bootstrap 5は今年前半リリースか、PostgreSQLでやってはいけないリストほか
- 20200114前編 config_forのbreaking change、Active Storage variantをDBでトラッキング、SprocketsとWebpackの違いほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp Slackなど)です。