りばーさいだー

気ままに書いてみます

Railtieに積む位置を考える

tl;dr

  • after_initialize だと eager_load = true の場合にロードが遅すぎてシンタックスエラーやバリデーションエラーとなる場合がある
  • before_eager_load だと eager_load = false の時に評価されない
  • ActiveSupportlazy_load_hooks を使うと適切なタイミングで評価させることができる

やりたいこと

「デフォルトの Rails で valid として扱われるオプションを拡張するモンキーパッチを作る」というケースを想像します。

やりたいことはこんな感じ

# ActiveRecord::Associations::Builder::Association の オプションを拡張したい
def valid_options
  super + [:hoge]
end
# モンキーパッチをあてる
class Railtie < Rails::Railtie
  config.after_initialize do  # 良い感じの位置に積みたい
    ActiveRecord::Associations::Builder::Association.prepend(Hoge::Ext)
  end
end

どこに積むべきか考える

まずはこちらをご査収ください。

1) require "config/boot.rb" to setup load paths

2) require railties and engines

3) Define Rails.application as "class MyApp::Application < Rails::Application"

4) Run config.before_configuration callbacks

5) Load config/environments/ENV.rb

6) Run config.before_initialize callbacks

7) Run Railtie#initializer defined by railties, engines and application. One by one, each engine sets up its load paths, routes and runs its config/initializers/* files.

8) Custom Railtie#initializers added by railties, engines and applications are executed

9) Build the middleware stack and run to_prepare callbacks

10) Run config.before_eager_load and eager_load! if eager_load is true

11) Run config.after_initialize callbacks

ref: railties/lib/rails/application.rb

見ていただきたいのは 10)11)production だと eager_load = true になっているはずであり、after_initialize より前に eager_load! が走ることがわかります。

この例において、after_initialize のままだと eager_load = true の場合にロードされたタイミングではまだ Hoge::Extprepend されておらず、valid なオプションが拡張される前にモデルが評価され、エラーとなってしまう。

では、before_eager_load だとどうかと言うと Run if eager_load is true ということで、開発環境などの eager_load = false の環境でダメ。

だからと言って before_initialize では早すぎる気がする…どうすればいいんだ………

コードを読んでみよう

そもそも before_initialize ってどうやって実現されてるの?

# Second configurable block to run. Called before frameworks initialize.
def before_initialize(&block)
  ActiveSupport.on_load(:before_initialize, yield: true, &block)
end

なんだか ActiveSupport#on_load ってのが本体らしいですね。こいつがどこで定義されているか調べてみると active_support/lazy_load_hooks.rb というファイルで定義されています。

ドキュメントが詳しく書かれているので見てみましょう。

Here is an example where +on_load+ method is called to register a hook.

ruby initializer 'active_record.initialize_timezone' do ActiveSupport.on_load(:active_record) do self.time_zone_aware_attributes = true self.default_timezone = :utc end end

When the entirety of +activerecord/lib/active_record/base.rb+ has been

evaluated then +run_load_hooks+ is invoked. The very last line of

+activerecord/lib/active_record/base.rb+ is:

ruby ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base)

これこれ! これがやりたかったんですよ~。

#on_load の中身は「既にロード済みであればすぐに実行、そうじゃないならロードされたタイミングで実行」するように定義されています。 hook を仕掛けられる側のモジュール(仮に HookTarget とする)にて、ActiveSupport.run_load_hooks(:hook_name, HookTarget) とすることで、 :hook_name という名前に対して積まれた hooks が一斉に実行されていきます。

ActiveRecord::Base のコードを見てみると、様々なモジュールを extend, include した後、最後に ActiveSupport.run_load_hooks(:active_record, Base) と書かれています。 つまり、 ActiveSupport.on_load(:active_record) とすることで ActiveRecord::Base が読み込まれた直後に実施する処理を定義することができます。

これを踏まえて再定義

# モンキーパッチをあてる
class Railtie < Rails::Railtie
  initializer 'active_record.valid_option_ext' do
    ActiveSupport.on_load(:active_record) do  # 良い感じの位置に積めた
      ActiveRecord::Associations::Builder::Association.prepend(Hoge::Ext)
    end
  end
end

おわりに

かなりスッキリ解決できて満足です。

