Rails Metal と Controller のセッション共有2010/02/26

Railsノート - セッションまわりを読む (1)

ここが詳しい。

Controller クラスのインスタンスメソッド session で参照できる Hash と同じものが、 Metal クラスでは env['rack.session'] で参照できる。

(env は call の引数)

Rails Metal の URIマッピング2010/02/26

発端

passenger + Rack 環境にアプリを置こうと思ったのだけど、 いろいろあって、「ActiveRecord があった方が良くね?」という意見が発生。

「それだったら、Rails Metal で良くね?」という結論が、脳内会議で決議された訳だけど、そこで悩んだのが、URIマッピングのルールが良く判らない、という点。

ソースはあんまり見てない(特にroutes関係)なので、あまり信用しない方が良いけど、何かの役に立つかも知れないので、晒しておく。

テンプレート

$ ./script/generate metal foobar

の結果作成される app/metal/foobar.rb のテンプレートは

# Allow the metal piece to run in isolation
require(File.dirname(__FILE__) + "/../../config/environment") unless defined?(Rails)

class Foobar
  def self.call(env)
    if env["PATH_INFO"] =~ /^\/foobar/
      [200, {"Content-Type" => "text/html"}, ["Hello, World!"]]
    else
      [404, {"Content-Type" => "text/html"}, ["Not Found"]]
    end
  end
end

こんな感じになる。 これは(require行とその作用を除けば)、非常に単純なRackアプリケーションそのものなので、 詳細は省略する。

これで、

$ ./script/server &

$ curl http://localhost:3000/foobar
Hello, World!

foobar 以下のURIがこの metal で処理されていることが判る。

routes.rb ?

Rails アプリケーションのURI情報の設定なら、config/routes.rb が正道だ、と思ったが、 どうも routes.rb には metal 関係のキーワードは定義されていない。

ルーティング情報はどこに?

となると、怪しいのは

    if env["PATH_INFO"] =~ /^\/foobar/

この if文だという訳で、regexpを書き換えたりしてみたところ、どうもこいつがルーティング情報らしいことが判明。

# Allow the metal piece to run in isolation
require(File.dirname(__FILE__) + "/../../config/environment") unless defined?(Rails)

class Foobar
  def self.call(env)
    if env["PATH_INFO"] =~ /^\/hogehoge/
      [200, {"Content-Type" => "text/html"}, ["Hello, World!"]]
    else
      [404, {"Content-Type" => "text/html"}, ["Not Found"]]
    end
  end
end

だと

$ curl http://localhost:3000/foobar
(略)
Routing Error
(略)
$ curl http://localhost:3000/hogehoge
Hello, World!

になる。

まとめ

  • metal は routes.rb に書いた規則よりも前に実行される。
  • metal は全てのURIに対して実行される。
  • metal が、404のレスポンスを返した場合は、他の metal(あれば)、routes.rb の規則の適用の順にリクエストが取り回される。
  • 複数の metal が存在する場合の実行順は未定義(ファイルのiノード順っぽい)

ということみたいだ。

演繹できる事実として、同名の metal と controller が居た場合は metal が勝つのと、 controller に落ちるまでには metal が全て実行されるので、極端に metal のクラスが多い場合は要注意かも知れない。

あと、passenger と共に使用する場合は、metal の中で、request.body() とかやってしまった後に、404で次の metal や controller に処理を 回すと大変な事になるような気がする。

(passenger の Rackインターフェイスに入ってくる request.body()はrewindableなオブジェクトではないため)

追記

長々と書いた後で、config/config.ru に rackup用のconfigが書けることに気がついた。

ここなら、 マッピングルールを自在に書ける。

Heroku Garden is no longer supported2009/08/28

Herokugarden から、近日中にサービスを終了するので、後継サービスのHerokuへ移行するようにとのメールが来た。

Herokugarden は無料のRailsホスティングサービスの一つ。

Web上でRailsプロジェクトの作成、ファイルの編集、DBのマイグレーション等を完結できる、どちらかと言うとネタ系に近いサービスだった。(javascriptによるエディタが多少残念な感じだったので、テストプロジェクトを作成したまま放置状態だった)

対するHeroku は、Web上での編集機能はオミットされ、純粋なRailsホスティングサービスになったようだ。

