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

週刊Railsウォッチ(20191209前編)Pumaのphased-restartとUnicornのgraceful restart、RailsのTZハックが不要になった話ほか

こんにちは、hachi8833です。ChainerがPyTorchに乗り換えられたそうです。


つっつきボイス:「ついさっき上のツィートが流れてきました🛶」「Chainerといえばメジャーな機械学習フレームワークだったのに」

参考: Chainer を振り返って

「中の人のブログを見るとメンテナンスモードに移行するってあるし😳」「機械学習やってる人には結構大きな変更でしょうね〜」

「Rubyで機械学習やる人は最近増えてきましたけど、RubyからPythonのライブラリを呼べるpycall.rbで有名なmrknさんがSciRubyというプロジェクト↓をやっていますね」「おぉ」「pycallはRubyからPythonライブラリ経由でPythonコードにアクセスして、Python側で作ったデータにRubyからアクセスできるようにするというものですね: 同じメモリ内でやるのでデータをメモリコピーしなくていい😋」


sciruby.comより

「mrknさんは最近だとデータ処理系のApache Arrow↓もやってたりしますね(ウォッチ20190402)」


arrow.apache.orgより


  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • 毎月第一木曜日に「公開つっつき会」を開催しています: お気軽にご応募ください

今回は週刊Railsウォッチ第17回公開つっつき会を元にお送りいたします。定員枠を初めて広げました。お集まりいただいた多くの皆さま、ありがとうございました!😂🙇

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

ActiveSupport::Duration#inspectの結果がおかしいのを修正

# 同PRより
(1.day / 24).inspect #=> "0 seconds" # inspectの結果がおかしい
(1.day / 24).to_i #=> 3600
(1.day / 24) == 1.hour #=> true
(1.hour).inspect #=> "1 hour"
(1.hour).parts #=> {:hours=>1}
(1.day / 24).parts #=> {}

つっつきボイス:「Active SupportのDuration周りのバグだったようです」「ActiveSupport::Durationは、名前のとおり期間を抽象化したものですね」

assert_equal "3600 seconds", (1.day / 24).inspect

「追加されたテストコード↑を見ると、1.day / 24inspectしたら3600秒になるべきだったのに0秒が返ってたのか〜」「他にもいくつか類似の修正PRがありましたので上に貼っておきました」「Durationとしては分でも秒でも同じ長さのはずなんでしょうけど、内部状態の更新あたりが微妙に失敗してたのかもしれませんね😅」「単位が絡むと面倒そう...」

# activesupport/lib/active_support/duration.rb#L377
def inspect #:nodoc:
- return "0 seconds" if parts.empty?
+ return "#{value} seconds" if parts.empty?

parts.
sort_by { |unit, _ | PARTS.index(unit) }.
map { |unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}" }.
to_sentence(locale: ::I18n.default_locale)
end

「Railsでは時刻から時刻を引くとDurationになるんでしたっけ?」「そうそう、Time同士の差を取るとActiveSupport::Durationオブジェクトになりますね☺️」

Rails5: ActiveSupport::Durationでの数値へのパッチ

redirect_to.action_controllerの通知のペイロードにActionDispatch::Requestを追加した

| Key | Value |
| ----------- | ----------------------------- |
| `:status` | HTTP response code |
| `:location` | URL to redirect to |
| `:request` | The `ActionDispatch::Request` |

つっつきボイス:「これはredirect_toの引数が増えたのかと思ったらinstrumentが出てきてるからログ周りの話か」「あ、なるほど」「instrumentationはログとは少し違いますが、redirect_to.action_controllerのinstrumentationでrequestも拾えるようになったということだと思います☺️」

# #L
def redirect_to(*)
- ActiveSupport::Notifications.instrument("redirect_to.action_controller") do |payload|
+ ActiveSupport::Notifications.instrument("redirect_to.action_controller", request: request) do |payload|
result = super
payload[:status] = response.status
payload[:location] = response.filtered_location
result
end
end

RailsのInstrumentationを「バス」で理解する