しかしなかなかやりたいこと(Rails にモンキーパッチを当てる)の内容から lazy_load_hooks という名前にたどり着けませんね。

この記事で今後モンキーパッチを当てる人の役に立てば嬉しいです。

tmux v2.4 以降はコピーモード設定が変更された

tl;dr

  • 新しい環境をセットアップしていた
  • いつもの dotfiles が使えなかった
  • tmux v2.3 -> v2.4 で設定のしかたが大きく変わっていたので変える必要がある
# こんな感じ
- bind -t vi-copy v begin-selection
+ bind -Tcopy-mode-vi v send -X begin-selection

きっかけ

新しい環境を用意していました。

当然のようにいつもの dotfiles で initialize したら tmux がうまく動かなかった。どうやら一部の bind-key の構文がうまく動いていないようだった。

エラーを追う

usage: bind-key [-cnr] [-T key-table] key command [arguments]

なんか bind の構文が変わったのかな。

とりあえず自分の tmux のバージョンを知っておく。

$ tmux -V
tmux 2.6

あれ、そんなにバージョンの数字大きかったっけ…調べてみよう。

tmux のリポジトリでチェンジログ( tmux/CHANGES )から grep してみると ココがどうもそれっぽい。 いくつかのキーテーブルに後方互換の無い変更が行われたらしい。

今回関係がある部分だけ抽出すると、

The emacs-copy and vi-copy tables have been replaced by the copy-mode and copy-mode-vi tables. Commands are sent using the -X and -N flags to send-keys.

ということだった。親切にも例を書いてくれているので見てみる。

# So the following:

  bind -temacs-copy C-Up scroll-up
  bind -temacs-copy -R5 WheelUpPane scroll-up

# Becomes:

  bind -Tcopy-mode C-Up send -X scroll-up
  bind -Tcopy-mode WheelUpPane send -N5 -X scroll-up

なるほどね。 hoge-copy というテーブルが消えて copy-mode に置き換わり、 bind してから send -X で実現するように修正されたと。

では例に習って自分の設定も修正してみる。

- bind -t vi-copy v begin-selection
+ bind -Tcopy-mode-vi v send -X begin-selection

幸せになれました。

付録

ちょっとググっても新しい tmux での vi キーバインドによるコピーモード設定が見つからなかったので共有。

# 選択開始: v
- bind -t vi-copy v begin-selection
+ bind -Tcopy-mode-vi v send -X begin-selection

# 行選択: V
- bind -t vi-copy V select-line
+ bind -Tcopy-mode-vi V send -X select-line

# 矩形選択: C-v
- bind -t vi-copy C-v rectangle-toggle
+ bind -Tcopy-mode-vi C-v send -X rectangle-toggle

# ヤンク: y
- bind -t vi-copy y copy-selection
+ bind -Tcopy-mode-vi y send -X copy-selection

# 行ヤンク: Y
- bind -t vi-copy Y copy-line
+ bind -Tcopy-mode-vi Y send -X copy-line

# ペースト: C-p
- bind C-p paste-buffer
+ bind-key C-p paste-buffer

ペーストは好みが別れるバインドかもしれない。

誰かの参考になれば良いな。

Rails5.0 から ActiveRecord::Relation#slice が使えなくなる話

tl;dr

  • 4.2 までは ActiveRecord::Relation#method_missing で一部メソッドを Arraydelegate していた
  • 5.0 からはその処理が削除され、その代わりに Enumerable を include するように修正された
  • そのため、ほとんどのメソッドはそのまま使えるが、 #sliceEnumerable に存在しないため、使えなくなった

きっかけ

某システムのバージョンアップ中、変な箇所で落ちた。

