irohiroki's blog

Ruby, Rails, and Web technologies

Tech

Unicornのホットデプロイについて会社の方に書きました

去年の9月からKRAYで働いているのですが、そちらにエントリ書きました。

RailsのデプロイとUnicornのトラブルシューティング | KRAY Inc

UnicornのしくみはUnixらしくてきれいです。よかったら読んでみてください。

Published on 18/01/2012 at 15h10 under .

Rack middlewareで発生する例外をrescueする

Rack middlewareで発生する例外とは

アプリケーションで発生する例外を捕捉し、適切な処理をするのは一般的なことだと思います。コントローラの中で発生する例外はbegin rescueで囲ったり、rescue_fromを使えば捕捉できますが、Rack middlewareで発生する例外はどうでしょうか?

例えばMySQLを使っていてデータベースサーバに接続できない場合、ActiveRecord::ConnectionAdapters::ConnectionManagementというmiddlewareからMysql2::Errorが発生します。

ActiveRecord::ConnectionAdapters::ConnectionManagementはrake middlewareすると10番目に出てきます。

$ rake middleware
  use ActionDispatch::Static
  use Rack::Lock
  use ActiveSupport::Cache::Strategy::LocalCache
  use Rack::Runtime
  use Rails::Rack::Logger
  use ActionDispatch::ShowExceptions
  use ActionDispatch::RemoteIp
  use Rack::Sendfile
  use ActionDispatch::Callbacks
  use ActiveRecord::ConnectionAdapters::ConnectionManagement
  use ActiveRecord::QueryCache
  use ActionDispatch::Cookies
  use ActionDispatch::Session::CookieStore
  use ActionDispatch::Flash
  use ActionDispatch::ParamsParser
  use Rack::MethodOverride
  use ActionDispatch::Head
  use ActionDispatch::BestStandardsSupport
  run MyApp::Application.routes

ウェブサーバはこの上にあり、自分のアプリケーションは一番下です。例外はrescueしなければ上へ上がって行きます。つまり、どうやってもアプリケーションではrescueできません。

ではどうハンドリングすればいいでしょうか?

ActionDispatch::ShowExceptionsを使う?

スタックを見ると上から6番目にActionDispatch::ShowExceptionsというのがあります。ソースを見ると、例外のクラス名をステータスコードにマップしていることがわかります。

例えば、Mysql2::Errorを507 (Insufficient Storage)にマップするだけなら、config/application.rbに下の行を追加し、

ActionDispatch::ShowExceptions.rescue_responses['Mysql2::Error'] = :insufficient_storage

表示したいページをpublic/507.htmlとして置いておけば済みます。

しかしMysql2::Errorの原因が全てInsufficient Storageではありませんし、MySQLが無関係なInsufficient Storageも有りうるので、適切な措置とは言えません。

ActionDispatch::Rescue to the... you know, rescue!!

Rack middlewareで発生した例外はActionDispatch::Rescueというmiddlewareで捕捉できます。ActionDispatch::ShowExceptionsと違い、ActionDispatch::Rescueは例外クラスに対してハンドリングするRackアプリケーションを指定できます。

例えば、application.rbに下のように追加すれば、Mysql2::Errorに対して「Database failure.」と表示できます。

config.middleware.insert_before ActiveRecord::ConnectionAdapters::ConnectionManagement, ActionDispatch::Rescue do
  rescue_from Mysql2::Error, lambda {|env| [500, {'Content-Type' => 'text/plain'}, 'Database failure.'] }
end

ステータスコードは500 (Internal Server Error)で十分でしょう。ポイントは、レスポンスボディとして任意の内容を返せることです。任意の内容を返せるということは、ユーザに適切なメッセージを見せたり、JavaScriptのフロントエンドにJSONなどで適切なステータスを返したりできるということです。

なお、上のrescue_fromはActionDispatch::Rescue#rescue_fromで、コントローラのマクロ風rescue_fromとは別物です。

application.rbに書きたくない?

しかし、前出の

lambda {|env| [500, {'Content-Type' => 'text/plain'}, 'Database failure.'] }

はapplication.rbに書くにはちょっと生々しい印象です。もっとコードらしい場所へ移動するにはどうしたらいいでしょうか。