「Instrumentation(測定、計測といった意味)について簡単に説明しますね: Railsではpub/sub(パブリッシャー/サブスクライバ)的な方法でInstrumentationが使えるんですけど、上のようなパブリッシャーをサブスクライバで拾うような形になります」

「pub/subってどう説明するのがいいかな...いわゆる差し込みフリータイプの電源タップ↑では電源プラグをどこにいくつ挿してもよくて、誰も使っていなくても電気が常に流れていますけど、コンピュータなんかにおける『バス』↓という概念もそれに似ていて、pub/subはそういうバスで考えるとわかりやすいと思います」「おぉ〜」

参考: バス (コンピュータ) - Wikipedia


Wikipedia-jaより

「Instrumentationではパブリッシャー(pub)がこういうバス的なところにいます: バスに誰もいなくても大勢いいてもお構いなしで」「そしてそのバス的なところにサブスクライバ(sub)が登録されていればそいつらがpubからメッセージを拾える、というような形になります」「ふむふむ」「おおむねそんな感じだったと思います😆」

「InstrumentationはRailsのログ出力にとても良く使われる仕組みですね: いろんなレベルのログを片っ端から出力すると遅くなるので、ログレベルに応じて自分の欲しいものだけをサブスクライブすることで、負荷を上げずにログを柔軟に取れるようにしているわけです」「Railsフレームワークのコードでinstrumentって出てきたらそこがログに出せるポイントになっているということです」


はみ出し:「そういえばInstrumentationっていう用語はWMI↓で初めて知ったんですけど、長ったらしくて収まりが悪いせいか未だに日本語にもカタカナにもなってなくて、Railsガイドでも英語表記で統一しました」「pub/subの方がしっくりくるな〜😆」「instrumentって楽器?」「元々は機材とか装置みたいな意味なんですけど楽器の意味もあって、musical instrumentと書けば確実に楽器ですね」

参考: Windows Management Instrumentation - Wikipedia

service_urlが非推奨化、今後はurl

Variant#service_urlPreview#service_urlを非推奨化に。
今後はBlobと一貫するurlを使う
同PRより大意


つっつきボイス:「Active Storageの改修だそうです」「VariantPreviewってのがあるのね😳」「Active Storageをまだ使ってない身としてはそうですか〜としか言えないし😆」「差分を見た感じではservice_が冗長だったってことなんでしょうね☺️」「使う人が増える前にメソッド名直しちゃえという感じなのかも☺️」「service_以外のurlがないならurlだけでええやろってことかな😆」

「Active StorageはRails 6でrails newするなら使ってもいいんじゃないかと思いますね: 前のRailsから移行してActive Storageにも移行するのは大変そうですけど😆」

URIかURLか

「めちゃ細かいこと言うとurlでいいんだろうかと🤣」「uriじゃないのかと😆」

「もともとUniform Resource Locatorの略でURLですけど、もう随分前に『本来やりたかったのはlocaltorみたいな物理的な場所ではなく識別子(identifier)だよな』ってURI(Uniform Resource Identifier)を使うべきという空気になったのに、未だにURLが広く使われているという🤣」「URL消えませんね🤣」「もう滅びない感🤣」

「ただRFCではURLもURIも一応両方あると思います☺️」「そうでしたか😳」「でURIがURLを包含していた気がする...この辺かな↓」「URIの方が意味が広いんですね」「URLは//:@:/↓みたいなよく見かける形」

//:@:/

「でURIはtel:+1-816-555-1212みたいなのも含む↓」「これはURIだけどURLではないと」「気になる人はこの辺のRFCを読んでみるといいと思います☺️ 」

The following example URIs illustrate several URI schemes and variations in their common syntax components:
ftp://ftp.is.co.za/rfc/rfc1808.txt
http://www.ietf.org/rfc/rfc2396.txt
ldap://[2001:db8::7]/c=GB?objectClass?one
mailto:John.Doe@example.com
news:comp.infosystems.www.servers.unix
tel:+1-816-555-1212
telnet://192.0.2.16:80/
urn:oasis:names:specification:docbook:dtd:xml:4.1.2

参考: Uniform Resource Identifier (URI): 一般的構文 -- RFC3986 日本語訳の複製

