RailsでJenkinsのジョブの経過みたいに処理経過を表示したかった。

今作っているシステムで、データにインポート処理があるのだけれど、それをコマンドラインからじゃなくて画面からできるようにしたかった(俺以外の人もできるように)。
それはrake taskをコントローラーから呼び出せばいいのかなと思ったのだけれど、途中経過をJenkinsみたいに表示できたらなぁと思って調べていた。

かずさんから、Rails3.1からstreamingができるようになってるという話を聞いたので、まずはそれを確認したところ、それはそこまで難しくなかった。
ただし、streamingを使う場合はWebrickではできないらしい。ローカルでunicornを起動しないといけないのでお手軽さが少々欠ける。

unicornのインストール・設定・起動

unicornはGemfileに記述してからbundle installすればいい。あとはconfig/unicorn.rbを記述する。
unicorn.rb

# coding: utf-8
listen 3000, :tcp_nopush => false
worker_processes 4

bundle installしてからunicornを起動する。

bundle install # Gemfileに gem 'unicorn'を記述すること
unicorn_rails --config-file config/unicorn.rb # unicornを起動

サーバサイド側の実装

streamingをするためのコントローラーの記述はそこまで複雑ではなかった。
参考にしたサイト

# coding: utf-8
require 'pty'

class ImportsController < ApplicationController
  authorize_resource class: false # Deviseでログインしていることを確認
  layout :layout # 

  def index
  end

  def hoge
    render_stream do |r|
      # rake taskでimport:hogeを作ってたとする
      # rake import:hogeで逐次表示される内容をstreamで出力する
      PTY.spawn("rake import:hoge") do |stdin, stdout, pid|
        begin
          stdin.each do |line|
            r << line
          end
        rescue Error::EIO
        end
      end
    end
  end


  private
  def render_stream(&block)
    # streamingで出力するための処理をここでまとめてる。
    headers["Cache-Control"] ||= "no-cache"
    headers["Transfer-Encoding"] = "chunked"
    self.response_body = Rack::Chunked::Body.new(Enumerator.new do |r|
      block.call r
    end)
  end

  def layout
    if action_name == 'index'
      'application'
    else
      false
    end
  end
end

クライアント側の実装

import_hoge_pathにアクセスすれば、徐々に結果が表示されることは確認できたのだが、Ajaxで取得しにいったときの処理方法がなかなかわからなかった。
jQueryを使ってAjaxをした場合、successやcompleteのタイミングで処理をするとストリーミングにならない。全部出力した後にコールバックされるからだ。
どうすればストリーミング的に処理できるかを調べたら、参考になるサイトがあったので載せておく。

jQueryの$.ajaxで通信途中のresponseTextを取得する : あらびき日記

CoffeeScriptで関数を実装したらこんな感じ。
setIntervalで定期的にresponseTextを確認して、追加された分だけ抽出して#import_resultに結果を出力している。

ajax_with_stream = (method, url) ->
  $('#import_result').html('')
  $.ajax({
    type: method,
    url: url,
    xhrFields: {
      onloadstart: ->
        xhr = this
        length = 0
        line_count = 0
        $.mytimer = setInterval(->
          if length != xhr.responseText.length
            length = xhr.responseText.length
            lines = xhr.responseText.split("\n")
            for i in [line_count..lines.length-1]
              line = lines[i]
              if line
                $('#import_result').append line + "\n"
            line_count = lines.length - 1
        , 400)
    },
    success: ->
      console.log 'finished!!'
      setTimeout('clearInterval($.mytimer)', 400)
  })

これでいけたわーと思ったのですが、rake taskの処理が重たすぎたりすると、Unicornがタイムアウトしてしまって、結果が表示されたりされなかったりと誤差が出るようになってしまった…。ものすごく重たい処理を同期処理でサーバに表示させながらやるにはちょっとダメかもしれん…と思いながら、急場をしのぐためにunicornのtimeoutを600に変更した。そしたらうまくいったけど何かが違う。そうじゃない。

悩んでたら、delayed_jobやsidekiqでやるのがいいんじゃない?とtwitterのTLで助言をいただいたのでそちらを参考にしてみようかと思う。Jenkins的に結果を出したいのは、できればいいなーという感じだったので。
あとstreamingを使うために開発環境までunicornにするのは面倒だしなぁ…というのもある。sidekiqが気になるでまた調べてみようと思う。


タグ Ruby, Ruby on Rails | パーマリンク.

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です