ActionDispatch::Rescue#rescue_fromの第2引数はRackアプリケーションですから、Rackアプリケーションとして振舞うクラスを指定してやれば済みます。例えばActionController::Metalを継承すればそういうクラスを簡単に作れます。

class DatabaseFailure < ActionController::Metal
  def self.call(env)
    action(:respond).call(env)
  end

  def respond
    self.status = 500
    self.content_type = 'text/plain'
    self.response_body = 'Database failure.'
  end
end

application.rbには下のようになります。

config.middleware.insert_before ActiveRecord::ConnectionAdapters::ConnectionManagement, ActionDispatch::Rescue do
  rescue_from Mysql2::Error, DatabaseFailure
end

Published on 15/06/2011 at 23h25 under . Tags

irb (Readline)で日本語が通るようにする

MacにRVMでRuby 1.9.2をインストールしたとき、irbなどのReadlineのプロンプトに日本語を入力すると「???」に化けて、そのままEnterするとinvalid multibyte charなどのエラーになる問題の解決方法。

Bug #550によると、MacではGNU readlineの代わりにEditline Libraryが使われているそうで、GNU readlineを使えば解決するらしい。

RVMには簡単にGNU readlineを導入する機能があって、Rubyをインストールした後に修正する手順は下の通り(追記:一度Rubyを消さないとうまく行かないので修正。リンク先の丸写しです)。

rvm package install readline

rvm remove 1.9.2

rvm install 1.9.2 --with-readline-dir=$rvm_path/usr

cd $HOME/.rvm/src/ruby-1.9.2-p180/ext/readline

ruby extconf.rb -- --with-readline-dir="$HOME/.rvm/usr"

make install

なお、2行目のruby-1.9.2-p180の部分はインストールしたRubyのバージョンに合わせて変えてください。

追記

GNU readlineにしたら、Readline.refresh_lineがNotImplementedErrorになりました。refresh_lineを使う場合には向きませんね。

Published on 22/03/2011 at 15h25 under . Tags , ,

RSpecを2ヶ月くらい使って気づいた事と分からない事

RSpec勉強会@万葉で発表してきたので、資料を公開します。

RSpecを2ヶ月くらい使って気づいた事と分からない事

資料の22ページで、subjectのためにdescribeを切ることについて「議論ありそう」と書いたんですが、やっぱり@ukstudioさんからツッコまれました。

曰く、describeは振る舞いの主体を表すもので、処理の結果を指すものではないでしょうとのこと(と理解した)。確かにそうですね。

しかし同じcontextでsubjectを変えると書きやすくなる場合もやっぱりあって、この問題はまだまだ楽しめそうです。

Published on 08/03/2011 at 01h05 under . Tags ,

Autotest, guard-spork, and ruby-debug

Quick link to a sample app.

Autotestはテスト駆動開発において欠かせないツールです。しかし、特にRuby 1.9ではRspecの起動の遅く、イライラしている人も多いでしょう。

Rspecの起動を早くするツールにSporkがありますが、以下のような問題があります:

  • 製品コードを更新してもリロードしてくれない(ぇ
  • ruby-debugが使えない

このエントリではこれらの問題を解決していきます。目指すのは次のような環境です:

  • appの下はもちろん、configの下を更新した場合もリロードしてテストしてくれる
  • 製品コードでもテストコードでも、debuggerと書いたらruby-debugが使える

Spork

まずはベースの環境から作りましょう。なお、ここで使うのはMac上のRails 3.0.3とRuby 1.9.2です。

gem install rails
rails new my_app

最初にRSpecとautotest、sporkを入れるために、Gemfileに以下の行を加えます。

gem 'rspec-rails'
gem 'autotest-rails'
gem 'spork'

Gemfileを編集したらbundleしておきます。

bundle

RSpecをインストールします:

rails g rspec:install

Sporkに必要な準備をします:

spork --bootstrap

ここで、表示される指示に従ってspec/spec_helper.rbを編集します。下のようにしておけばよいでしょう。

require 'rubygems'
require 'spork'

Spork.prefork do
  ENV["RAILS_ENV"] ||= 'test'
  require File.expand_path("../../config/environment", __FILE__)
  require 'rspec/rails'

  Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}

  RSpec.configure do |config|
    config.mock_with :rspec
    config.fixture_path = "#{::Rails.root}/spec/fixtures"
    config.use_transactional_fixtures = true
  end