URI は、それが位置指定子か、名前か、あるいはその両方かという点において、更に分類できる。 "Uniform Resource Locator" (URL) という用語は、リソースを識別するのに加えて、その主なアクセスメカニズム (例えば、そのネットワーク上の "位置") を記述する事によってリソースの場所を見つける方法を提供するような、URI の部分集合を指す。 "Uniform Resource Name" (URN) という用語は、例えそのリソースが存在しなくなったり、あるいは利用不可能になっても全体において一意で永続的である事が要求される "urn" スキーム [RFC2141] の下での両方の URI、また名前の特性を持つあらゆる他の URI を参照するために歴史的に使用されている。
triple-underscore.github.ioより

HashWithIndifferentAccess#convert_valueのアロケーションを削減

productionのプロファイリングでActiveSupport::HashWithIndifferentAccess#convert_valueがかなり高いようなので調べたところ、妙なbindingアクセスを見つけた。
履歴を調べたところ、#36758@64a4301あたりが由来らしい。
元々、キーワード引数をホットスポットで用いることでアロケーションを削減するのが狙いだったが、forがキーワードなのでbinding.local_variable_getでしのいでいた。
しかしbindingは呼び出しのたびに新しくBindingをアロケートするのでかなり遅くなる。
convert_valueはprivateメソッドなので、引数をリネームするだけでこの辺りの問題を回避できると思われる。
同PRより大意

# activesupport/lib/active_support/hash_with_indifferent_access.rb#L365
private
...
- def convert_value(value, for: nil) # :doc:
- conversion = binding.local_variable_get(:for)
-
+ def convert_value(value, conversion: nil) # :doc:
if value.is_a? Hash
if conversion == :to_hash
value.to_hash
else
value.nested_under_indifferent_access
end
elsif value.is_a?(Array)
if conversion != :assignment || value.frozen?
value = value.dup
end
- value.map! { |e| convert_value(e, for: conversion) }
+ value.map! { |e| convert_value(e, conversion: conversion) }
else
value
end
end

参考: ActiveSupport::HashWithIndifferentAccess


つっつきボイス:「お、HashWithIndifferentAccess出たな😆」「😆」「便利なんですけど、とにかく名前長い😆: IDEなら補完が効くからまあいいんですけど」

ここでいったん録音が途切れました🙇

「...binding.local_variable_getはいわゆるリフレクション的なアプローチで、本来のオブジェクト指向的なアクセスではなく、今動いているRubyのプロセスからアクセス権限とか全無視で無理やりぶっこ抜けます😆」「😆」「こういうRubyインターナルなものはやってて楽しいんですけど、やりすぎるといろいろぶっ壊れます😇」

参考: リフレクション (情報工学) - Wikipedia

「改修の方は今時間なくてちゃんと見てませんが、binding.local_variable_getみたいな方法で速くしていたのが裏目に出て遅くなるのはよくあるパターンかも☺️」

ネストしたconfig_forハッシュへの非シンボルアクセス(非推奨)を削除

# railties/lib/rails/application.rb#L222
def config_for(name, env: Rails.env)
if name.is_a?(Pathname)
yaml = name
else
yaml = Pathname.new("#{paths["config"].existent.first}/#{name}.yml")
end

if yaml.exist?
require "erb"
- config = YAML.load(ERB.new(yaml.read).result) || {}
- config = (config["shared"] || {}).merge(config[env] || {})
+ config = YAML.load(ERB.new(yaml.read).result, symbolize_names: true) || {}
+ config = (config[:shared] || {}).merge(config[env.to_sym] || {})

ActiveSupport::OrderedOptions.new.tap do |options|
- options.update(NonSymbolAccessDeprecatedHash.new(config))
+ options.update(config)
end
else
raise "Could not load configuration. No such file - #{yaml}"
end
rescue Psych::SyntaxError => e
raise "YAML syntax error occurred while parsing #{yaml}. " \
"Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
"Error: #{e.message}"
end

つっつきボイス:「これはdeprecatedな機能を削除したのね」

Railsのdeprecationプロセス