内容は ActionView::Template::Error : undefined method `slice' for <Hoge::ActiveRecord_Relation:0x0000> というもの。

最初は、そんなわけあるか? と思いながら rails のコードを読みにいったのでした。

ブラックリストは管理しづらい

早速 railsリポジトリに飛び、 compare しながら 該当のコミットを探しだし 、コメントを要約すると以下のようなものでした。

mutaion methods( #compact! とか #sort! とか )をブラックリストで管理しているのは非網羅的であり、 Ruby のバージョンごとに差分をキャッチアップしていく必要がある。

そう、4.2 までは Array のメソッドの中でも mutation methodsブラックリストとして定数で管理していたのでした。

  BLACKLISTED_ARRAY_METHODS = [
    :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!,
    :shuffle!, :slice!, :sort!, :sort_by!, :delete_if,
    :keep_if, :pop, :shift, :delete_at, :select!
  ].to_set # :nodoc:

当然運用が面倒で、Ruby のバージョンと Rails のバージョンの依存関係も複雑になることは容易に想像がつきます。

そして Enumerable を include したことにより(結果的に)複雑な運用から手を引けるようになった、と。なるほど。

深淵に落ちていった #slice メソッド

待て待て、いやいやおかしいじゃないかと、そう思いますよね。

うんだって、 ActiveRecord::Relation で普通に #[] メソッドは使っているはずだし、このメソッドは機能として落とされることは考えづらい。

にも関わらず #slice が使えないなんてどういうこと…? 、と。 ※ Array では #slice#[]エイリアス

では 見ていきましょう

  delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join,
    :[], :&, :|, :+, :-, :sample, :reverse, :compact, to: :to_a

... え?

いやこれ、#[]#to_adelegate されてますよね。 #to_a@records で、 Array ですよね??

もしかして忘れられてそのまま落っこちていった、そういうことなんですか…?

おわりに

とまあ、真相は闇の中ですがとにかく、 #slice は使えなくなっちまいましたよと、そういうことですはい。

僕からは「 delegate するメソッドも結局 Ruby のバージョンと完全に切り離して考えることもできないんじゃないの(管理むずいんじゃないの)」というツッコミだけ残して終わりにしておきます(とはいえ良さげな解決策もパッとは思いつかないけど)。

ではこのへんで、ありがとうございました。

protect_from_forgery と callback の関係を調べてみた

発端

某システムにて Rails のバージョンアップをする機会がありまして。

調べてみると、 Rails 5.0 からは protect_from_forgeryデフォルト挙動が変わる と言うじゃないですか。

内容は

protect_from_forgeryのprependのデフォルトをfalseに変更。

ですか。つまり、

今までは controller のどこに protect_from_forgery と書いても callbacks の一番上に積まれていたのが、デフォルトではそうではなくなる

ということですか。

なるほど、これは取り急ぎは既存のままの挙動と合わせたいぞ、という気持ちになりますね。

というのが調べる発端でした。

見てみる

そもそも 4.2 ではどうなんでしょうか。Rails のソースを見てみましょう

def protect_from_forgery(options = {})
  self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
  self.request_forgery_protection_token ||= :authenticity_token
  prepend_before_action :verify_authenticity_token, options
  append_after_action :verify_same_origin_request
end

なるほど、特別 options には手を加えずに prepend_before_action を呼んでいる、と。

じゃあ こいつは何してんですか というと

define_method "prepend_#{callback}_action" do |*names, &blk|
  _insert_callbacks(names, blk) do |name, options|
    set_callback(:process_action, callback, name, options.merge(:prepend => true))
  end
end
alias_method :"prepend_#{callback}_filter", :"prepend_#{callback}_action"

なるほど。

ちょっとメタメタしいですが、上の before_action の definition と見比べるにどうやら options.merge(:prepend => true) した上で before_action しているだけのようですね。

4.2 はわかりました。じゃあ 5.0 は どうなんですか

def protect_from_forgery(options = {})
  options = options.reverse_merge(prepend: false)

  self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
  self.request_forgery_protection_token ||= :authenticity_token
  before_action :verify_authenticity_token, options
  append_after_action :verify_same_origin_request
end

options.reverse_merge(prepend: false) ですか。prepend が無ければデフォルトで false になるんですね。

で、あとはその options でそのまま before_action していると。

結論

少し冗長ですが 4.2 のうちに protect_from_forgery prepend: true と書いてしまっておいて問題ないですね。

この変更が無いと好きな順番で protect_from_forgery を差し込みたい、という時に困りそうなので、良い変更ですね。


いやはやそれにしても、この 5.2 beta2 が出たというタイミングで焦って 5.0 へのアップデートをしているサービスなんてあるんでしょうかね。滑り込みアウトってところですよね。

そんなタイミングで書いてもアレな記事ネタですが、protect_from_forgery にちょっと自信がついたので良いとします。

今後もちょいちょい書いていければと思います、よろしくどうぞ。