end

Spork.each_run do
end

またSporkを有効にするため、rspec:installによって生成された.rspecファイルに--drbを加えておきます。

--color --drb

これで素のAutotestとSporkの環境はできました。適当なコードをscaffoldして試してみてもいいでしょう。

rails g scaffold user name:string
rake db:migrate RAILS_ENV=test

sporkを起動し、別のターミナルでautotestも実行します。

spork
autotest

さらに別のターミナルでテストコードを失敗するように修正するとすぐさまautotestが赤くなります。しかし、製品コードを壊した場合、テストは走りますが緑のままです。

リロードさせる

製品コードのリロードは、実は次の2つの問題に分けられます。

  1. app下のファイルを修正した時
  2. それ以外

1はRuby on Rails Tutorial: Learn Rails by Example | Ruby on Rails 3 Tutorial book and screencasts | Static Pagesに回避方法が載っています。まず下のようにspec/spec_helper.rbのRSpec.configureのブロックに1行加えます。

diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 9fd4d4a..0d793e4 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -12,6 +12,8 @@ Spork.prefork do
     config.mock_with :rspec
     config.fixture_path = "#{::Rails.root}/spec/fixtures"
     config.use_transactional_fixtures = true
+
+    ActiveSupport::Dependencies.clear
   end
 end
 

そしてconfig/application.rbも下のように修正します:

diff --git a/config/application.rb b/config/application.rb
index 590b85c..02b3ca4 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -38,5 +38,13 @@ module MyApp
 
     # Configure sensitive parameters which will be filtered from the log file.
     config.filter_parameters += [:password]
+
+    ### Part of a Spork hack. See http://bit.ly/arY19y
+    if Rails.env.test?
+      initializer :after => :initialize_dependency_mechanism do
+        # Work around initializer in railties/lib/rails/application/bootstrap.rb
+        ActiveSupport::Dependencies.mechanism = :load
+      end
+    end
   end
 end

これでapp以下を修正した場合でもリロードしてテストしてくれます。

app以外、例えばconfig/application.rbやconfig/initializersの中は、rspecの起動を速くするためにプリロードしているので、sporkの再起動で解決します。これを自動的にやってくれるのがguard-sporkです。まずguard-sporkとFSEvent APIのRubyバインディングであるrb-fseventをインストールします。ただしここで使うguard-sporkは、後述するruby-debugのために私が改造したものです。

gem 'guard-spork', :git => 'https://github.com/irohiroki/guard-spork.git'
gem 'rb-fsevent'

加えたらbundleしてguard-sporkの初期化をします。

bundle
bundle exec guard init spork

そしてguardを起動。

bundle exec guard start

sporkはguardが起動してくれます。config/application.rbなどを更新するとguardがsporkを再起動します。

ところでguardが監視してくれるファイルの中にはconfig/routes.rbがないのですが、これは下の修正をすることでテストに反映できるようになります。

diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 6f6a223..cffc9f5 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -19,4 +19,5 @@ Spork.prefork do
 end
 
 Spork.each_run do
+  MyApp::Application.reload_routes!
 end

ruby-debugを使う

この時点では、rdebugを起動してコードの中にdebuggerと書いても止まってくれません。なぜなら、実際のテストはDRbサーバであるsporkで動いているからです(たぶん)。

この問題に対して、sporkの作者のTim Harper氏がrdebugのセッションをフォワードするプロキシを書いてくれました。これを使うと、debuggerが現れたときにsporkのターミナルにrdebugのプロンプトが出ます(autotestのターミナルではない)。ただし、一箇所RuntimeErrorが出るところがあるので、ここでは私の修正版を使います。まずcurlなどでプロキシモジュールをspecディレクトリに入れます。

curl https://gist.github.com/raw/814375/ccbe163559376e109e30574b1086a32fb626e374/spork-ruby-debug.rb > spec/spork-ruby-debug.rb