5MBまでのストレージと、1サーバプロセスが無料。それぞれ追加は有料。

フルオプションだと、ストレージ2TB、24サーバプロセスで、月$2428。

4サーバプロセスで、大体普通(って何?)のCPU1つ分らしい、これに50MBストレージで月$123。 サービスの品質次第ではアリかなあ。

後で評価する。

ActionMailer でテンプレートを指定する2009/08/11

ActionMailer のRDocでは何故か解説が無いようですが

Model側で

class FooMailer < ActionMailer::Base
  def bar
    from 'foobar@example.com'
    recipients 'foobar@example.com'
    
    template 'hoge'
    body :mes1 => 'hogehoge', :mes2 => 'foobar'
  end
end

FooMailer.create_bar # foo_mailer/bar.erb ではなく foo_mailer/hoge.erbが使用される

と、templateメソッドを利用すると、当該のメールがcreate!される際のテンプレートが明示的に指定できます。 引数は、APP_ROOT/app/views/foo_mailer からの相対パスです。 拡張子は、例のルールで補完されます。

テンプレートをファイルではなく String で与えたい場合に適切なメソッドは無いのですが、 ActionMailer::Base#create! は実行された際、@body が 既にStringのインスタンスならレンダリング処理はスキップして @body の内容をそのままメール本文とするようです。

class FooMailer < ActionMailer::Base
  def piyo
    from 'foobar@example.com'
    recipients 'foobar@example.com'
    
    body 'piyo piyo'
  end
end

FooMailer.create_piyo.body #=> 'piyo piyo'

あとは、自前でerbのコンパイルをするか、 ActionController::Base#render に、:inlineを与えたときのようなメソッドを実装すればいけるはず。 (ActionMailer::Base#render はファイル名しか取れない)

とりあえず、今日はここまで

Rails と Thread と autorequire2009/05/28

忘備録として

Railsというか、autorequire が効いている環境で、Threadを使う場合には注意が必要だ。

模擬コード

def send_many_mail(recipients)
  recipuents.each_slice(1_000) do |recipients_sub|
    Thread.new do
      recipients_sub.each do |recipient|
        TestMailer.deliver_test_mail(recipient)
      end
    end
  end
end

意図としては、大量にメールを送信したいという要件があって、それらを多重でなんか頑張ってみようというもの。

コードを簡単にするために、受け取り人の数で each_sliceしているけど、実際にはスレッド数を予め決めておいて、その数に受け取りメールアドレスを分割すべき。

また、これらのスレッドがちゃんと天寿を全うするには(全てのメール送信が完了するまで生き続けるためには)、これらスレッドにjoinするか、素でこれらのスレッドより長いライフタイムを持つプロセスの下で生成される必要がある。

要は、単純に controller のメソッド内で、Thread.new しても、大抵の場合は上手くいかないという話し。

小さな不特定多数のプロセスをspinするのが目的なら、BackgrounDrbが便利だし、今回のようにバッチっぽい処理をブラウザからキックしたい場合は、予め 自前でdrb のプロセスを立てておくのが良いと思う。

このあたりは機会があったら別のエントリを書くかも。

問題点

で、先程のコードは、上手く動くかもしれないし、動かないかもしれない。

実際、大抵の場合は上手くいくのだけれど、プロセスを(再)起動した直後に、メール送信に失敗することがある。

何が問題かというと、autorequireがthread safeでないということで、

プロセスの(再)起動時において、
Thread.new に突入した時点では TestMailer は未定義である

ということである。

そこで、autorequire が がりがりと
TestMailer のソースファイルを読み込んでクラスを定義する

のだが、

複数のスレッドで autorequire がクラス定義を始めてしまうことになる

為に

Object is not missing constant TestMailer!

で落ちる訳だ。

対処法

要は Thread.new する前にクラス定義が終了していれば良いので、模擬コードはこんな感じになる。

def send_many_mail(recipients)
  TestMailer
  recipuents.each_slice(1_000) do |recipients_sub|
    Thread.new do
      recipients_sub.each do |recipient|
        TestMailer.deliver_test_mail(recipient)
      end
    end
  end
end

もっと前に明示的に require を書くのが正しい気がしないでもない。