「ちょっと解説すると、基本的にRailsでは何かの機能をなくすときに、いきなり消すのではなくて、まずdeprecation warningをログに出力するコードを追加して、次のメジャーバージョンアップで実際に消すという形で進めていきます」「みんなはそれを見て、消される前に対応することになりますね」

connected_toのキーワード引数databaseを非推奨化(代替なし)

connected_toのキーワード引数databaseはシャーディングのキーとして想定されていない場合に大量のバグが発生する。databaseキーワード引数が使われているテストとユースケースを検討した結果、これは削除すべきという結論に達した。
今後シャーディングをサポートする計画はあるが、それまでdatabaseキーワード引数は意味なしとなる。コネクションを新たに作成するアプリではestablish_connectionconnects_toを利用できる。
connected_todatabaseキーワード引数をリクエスト中または使い捨てでないコネクションで使うと、databaseというキーでバグの原因となる。
同PRより大意

# activerecord/lib/active_record/connection_handling.rb#L98
def connected_to(database: nil, role: nil, prevent_writes: false, &blk)
+ if database
+ ActiveSupport::Deprecation.warn("The database key in `connected_to` is deprecated. It will be removed in Rails 6.2.0 without replacement.")
+ end
+
if database && role
raise ArgumentError, "connected_to can only accept a `database` or a `role` argument, but not both arguments."
elsif database
if database.is_a?(Hash)
role, database = database.first
role = role.to_sym
end
db_config = resolve_config_for_connection(database)
handler = lookup_connection_handler(role)
handler.establish_connection(db_config)
with_handler(role, &blk)
elsif role
if role == writing_role
with_handler(role.to_sym) do
connection_handler.while_preventing_writes(prevent_writes, &blk)
end
else
with_handler(role.to_sym, &blk)
end
else
raise ArgumentError, "must provide a `database` or a `role`."
end
end

つっつきボイス:「シャーディングでうまくいかないことがあったみたいです」「a lot of bugsですか😆」

「ちょっと説明すると、Rails 6ではマルチプルデータベース機能が入ったんですけど、たとえばマスターとレプリカがある場合にconnected_toを使ってそのブロックの中でだけ一時的に接続先を変更することができるようになっていて、そこがシャーディングで不具合が起きてたということですね☺️」

「シャーディングをどのレイヤでやっているかにもよりますね: 最近のPostgreSQLだとデータベースレベルでシャーディングができたりしますし」「Railsは6.1でシャーディングをサポートするつもりらしいのでそのときに考えるということのようです」

「Railsでシャーディングをやりたいことって結構あるんでしょうか?」「それはもうめちゃありますよ: Railsでやるかどうかは別として、シャーディングしないとやっていけないようなユースケースはいろいろあります☺️」「なるほど」「まあシャーディングをgemでやるのかRailsの機能に入れるのかというのは議論の余地がありそうですけど、ユーザーがどれだけいるかわからないようなAction MailboxもRailsに機能として入ってくるぐらいなので、たぶんシャーディングもRailsに入るんでしょうね☺️」

シャーディングについて

「シャーディング(sharding)はデータベースでは水平分割とも言われる手法ですね: 雑に説明すると、たとえばIDが奇数のデータベースとIDが偶数のデータベースを用意しておくと、IDで検索したときにきれいに分散アクセスできる、みたいな感じです」「おぉ」「ソシャゲ開発の全盛期にはシャーディングがよく使われていましたけど最近あまり聞きませんね: 単に普及して当たり前になったのか、実はつらくて止めたのかは知りませんけど😆」

参考: 分割 (データベース) - Wikipedia
参考: シャーディング - Qiita

はみ出し: キーワード引数とRails

「以下は最初上のconnected_toのキーワード引数の話かな?と思ってつっつきで引用してみましたが、別の箇所でした😅」「最近話題の、Ruby 2.7でキーワード引数周りのbreaking changesに絡んだ話ですね☺️」「こちらの翻訳記事をどぞ↓」

Ruby 2.7: ハッシュからキーワード引数への自動変換が非推奨に(翻訳)

Rails

RailsのTZハックが不要になった話


つっつきボイス:「これはTwitterで見かけましたね☺️」「2本立ての記事で、TZ環境変数を設定したらRailsが速くなったけど実はRuby 2.6で解決されていたのでハック不要だったというお話」「おぉ」「これはいい話でしたね〜😋」「はてブでも結構上がってたかも」

