今作っているシステムで、データにインポート処理があるのだけれど、それをコマンドラインからじゃなくて画面からできるようにしたかった(俺以外の人もできるように)。
それは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が気になるでまた調べてみようと思う。
