はじめに
rakeタスク実装に当たり、dry_run
がなぜ必要なのか、どんな仕組みで機能しているのかをすぐに腹落ちさせることができなかったので、今回は記事にすることで理解を深めようと思い、書いています。
dry_runとは?
まず、前提としてプログラム(処理)を実行することをrunといいます。
dry_runとは、プログラミングやシステムの用語で、「本番の処理を実行せずに、その過程をシミュレーションする」ための方法や、フラグを指します。
dry_runの目的
dry_runは実際にデータベースや外部に影響を与えずに、処理が正常に動作するかを確認できるので、本番作業へ移る前に欠かせないステップです。
かなりの量のデータを更新するrakeタスクを実装した場合、それをすぐに本番環境で実施するのはリスキーです。
意図しないデータまで書き換わってしまったり、思ってたよりも影響範囲が狭かったりなど。一か八かでコマンドを押すようなギャンブルはやめようということで、事前に処理をシミュレーションできる(ログ)、dry_runを利用していきます。
dry run(シミュレーション)モード
- 実際のデータの更新や変更は行われない
- その処理が実行されるときにどのデータがどのように変わるか、標準出力やログで確認できるようにすることで、実際の処理を実行する前に動作確認を行うことができる
実装方法
実装コード
以下は、dry_run
を取り入れたRakeタスクの一例です
メインロジック
module Patches
class Example
attr_reader :run_mode
def initialize(run_mode: "dry_run")
@run_mode = run_mode
end
def execute
update_examples
end
private
def dry_run?
@run_mode != "run"
end
def update_examples
Rails.logger.info("更新対象のexample数:#{count.example}件")
examples.each do |example|
Rails.logger.info("id: #{example.id}の処理を開始します")
unless dry_run?
begin
example.update!(status:"hoge")
Rails.logger.info("id: #{example.id}のstatusがhogeになりました")
rescue => e
Rails.logger.error("id: #{example.id}の更新に失敗しました")
end
end
end
end
end
end
Rakeタスクの定義
namespace :patches do
desc "Example Rake Task"
task :update_example, ['run_mode'] => :environment do |t, args|
Patches::Example.new(run_mode: args.run_mode).execute
end
end
コードの詳細解説
dry_run
の機能を実際にはどうやって実装しているのか、パーツを確認していきます。
attr_reader
run
するかdry_run
するかの判断をするためにrun_mode
をセットします。
attr_reader :run_mode
attr_reader
はインスタンス変数に対して「読み取り」を可能にするアクセサーメソッドを自動で定義します。今回は@run_mode
というインスタンス変数に対して、run_mode
(ゲッター)というメソッドを提供します。attr_reader
は外部からの不正なデータ変更を防ぐため、インスタンス変数を変更させたくない状況の時に使えるメソッドです。
参考
-
attr_accessor
: インスタンス変数への外部からのアクセス(参照・代入)を可能にする
インスタンス変数に対して「読み取り」と「書き込み」を可能にするアクセサーメソッドを自動で定義する。
@name
というインスタンス変数に対して、name
(ゲッター),name=
(セッター)という2つのメソッドを提供する。 -
attr_reader
:外部からの不正なデータ変更を防ぐため、インスタンス変数を変更させたくない状況の時に使える。 -
attr_writer
:外部から値は変更できるが、読み取りを禁止したい場合に用いられる(パスワードなど)
def initialize(run_mode: "dry_run")
def initialize(run_mode: "dry_run")
@run_mode = run_mode
end
initialize
メソッドのrun_mode
引数にデフォルト値として"dry_run"
が設定されています。そのため、run_mode
を明示的に指定しない場合、dry_run
モードで動作します。
def execute
def execute
update_hoge
end
update_hoge
メソッドを実行するためのメソッドです。
Patches::example
クラスのインスタンスを作成して、execute
メソッドを呼び出すことが今回のパッチ処理のスイッチみたいなものです。
def dry_run?
def dry_run?
run_mode != "run"
end
run_mode
の引数として渡されてきた値が、"run" の時だけ、false
を返すメソッドです。この判定のおかげで dry_run
が実装できます。
Rails.logger
を活用したログ出力
loggerはログを出力するために、Railsにあらかじめ用意されている機能です。
Railsのloggerの機能は、ActiveSupport
クラスを継承したLogger
クラス(ActiveSupport::Loggerクラス
)を利用しています。
※Rails.logger
の後につけるものはログの種類によって分けますが、詳しくはこの記事では言及しません。
Rails.logger.info("hogehogeって言います!")
=> hogehogeって言います!
以下のように、ログを処理の前に用意することで実行対象の件数を確認することができます。
Rails.logger.info("id: #{example.id}の処理を開始します")
#以下の処理はrun_modeがrunの時だけ実行される
unless dry_run?
begin
#更新処理を実行
example.status.update!("hoge")
Rails.logger.info("id: #{example.id}のステータスが#{example.status}に変更されました")
rescue StandardError => e
#エラーが発生した場合
Rails.logger.error("id: #{example.id}の更新に失敗しました")
end
end
dry_runの時に確認したいのが、処理実行前のこのコードです。
Rails.logger.info("id: #{example.id}の処理を開始します")
このログは unless dry_run?
の真偽に関係なく実行されるので、本番作業を行う前に処理対象が正しいものかどうかを(期待値通りか)確認するのに活躍してくれます。
run_modeがrun
の時は以下の処理が実行されますが、
unless dry_run?
begin
#更新処理を実行
example.status.update!("hoge")
Rails.logger.info("id: #{example.id}のステータスが#{example.status}に変更されました")
rescue StandardError => e
#エラーが発生した場合
Rails.logger.error("id: #{example.id}の更新に失敗しました")
end
end
その時もロガーを用意することで処理がうまく行ったかどうかを追跡することができます。
Rakeタスク
task update_example, ['run_mode'] => :environment do |task, args|
Patches::Example.new(run_mode: args.run_mode).execute
end
rakeタスクは以上のように定義したものをコマンドで呼び出すことで実行します。
Rakeタスクでは、引数はすべて文字列として渡されます。
なぜかというと、コマンドラインで入力される値はすべてテキスト形式(文字列)で処理されるからです。 そのため引数も文字列として扱われるわけです。
正しく文字列として認識されるために明示的に''
クォートで囲ってあげてます。
|task, args|
-
task
:タスクオブジェクトは、タスクに関する情報や操作を提供するRake特有のオブジェクト。タスクを実行する際に、ブロックの引数として渡されるもので、通常、t
という名前で使用されているが、基本的に使う機会はないっぽいです。 -
args
:タスクに渡された引数(arguments)を保持するタスク引数オブジェクトで、args.run_mode
のように指定してあげることで特定の引数を取り出すことができます。
ただし、Rakeは第1引数にタスクオブジェクトを受け取り、第2引数にタスク引数を受け取ることになっているので、args
にしか興味がなくても|t,args|
と書いてあげましょう。
コマンド操作
そして、rakeタスクを書き終わったら、あとはコンソールでコマンド実行してあげるだけです。rake
に名前空間:タスク名
そして、引数[]
を受け取ります。
rake patch:update_example[run以外]
rake patch:update_example
# run以外、もしくは指定なしの場合はdry_runで実行されます。
[run]
と渡す以外ではdry_run
が実行されます。
Patches::UpdateExample.new(run_mode: "dry_run").execute
実際にrunを実行したいときは引数でrun_modeに値を渡してあげることで、明示的に指示してあげます。
rake patch:update_example[run]
をコマンドで実行すると、Patches::UpdateExample
クラスのインスタンスに"run"が引数として渡されるので、Rakeタスクファイルで以下の処理ができあがります。
Patches::UpdateExample.new(run_mode: "run").execute
最後に
最後までお読みいただきありがとうございました。
どうやってdry_run処理が実現されているのか、もしくはPatch処理ができているのかを理解する助けになれば幸いです。