jnchitoさんの振り返り記事

ここと次の記事で録音が途切れていました🙇。

Webpackerでやらかしがちなミス

# 同記事より
app/
javascript/
packs/
application.js
components/ # lots of files
images/ # lots of files
stylesheets/ # lots of files
...

つっつきボイス:「WebpackerのOverpackingってわかる😆」「とってもありそう😆」「詰め過ぎ💼」

シンプルなルール: Webpackerのpacksディレクトリのファイルのうち、対応する記述がアプリケーションのjavascript_pack_tagにないものはoverpackingである。
同記事より抜粋

Rails 6+Webpacker開発環境をJS強者ががっつりセットアップしてみた(翻訳)

Rails 6の細かな新機能をひたすら試す記事


qiita.comより


つっつきボイス:「ひとつひとつの記事は短いですけど、Rails 6のリリースノートに載っているかいないかみたいな細かい機能をひたすら試していてびっくりしました😳」「これは凄いな〜👍」「110件目って...」

今年の4月からずっとやってるんですね😳。

Pumaのphased-restart


つっつきボイス:「Pumaサーバーにphased-restartというのがあるってこれで初めて知りました」「Unicornサーバーのgraceful restartとは別なのかな?Pumaだとphased-restartをするとダウンタイムなしでコードを再読み込みできるということか」


puma.ioより
* サイト: unicorn: Rack HTTP server for fast clients and Unix

「サーバーの再起動周りは何かとややこしい😅」「日本語記事の方を見た感じではPumaのphased-restartはmaster processを使い回し、新しいworker processはサーバー起動時のmaster processからforkされるとある: これだけ見るとUnicornのgraceful restartと同じような感じ?🤔」「Unicornのgraceful restartは、たしかmaster processが死ぬんだった覚えがありますね🤔」「お〜、そうでしたっけ?」「だったはずです」「ということはPumaのphased-restartはUnicornのとは違うということなんでしょうね」

Railsサーバーの再起動について

「Unicornサーバーの再起動はあんまりいい思い出がないんですが😭、ちょっとだけこの辺の話をしますね」「はい〜😋」

「RailsのWebサーバーとして使われるUnicornやPumaは、起動するとまずmaster processが立ち上がって、外からのリクエストはたしかいったんmaster processが受けてそこからworker processをforkしてそこに振る形になります」「おぉ」

「こういうサーバーの再起動で何が問題になるかというと、上の記事にもあるように環境変数の更新なんですね😆: ご存知のとおり、Linuxでは親プロセスが死ぬとそこからforkした子プロセスも死ぬ仕組みになっているんですが、トランザクショナルな処理を実行中のworkerがいるときにmasterを殺してしまうと処理中のworkerも死んでデータが不整合になってしまうわけです😅」

「その辺を回避する仕組みがたとえばUnicornのgraceful restartで、中途半端な状態でworkerが殺されないようにいい感じに処理中のworkerを避けて落としていって、処理中のworkerがいなくなったら晴れて新しい環境に入れ替わることになってるんですが、実際には期待どおりに動かないこともあって😅」「😅」

「一般にサーバーの再起動は重たい処理なので、とてもリクエスト数の多いRailsサーバーをカジュアルに再起動するとその間サービスは止まりますし、再起動でメモリがいったん全解放されてしまうのでサービス復帰まで時間かかります」「他にも、リクエストの多いRailsサーバーを普通に再起動するとリクエストのキューが大量にたまって、復帰したけどリクエストをさばききれなくなってサーバーが繰り返し落ちるなんてこともあったりします😇」「このあたりはH2Oの作者のブログなどに詳しく載っていたと思います↓」

その他Rails

先ほど見つけたツィートです。


前編は以上です。

バックナンバー(2019年度第4四半期)

週刊Railsウォッチ(20191204後編)Rubyコードをトランスパイルするruby-next、Cloud Run正式リリース、2019年Web年鑑レポート、V言語ほか

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

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

Rails公式ニュース

Ruby Weekly


CONTACT

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