7839

雑草魂エンジニアブログ

【Rails】ControllerからRakeタスクを実行する

バックエンド側で定期実行しているRakeタスクをフロントエンド側から任意のタイミングで実行したくなった。そのために、RailsアプリケーションのControllerからRakeタスクを実行する方法を検討したので備忘録として残しておく。

Rakeタスクとは

Rake とはRuby製のビルドプログラム(UNIXのビルドプログラム"Make"のようなビルドプログラム)で、プログラム実行を”タスク”という単位でまとめて扱うことができる。

Rake is a Make-like program implemented in Ruby. Tasks and dependencies are specified in standard Ruby syntax.

github.com

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にはrailsrakeという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タスクを実行する」パターンは緊急対応用で、利用頻度が多くないと想定しているのでこのまま進めることにした。

参考記事

stackoverflow.com