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

週刊Railsウォッチ(20210614前編)Pumaのgraceful restart、partial_writesコンフィグが非推奨化、Active Recordの楽観的ロックほか

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

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

今回は以下のコミットリストのChangelogを中心に見繕いました。

🔗 ActiveModel::AttributeSet#values_for_databaseActiveRecord::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_insertspartial_updatesを使うようにとのことです」「partial_writesってそもそも何を行うのかな?」「部分書き込みに関連してそう」「stackoverflowを見るとpartial_updatesメソッドはRails 4.1で削除されたとあるな」(しばらく探す)「メソッドがないなと思ったら、partial_writespartial_updatespartial_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より

🔗 音声チャネル関連の改修


つっつきボイス:「動画音声がらみの改修が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_idexpires_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に詳しくなれそうな記事👍」

puma/puma - GitHub

「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と強く結合している感じはあります」

HotwireはRailsを「ゼロJavaScript」でリアクティブにできるか?前編(翻訳)

🔗 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ほか

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

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

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h


CONTACT

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