🚀

Capistranoで動的な値をセットするときはProcを渡そう

2020/10/20に公開

Rails のデプロイで使われることの多い Capistrano、こんな感じで設定ファイルを書きますよね:

config/deploy.rb
# If the environment differs from the stage name
set :rails_env, 'staging'

# Defaults to :db role
set :migration_role, :db

# Defaults to the primary :db server
set :migration_servers, -> { primary(fetch(:migration_role)) }

capistrano/rails から引用しました)

このサンプルコードにも含まれていますが #set にはシンボルや文字列だけでなく Proc を渡すこともできます。
Procを渡したときはそうでないときと少し違う挙動になるので気を付けよう、という話です。

#set に Proc 以外を渡したときの挙動

set :rails_env, 'staging'
set :migration_role, :db

ここでセットした値は #fetch を呼ぶことで他の部分でもアクセスすることができます。
そのときに返ってくる値は #set に渡した値そのもの('staging':db)です。

#set に Proc を渡したときの挙動

set :migration_servers, -> { primary(fetch(:migration_role)) }

ここでは :migration_servers というキーに -> { primary(fetch(:migration_role)) } という Proc が値としてセットされています。

この Proc はすぐに評価されるわけではなく、キーが #fetch されたときに初めて評価されます。
そのため :migration_role に値をセットするのが後になっても参照されて動作します。

set :migration_servers, -> { primary(fetch(:migration_role)) }
set :migration_role, :db

fetch(:migration_servers) # このタイミングで初めて Proc が評価されて値がセットされる

評価されるタイミングが遅くなるのは Proc だからです。
↓ の例のように文字列 & 式展開をした場合は先ほど 'staging' を渡したときと同様にすぐに値がセットされてしまいます。

set :migration_servers, "#{primary(fetch(:migration_role))}" # migration_roleはまだセットされていない = nilとして扱われる
set :migration_role, :db

fetch(:migration_servers) # set :migration_serversしたときの値が返ってくる

動的な値をセットするときは Proc を渡そう

上の例のように、他にセットした値を使って動的に値を作る場合は Proc を渡すと安全です。
値がセットされる順番に変に依存したコードにならないで済むためです。

例えば capistrano/rbenv の README にはこのような例が載っています。

config/deploy.rb
set :rbenv_prefix, "RBENV_ROOT=#{fetch(:rbenv_path)} RBENV_VERSION=#{fetch(:rbenv_ruby)} #{fetch(:rbenv_path)}/bin/rbenv exec"

ここでは rbenv_pathrbenv_ruby を使って rbenv を実行するときのコマンドを定義しています。
README に載っている定義通りに値をセットすれば問題なく動作しますが...
何かの拍子(設定ファイルの一部を切り出したなど)に rbenv_ruby がセットされるタイミングが rbenv_prefix よりも遅くなってしまった途端に動かなくなってしまいます。

こうした不具合を避けるために、順番に依存しないコードにしたい場合は次のようなコードにしましょう。

set :rbenv_prefix, -> { "RBENV_ROOT=#{fetch(:rbenv_path)} RBENV_VERSION=#{fetch(:rbenv_ruby)} #{fetch(:rbenv_path)}/bin/rbenv exec" }

(おまけ)Capistrano のコードを追ってみる

Capistrano の #setCapistrano::Configuration::Variables に定義されています。
要はハッシュである valueskey に渡された value をセットしているだけです。

lib/capistrano/configuration/variables.rb
def set(key, value=nil, &block)
  @trusted_keys << key if trusted? && !@trusted_keys.include?(key)
  remember_location(key)
  values[key] = block || value
  trace_set(key)
  values[key]
end

#fetch の定義は こちら
Internal use only な #peek というメソッドへと処理が移譲されています。

lib/capistrano/configuration/variables.rb
def fetch(key, default=nil, &block)
  fetched_keys << key unless fetched_keys.include?(key)
  peek(key, default, &block)
end

そして #peek の定義は こちら
value#callable_without_parameters? な値のときは #call を呼んで、その値をセットしています。

lib/capistrano/configuration/variables.rb
# Internal use only.
def peek(key, default=nil, &block)
  value = fetch_for(key, default, &block)
  while callable_without_parameters?(value)
    value = (values[key] = value.call)
  end
  value
end

すなわち

  • #set でセットされた値が #callable_without_parameters? なときは #fetch されたときに #call された値が返る
  • #set でセットされた値が #callable_without_parameters? ではないときはその値をそのまま返す

という挙動になります。

ちなみに #callable_without_parameters?#call メソッドを実装している && 引数が0個」という判定をしています。
Method#arity なんてこのコードリーディングをしていたときに初めてみました... 👀 )

自作の Class を作って #call メソッドを実装すれば #callable_without_parameters? に当てはまります。
しかしこの用途ではほとんどの場合 Proc を使うだろうことを踏まえて、冒頭から Proc に限った話にしていました。

Discussion