- Ruby / Rails関連
週刊Railsウォッチ(20210614前編)Pumaのgraceful restart、partial_writesコンフィグが非推奨化、Active Recordの楽観的ロックほか
こんにちは、hachi8833です。
🔗Rails: 先週の改修(Rails公式ニュースより)
今回は以下のコミットリストのChangelogを中心に見繕いました。
🔗 ActiveModel::AttributeSet#values_for_database
とActiveRecord::Base#attributes_for_database
が追加
つっつきボイス:「追加されたattributes_for_database
メソッドは、instantiate
してattributes
したものが元オブジェクトのattributes
に完全に等しくなる形で取るのに使えるようですね: シリアライズ <-> デシリアライズの等価変換保証のための機能という感じ」「お〜」「before_type_cast
だと生値かつreaderなので触れないから、シリアライズしたもので取れるメソッドを用意したということだと思います」
参考: APIドキュメント instantiate
-- ActiveRecord::Persistence::ClassMethods
要約: レコードを
instantiate
で再生成するときに使えるような、レコードの属性を返すメソッドが欲しかった。知っている限りではそういうメソッドがなかったので追加した。
今やっているシリアライズでは、レコードの属性を取り出してレコードの再作成に使えるような形でシリアライズを行おうとしている。その基準は、シリアライザを外部から見たときにMarshalと完全に同じように振舞うこと。
当初はfoo.attributes_before_type_cast
でシリアライズし、Foo.instantiate(attributes_before_type_cast)
でデシリアライズしてみるとjson属性で渡すキーがシンボル値のときに動かないことがわかった。foo.attributes_before_type_cast
は属性をシンボルキーで返すが、Marshal.load(Marshal.dump(...))
はjsonカラムがstring値のキーを持つインスタンスを返す。ここで問題なのは
attributes_before_type_cast
で、名前からもわかるように型キャストする前の属性を返しているが、自分たちはデータベース内にある属性をそのままinstantiate
に渡したい。このプルリクはそれ用の
attributes_for_database
を追加する。このプロパティは以下のように一意の形で元に戻せる。
Foo.instantiate(foo.attributes_for_database).attributes == foo.attributes
言い換えれば、このような属性を使えば元のレコードを完全に再作成できる。これはシリアライズで使うのに理想的。
それと合わせてActiveModel::AttributeSet#values_for_database
も追加した。こちらは値を実際に変換するときに適しているだろう。
同PRより大意
🔗 PostgreSQLのactive?
メソッドのSELECT 1
を空クエリに変更
# activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L293
def active?
@lock.synchronize do
- @connection.query "SELECT 1"
+ @connection.query ";"
end
true
rescue PG::Error
false
end
つっつきボイス:「SELECT 1
を空のクエリ(;
)に変えるとちょっとだけ速くなったそうです」「なるほど、active?
はPostgreSQLのコネクションが生きているかどうかを確認するためだけのメソッドだから、クエリを投げなくてもいいですよね」「言われてみればなるほど」「疎通確認ならこれでイケる👍」「コメントなしで;
だけ見ると一瞬バグかと思っちゃいそう」
「参考に張られているGitLab issueに一般的なベンチマークが載ってる↓」「たしかに速くなってますね」
# 同issueより抜粋
# SELECT 1の場合
progress: 30.0 s, 53157.6 tps, lat 0.150 ms stddev 1.418
progress: 60.0 s, 56619.2 tps, lat 0.141 ms stddev 0.022
progress: 90.0 s, 56882.5 tps, lat 0.141 ms stddev 0.010
progress: 120.0 s, 56631.8 tps, lat 0.141 ms stddev 0.027
# 空クエリの場合
progress: 30.0 s, 66476.7 tps, lat 0.120 ms stddev 0.010
progress: 60.0 s, 66723.3 tps, lat 0.120 ms stddev 0.024
progress: 90.0 s, 66661.8 tps, lat 0.120 ms stddev 0.010
progress: 120.0 s, 66596.9 tps, lat 0.120 ms stddev 0.024
🔗 partial_writes
コンフィグが非推奨化
つっつきボイス:「今後はpartial_inserts
やpartial_updates
を使うようにとのことです」「partial_writes
ってそもそも何を行うのかな?」「部分書き込みに関連してそう」「stackoverflowを見るとpartial_updates
メソッドはRails 4.1で削除されたとあるな」(しばらく探す)「メソッドがないなと思ったら、partial_writes
やpartial_updates
やpartial_inserts
はメソッドじゃなくてRailsのコンフィグなのか」「あ、コンフィグでしたか」「insertとupdateコンフィグでパーシャル書き込みの挙動を変えられるようにするためにpartial_writes
コンフィグを非推奨化にしたということのようですね」
これによってupdateとcreateで振舞いを変えられるようになる。
たとえば、コンカレントなマイグレーション実行によってカラムのデフォルト値が削除され、それによって古いスキーマを用いているプロセスがinsertに失敗するといった可能性を防ぐために、partial insertを無効にするのが望ましい状況が考えられる。
自分たちはまさにこの問題に遭遇したため、2015年から同様のパッチを走らせている。
partial_inserts
をデフォルトで無効にしておきたい気もするが、何か自分の知らないメリットがあるだろうか?
同PRより大意
参考: 3.7 Active Recordを設定する -- Rails アプリケーションを設定する - Railsガイド
config.active_record.partial_writes
: 部分書き込みを行なうかどうか(「dirty」とマークされた属性だけを更新するか)を指定する論理値です。データベースで部分書き込みを使う場合は、config.active_record.lock_optimistically
で楽観的ロックも使う必要がある点にご注意ください。これは、更新がコンカレントに行われた場合に、読み出しの状態が古い情報に基づいて属性に書き込まれる可能性があるためです。デフォルト値はtrueです。
railsguides.jpより
🔗 音声チャネル関連の改修
- PR: Add metadata value for presence of audio channel in video blobs · rails/rails@dd85734
- PR: Add audio analyzer to active storage by brenogazzola · Pull Request #42425 · rails/rails
つっつきボイス:「動画音声がらみの改修が2つありました」「動画に音声が入っているかどうかのmetadataと↓Analyzer::AudioAnalyzer
クラスが追加されたのね: 音声なしの動画もあるので、これを入れたのはわかる」
# activestorage/lib/active_storage/analyzer/video_analyzer.rb#L26
def metadata
- { width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio }.compact
+ { width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio, audio: audio? }.compact
end
🔗 Active Storageのsigned_id
にexpires_in:
キーワード引数を渡せるようになった
# activestorage/app/models/active_storage/blob.rb#L154
- def signed_id(purpose: :blob_id)
+ def signed_id(purpose: :blob_id, expires_in: nil)
super
end
参考: signed_id
-- ActiveStorage::Blob
つっつきボイス:「expires_in:
を指定できるようになったんですね: find_signed
で期限切れの日時を絞れるようになってる↓」「お〜、これうれしいかも」「いい改修だと思います👍」
# activestorage/test/models/attachment_test.rb#103
test "getting a signed blob ID from an attachment with a expires_in" do
blob = create_blob
@user.avatar.attach(blob)
signed_id = @user.avatar.signed_id(expires_in: 1.minute)
assert_equal blob, ActiveStorage::Blob.find_signed!(signed_id)
end
test "fail to find blob within expiration date" do
blob = create_blob
@user.avatar.attach(blob)
signed_id = @user.avatar.signed_id(expires_in: 1.minute)
travel 2.minutes
assert_nil ActiveStorage::Blob.find_signed(signed_id)
end
🔗Rails
🔗 Railsアプリのディレクトリ編成(Ruby Weeklyより)
つっつきボイス:「Railsを10年やってきた結果こういうディレクトリ構成に落ち着いたという感じの記事みたいです」「ざっと眺めた感じでは、Railsガイドとだいたい同じようなことを形を変えて書いているように見えますね」「特に変わったことはやってなさそうかも」
参考: Ruby on Rails ガイド:体系的に Rails を学ぼう
- concernは使っている。悪く言われることもあるけど、concernそのものではなく「よくないconcern」が問題なのだと思う(関連記事)。
- バックグラウンドジョブ: ジョブワーカーもコントローラ同様できるかぎり薄くしている(関連記事)。ワーカーにいろんなことをさせるのではなく、何かを呼ぶだけにとどめておくべきと信じている。
- JavaScriptはできるだけ使わないようにしている。使うときはStimulusでPOJO(plain old JavaScript Object)にすることが多い。
- テスト: RSpecを使っている。TDDの学習には随分時間をかけたが全然使ってない。テストのほとんどはmodel specとsystem spec。
- Service Object: 人気はあるようだけど、自分は使わない。普通にOOPでやっている。多くの人はService Objectを手続き的なコードとしてモデリングしているが、自分は宣言的なオブジェクトでモデリングしている(関連記事)。
同記事後半より大意
🔗 Pumaをgraceful restartする(Ruby Weeklyより)
つっつきボイス:「Pumaのgraceful restartにもいろいろあるよという記事のようですね」
- regular restarts: connections are lost, and new ones are established after booting the new application process
- hot restarts: connections are not lost but remain to wait for the new application server workers to boot
- phased restarts: current connections finish with old workers and new workers handle new ones
同記事より
「3番目のphased restartは、自分たちが一般的によく使う意味でのgraceful restartに相当するでしょうね: コネクションがないワーカーは即リスタートするけど、コネクションが生きているワーカーはコネクションが終了するまでリスタートしない」「そうそう、終わるまで待ってくれるリスタート」
「1番目のregular restartはコネクションが切れるタイプで、問答無用でリスタートする: これはgracefulではないでしょうね」「そう思います」「記事でも2番めと3番目をgracefulと呼んでました」
「2番目のhot restartは何だろう?」「アプリケーションサーバーの新しいワーカーが起動するまではコネクションを残す、現在のリクエストは終了させて新しいワーカーで同じものを再度動かそうとする、という感じみたい」「phased restartに近そう」「それぞれにシグナルがあるのね↓」
# 同記事より
$ kill -SIGUSR2 25197 # hot restartの場合
$ kill -SIGUSR1 25197 # phased restartの場合
「pumactl
でリスタート時にhotやphasedを指定できるともありますね」「へ〜」「他にも、tcp/9293をLISTENしてHTTPで叩けるcontrol-url
を使うと、/restart
や/phased-restart
にHTTPリクエストを送信してリスタートできる」「APIっぽい使い方もできるんですね」「Puma 1.2.2から--control
で同じことができたようですが、puma 5から--control-url
に変わったんですね」「最近のPumaに詳しくなれそうな記事👍」
「Webサーバー再起動の考え方は、制御の仕方こそ変わっても、ここ10年ほどアーキテクチャレベルでは大きくは変わっていない感じがしますね」「そうそう、Apacheが2.0だった頃を思い出します」「ApacheのMPMでも同じようなことやってた」
参考: マルチプロセッシングモジュール (MPM) - Apache HTTP サーバ バージョン 2.2
🔗 Hotwireスライド
つっつきボイス:「先週のウォッチ20210607で紹介した@willnetさんのHotwire記事↓に、元になったスライドがあったことに今頃気づきました😅」「これと同じかどうかわかりませんが、イベントでも見たような覚えありますね」
参考: まるでフロントエンドの“Rails” Hotwireを使ってJavaScriptの量を最低限に - ログミーTech
参考: クライアント側のJavaScriptを最小限にするHotwire - ログミーTech
「今さらですけどHotwireってJSのフレームワークなんですね」「そうですね、TurboはRailsと強く結合している感じはあります」
🔗 Active Recordのoptimistic lock(RubyFlowより)
つっつきボイス:「optimistic locが楽観的ロックで、pessimistic lockが悲観的ロックだったかな」「日本語だとわかった😅」「楽観的ロックは、ロックを取得できてもトランザクションが成功するとは限らない(実行開始後に競合して失敗する可能性がある)、悲観的ロックは、ロックを取得できればデッドロックやタイムアウトにならない限りトランザクションが成功する(ロックの取得に失敗する可能性はある)」「そうそう」
参考: 排他制御(楽観ロック・悲観ロック)の基礎 - Qiita
参考: APIドキュメント ActiveRecord::Locking::Optimistic
# 同記事より: ActiveRecord::StaleObjectErrorでエラーを出すようにした例
# PATCH/PUT /products/1 or /products/1.json
def update
respond_to do |format|
if @product.update(product_params)
format.html { redirect_to @product, notice: "Product was successfully updated." }
format.json { render :show, status: :ok, location: @product }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @product.errors, status: :unprocessable_entity }
end
rescue ActiveRecord::StaleObjectError => _error
@product.errors.add(:base, "Oops. Looks like the product has changed since you last opened it. Please refresh the page")
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @product.errors, status: :unprocessable_entity }
end
end
参考: APIドキュメント ActiveRecord::StaleObjectError
前編は以上です。
バックナンバー(2021年度第2四半期)
週刊Railsウォッチ(20210608後編)RubyでAppleのLZFSE圧縮データ解凍、AWS Lambda Extensionsが正式リリース、unixgame.ioほか
- 20210607前編 ActiveRecord::Relationのone?とmany?が高速化、RubyKaigi Takeout 2021登壇者募集開始ほか
- 20210601後編 Python使いから見たRuby、MySQLのインデックス解説、GitHubが採用したOpenTelemetryほか
- 20210531前編 RailsConf 2021の動画が公開、GraphQLのN+1を自動回避、Ruby 3のJITとRailsほか
- 20210525後編 Rubyのオブジェクトアロケーション改善、RubyKaigi Takeout 2021開催日発表、AWS App Runnerほか
- 20210524前編 Active Supportの知られてなさそうな機能5つ、RSpecの歴史、書籍『Practicing Rails』ほか
- 20210518後編 RubyのGCを深掘りする、Psych gemのbreaking change、11月のRubyConf 2021ほか
- 20210517前編 Bootstrap 5リリース、productionでSQLiteがwarning表示、rails-ujsの舞台裏ほか
- 20210511後編 AWS Lambda関数ハンドラをDSLで書けるyake gem、VPC Peeringが同一AZ転送量無料化ほか
- 20210511後編 AWS Lambda関数ハンドラをDSLで書けるyake gem、VPC Peeringが同一AZ転送量無料化ほか
- 20210510前編 属性メソッドをキャッシュして最適化、Railsのガバナンスに関する声明、bundle install高速化ほか
- 20210427後編 RactorでUDPサーバーを作る、JSONシリアライザalba gem、AppleのAirTagほか
- 20210420後編 ShopifyのJITコンパイラYJIT、PicoRuby、DynamoDBの3つの制約ほか
- 20210419前編 RailsのN+1クエリを定番以外の方法で修正する、GitLabのセキュリティ修正リリースほか
- 20210413後編 RubyMineのRBSサポートとCode With Me、GitHub ActionとDockerレイヤキャッシュほか
- 20210412前編 Active Record属性暗号化機能がRails 7にマージ、RailsNew.ioでrails newオプションを生成ほか
- 20210407後編 エイプリルフールのRuby構文プロポーザル、AWSのVPC Reachability Analyzerほか
- 20210406前編 GitHubが修正したRailsセッションハンドリングの競合、erb/haml/slimの速度比較ほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)