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