そしてそれをspec/spec_helper.rbからrequireします。

diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 0d793e4..6f6a223 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,5 +1,6 @@
 require 'rubygems'
 require 'spork'
+require File.expand_path("../spork-ruby-debug", __FILE__)
 
 Spork.prefork do
   ENV["RAILS_ENV"] ||= 'test'

もちろんruby-debugもインストールしておきましょう。Gemfileに下の行を加えてbundleします。

gem 'ruby-debug19'
bundle

これで完成です。bundle execを付けるのを忘れずにguardを起動してください。

bundle exec guard start

実はオリジナルのguard-sporkだとsporkのプロセスがbackgroundへ回されてしまってrdebugのプロンプトが取れないのですが、そこは私の修正版で回避してあります。

以上の手順で作った環境をgithubに置きましたのでよろしければ参考にしてください。

Published on 08/02/2011 at 05h16 under . Tags ,

OmniAuthでTwitter認証を作りインテグレーションテストも書く

(このエントリは、第58回 Rails勉強会@東京で発表した内容をまとめたものです。)

OmniAuthは、TwitterやGoogleなど様々な認証サービスプロバイダを統一したインターフェースで使えるようにしてくれるgemです。非常に便利なのでさっそく使おうとしたのですが、テストの書き方がわからなくて躓いたので、調べてわかったことやサンプルコードを公開します。

なお、OmniAuthは単体でも使えるのですが、伝統的なユーザ名とパスワードによる認証もサポートすることを想定して、Deviseと併用する構成となっています。

ポイントは以下の通りです:

  • TwitterはOAuth 1.0
  • 認証の過程でTwitterに3回のHTTPリクエストが飛ぶ
  • それらをスタブで受ける

発表で使った資料を下に貼っておきます。最低限のことしか書かれていませんが、参考になれば。

Published on 18/12/2010 at 07h09 under . Tags , , ,

Rails 3のroutesまとめ

Rails 3ではroutesのDSLが完全に刷新されました。特に、あの見難かったハッシュの塊が解かれて、:member:getなど予約語の役割をしていたシンボルはディレクティブになりました。例えばRails 2の以下の記述は

map.resources :users, :member => {:foo => :get}, :collection => {:bar => :post}

Rails 3では下のように書けます:

resources :users do
  member do
    get :foo
  end

  collection do
    post :bar
  end
end

このように、Rails 2では"予約語"がキーになったり(:member:collection)値になったり(:get:post)していたものが、Rails 3ではディレクティブになり、ユーザの与える値がシンボルとして残りました。

また、ActionController::Routing::RouteSet::Mapperインスタンスであるmapが必要なくなっています。

このエントリでは、8月21日のRails勉強会@東京第54回@a_matsudaさんに教えていただいたことをベースに、Rails 3のroutesについてまとめます。

基本

URLのパターンに対し、コントローラとアクションを指定する方法は、Rails 2では

map.connect 'friends/:id', :controller => 'users', :action => 'show'

でしたが、Rails 3では

match 'friends/:id', :to => 'users#show'

になります。mapが不要になったことと、コントローラとアクションを1つの値で指定できることで簡潔になっていますね。

逆に、オプションのパラメータは()で囲む必要があります。例えば、デフォルトルートは下のように定義されます。

match '/:controller(/:action(/:id))'

名前付き(Named)

Rails 2では下の.friendのようにMapperインスタンスへ送るメッセージがルート名になったのですが

map.friend 'friends/:id', :controller => 'users', :action => 'show'

Rails 3では基本のmatchディレクティブに:asを付けます。

match 'friends/:id', :to => 'users#show', :as => 'friend'

root

root、つまり「/」に対するrouteは、Rails 2ではmap.rootというnamed routeが「/」に予約されていましたが、Rails 3ではrootというディレクティブを使います。例えば下のようになります:

root :to => 'home#show'

基本の省略形

基本のmatchディレクティブでは、実は:toを省略できます。つまり、下の2つは同じことです。

match 'friends/:id', :to => 'users#show'
match 'friends/:id' => 'users#show'

さらに、パスのノード名がコントローラ名とアクションに一致するなら、それも省略可能です。よって下の2つは同じです。

