2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rakeタスクにdry_runを組み込む理由と実装方法

Last updated at Posted at 2024-12-09

はじめに

rakeタスク実装に当たり、dry_runがなぜ必要なのか、どんな仕組みで機能しているのかをすぐに腹落ちさせることができなかったので、今回は記事にすることで理解を深めようと思い、書いています。

image.png

dry_runとは?

まず、前提としてプログラム(処理)を実行することをrunといいます。

dry_runとは、プログラミングやシステムの用語で、「本番の処理を実行せずに、その過程をシミュレーションする」ための方法や、フラグを指します。

dry_runの目的

dry_runは実際にデータベースや外部に影響を与えずに、処理が正常に動作するかを確認できるので、本番作業へ移る前に欠かせないステップです。

かなりの量のデータを更新するrakeタスクを実装した場合、それをすぐに本番環境で実施するのはリスキーです。

意図しないデータまで書き換わってしまったり、思ってたよりも影響範囲が狭かったりなど。一か八かでコマンドを押すようなギャンブルはやめようということで、事前に処理をシミュレーションできる(ログ)、dry_runを利用していきます。

dry run(シミュレーション)モード

  • 実際のデータの更新や変更は行われない
  • その処理が実行されるときにどのデータがどのように変わるか、標準出力やログで確認できるようにすることで、実際の処理を実行する前に動作確認を行うことができる

実装方法

実装コード

以下は、dry_runを取り入れたRakeタスクの一例です


メインロジック

lib/pathces/update_examples
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タスクの定義

lib/tasks/example.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名前空間:タスク名そして、引数[]を受け取ります。

タスクの実行方法(dry_run)
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に値を渡してあげることで、明示的に指示してあげます。

タスクの実行方法(run)
rake patch:update_example[run]

をコマンドで実行すると、Patches::UpdateExampleクラスのインスタンスに"run"が引数として渡されるので、Rakeタスクファイルで以下の処理ができあがります。

Patches::UpdateExample.new(run_mode: "run").execute

最後に

最後までお読みいただきありがとうございました。
どうやってdry_run処理が実現されているのか、もしくはPatch処理ができているのかを理解する助けになれば幸いです。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?