【Rails】ControllerからRakeタスクを実行する
バックエンド側で定期実行しているRakeタスクをフロントエンド側から任意のタイミングで実行したくなった。そのために、RailsアプリケーションのControllerからRakeタスクを実行する方法を検討したので備忘録として残しておく。
- Ruby:v3.0.1
- Ruby on Rails : v6.1.3.2
Rakeタスクとは
Rake とはRuby製のビルドプログラム(UNIXのビルドプログラム"Make"のようなビルドプログラム)で、プログラム実行を”タスク”という単位でまとめて扱うことができる。
Rake is a Make-like program implemented in Ruby. Tasks and dependencies are specified in standard Ruby syntax.
RailsにおけるRakeタスクの使い方としては、wheneverというGemと組み合わせて定期実行したい処理を定義したり、他にも様々な用途で利用することができる。
今回、私も定期実行でIoTデバイスの監視を実施したくてwheneverとRakeタスクの組み合わせでシステムを構築した。
Rakeタスク使い方
Rakeタスクファイルの作成
rails g コマンドで「devices」という名のrakeタスクを作成する。
$ rails g task devices
lib/tasks以下のディレクトリにdevices.rakeというタスクファイルが生成される。
Rakeタスクの定義
基本的な実装
namespace :タスクのファイル名 do desc 'タスクの説明' task :タスクの名称 do # タスクの処理内容 end end
namespace :devices do desc '全てのデバイスのステータス確認' task :all_check => :environment do Rails.logger.info("device all check") devices = Device.all devices.each do |device| device.check() end end desc '任意のデバイスのステータス確認(id指定)' task :check, ['device_id'] => :environment do |_, args| Rails.logger.info("device check, device_id = #{args.device_id}") target_device = Device.find(args.device_id) target_device.check() end end
- RakeタスクでActiveRecordを扱う場合、taskに
:environment
オプションをつける必要がある - Rakeタスクで引数を複数受け取る場合、スペースは開けずにカンマ区切り
- NG : rake sample_task:execute[arg1, arg2, arg3]
- OK : rake sample_task:execute[arg1,arg2,arg3]
- 少しハマったので注意!!
Rakeタスクから別のタスクを呼び出す場合、
invoke(*args)
で実行するRake::Task['sample_task:execute'].invoke(*args) # ex. Rake::Task['devices:check'].invoke(39)
Rakeタスクの実行
Railsにはrails
とrake
という2つのコマンドがあるが、Rails 5以降はrails
コマンドで全て実行できるように統一されている。(Rails 4以下は、コマンドの種類によって2つのコマンドを使い分ける必要があるので注意。Rails 5以降でも、rake
コマンドはもちろん実行できる。)
Rakeタスクの一覧表示
$ rails -T # ~省略~ rails devices:all_check # 全てのデバイスのステータス確認 rails devices:check[schedule_id] # 任意のデバイスのステータス確認(id指定)
Rakeタスクの実行
$ rails devices:all_check $ rails devices:check[39]
ControllerからRakeタスクを実行する
いよいよ今回の本題である。
Controllerでフロントエンドからのリクエストを受け付けて、Rakeタスクを実行する。IoTデバイスのステータスを確認するために、実際のデバイスのステータスを取得したり、整合性の確認をしたりするので最大30秒ほどかかってしまう。複数台のステータスを確認する場合、デバイスの台数分だけ処理に時間がかかってしまう。そのため、リクエストを受け付けて、Rakeタスクを発行したら、処理の完了を待たずにレスポンスを返せるようにしないと、504 (Gateway Time-out)が出てしまう。
試験的にタスクの処理にsleep 30
を追加した。
namespace :devices do desc '任意のデバイスのステータス確認(id指定)' task :check, ['device_id'] => :environment do |_, args| Rails.logger.info("device check, device_id = #{args.device_id}") target_device = Device.find(args.device_id) sleep 30 #試験的に追加 end end
%記法を使ってコマンド出力
コマンドを出力する方法は以下が挙げられる。
- system("cmd")
- %x("cmd")
- Kernel#exec("cmd")
今回は、%記法を用いて、バックグラウンド実行になるように末尾に&
を入れてみた。
class DevicesController < ApplicationController def update %x(rake device:check[#{params[:id]}] &) render status: 200, json: schedule end end
しかしながら、%x("cmd")
はサブシェルで実行したコマンド結果が返り値となり、プロセスの終了を待ち合わせてしまった。
実際に実行してみると、案の定、504 (Gateway Time-out)が出てしまった。
spawn
一時的に別スレッドを立ち上げて実行できないかと探していた時にspawn
を見つけた。spawn
は子プロセスを起動するメソッドであり、引数で渡したコマンドを実行することができる。また、親プロセスは生成した子プロセスの終了を待ち合わせない。
class DevicesController < ApplicationController def update spawn("rake device:check[#{params[:id]}]") render status: 200, json: schedule end end
実際に実行してみると、APIリクエストを送信してから即座にレスポンスが返ってきた。そして、Rakeタスクも実行された。
まとめ
無事に「ControllerからRakeタスクを実行する」ことができた。この利用パターンはかなりイレギュラーな気がしている。
また、正直同時に大量のプロセスを立ち上げると、メモリを圧迫しすぎてしまうことがあるので安易に使うことはできないと思えた。ただ、今回の「ControllerからRakeタスクを実行する」パターンは緊急対応用で、利用頻度が多くないと想定しているのでこのまま進めることにした。