match 'users/summary' => 'users#summary'
match 'users/summary'

特定のコントローラに対するルートは、controllerディレクティブでまとめることができます:

controller :users do
  match 'summary', :to => :short
  match 'pretty', :to => :pp
end

resources

RESTfulなルートを定義するのに便利だったmap.resourcesですが、Rails 3でもresourcesとして使用可能です。例を挙げます:

resources :users

さらに、複数のリソースを渡せるようになりました:

resources :users, :products, :shops

resourcesはブロックを受け取り、その中で冒頭で紹介したmembergetといったディレクティブを使用できます。

resourcesをネストした場合は、Rail 2と同様、has_manyの関係を定義できます。

resources :users do
  resources :items
end

# user.rb
class User < ActiveRecord::Base
  has_many :items
end

:only:exceptといったオプションを与えて生成するルートを絞ることや、:constraintsオプションで受け付けるパラメータのパターンを制限するのもRails 2と同じです。

namespace

namespaceディレクティブを使うことで、パス(URL)とコントローラを任意の名前空間に入れることができます。例えば下のように書くことで、パスは/admin/users、コントローラはapp/controllers/admin/users_controller.rbに置かれたAdmin::UsersControllerになります。

namespace "admin" do
  resources :users
end

もしパスだけに名前空間をつけてコントローラはapp/controllersに入れたければ、scopeを使います:

scope "/admin" do
  resources :users
end

反対に、コントローラにだけ名前空間を付けたければ、scope :module => を使います:

scope :module => "admin" do
  resources :users
end

Rackアプリケーションへルート

Rails 3ではルーティングの機構自体がRackミドルウェアなので、別のRackアプリケーションを呼び出すようなルートも書けるようになりました。例えば:toの先にSinatra::Baseの子クラスを書くことができます。また、redirectというメソッドを使ってリダイレクトを指定することもできます:

match "/users/:id", :to => redirect("/accounts/%{id}")

参考

講師をしてくださった松田さんがRails 3の特集を書かれました。

WEB+DB PRESS Vol.58
WEB+DB PRESS Vol.58
  • 発売元: 技術評論社
  • 価格: ¥ 1,554
  • 発売日: 2010/08/24

Published on 29/08/2010 at 02h15 under . Tags ,

Gitの新機能を使ってHerokuへのpushを軽くする

HerokuRack専用のホスティングサービスで、Gitでpushすることでデプロイできるという特長があります。

ところが、git pushはブランチのヒストリを全て転送するため、実際のデプロイメントに必要ない過去のファイルも転送されてしまい、無駄です(バックアップとして使えるという考え方もあると思いますが、その目的であれば他にもっと効率的な方法があります)。

ここではGit 1.7.2の新しい機能を使って、実際のデプロイメントに必要なファイルだけをpushする方法を説明します。

Git 1.7.2では、checkoutコマンドに--orphanというオプションが追加されました。

* "git checkout --orphan newbranch" is similar to "-b newbranch" but prepares to create a root commit that is not connected to any existing commit.

つまり、--orphanを付けて新しいブランチを作ることで、ヒストリから切り離されたコミットを作ることができます。例えば以下のようにdeployというブランチを作ると、checkoutしたファイル(ここではREADMEのみ)が全てnew fileとしてステージされた状態になります。

$ git checkout --orphan deploy
Switched to a new branch 'deploy'
$ git status
# On branch deploy
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached ..." to unstage)
#
#       new file:   README
#

ここでcommitすればdeployブランチ唯一のコミットができます。

$ git commit -m 'for deployment.'
[deploy (root-commit) 38bbc6b] for deployment.
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 README

show-branchすることで、既存のブランチから完全に切り離されていることが分かります。

$ git show-branch
* [deploy] for deployment.
 ! [master] initial commit.
--
*  [deploy] for deployment.
 + [master] initial commit.

このdeployブランチをherokuへpushするには、以下のようにします:

$ git push heroku deploy:master

転送するオブジェクトは最小で済むはずです。

Published on 02/08/2010 at 01h40 under . Tags , ,

Powered by Typo – Thème Frédéric de Villamil | Photo L. Lemos