taslamの日記

>>mizincogrammerに移転しました。こちらは、今後更新されません。<<

2008-07-30

[][]並列処理でActiveRecordを使う

※ドキュメントを読みながらこんなもんかな?とやってみたやつなので問題あるかもしれません。何かあればコメント頂けると嬉しいです。

例えば、DBからデータを取り出して逐次メールを送信する場合。

よく知られているようにメールの送信はコネクションの確立やSMTPサーバの処理などの待ち時間が長く、逐次処理をしていると無駄が大きすぎる。

処理を並列化して、あるメールの送信待ち時間を他のメールの構築等に充てて無駄をなくすことを試みる。

環境

処理の並列化

Thread.newを使う。単純に実装すればこんなかんじ。

# 未送信のレコードをそれぞれ別々のスレッドで処理するサンプル
threads = []
mails = find(:all, :conditions => ["sent = ?", false])
mails.each do |mail|
  threads << Thread.new do
    Thread.pass
    Mailer.deliver_mail(mail)
    mail.update_attributes(:sent => true)
  end
end
threads.each { |t| t.join }  # すべてのスレッドの処理が終わるのを待つ

ActiveRecordの並列化

処理は並列化できたが、これだとDBへのコネクションは1つしかなく、例えばスレッド毎にトランザクションを開始するようなことはできない。ActiveRecordのコードを追ってみたところ、どうやら

ActiveRecord::Base.allow_concurrency = true

とすれば良いようだ。

こうすることで、スレッド毎にコネクションを保持するようになり、スレッド毎に別々のトランザクションを開始することができる。

これらを踏まえたサンプル

コネクションが増えすぎないように、あらかじめ決めておいた数のスレッドで並列処理する。メールの重複送信を防止するため、PostgreSQLの行レベルロックを活用した。

ActiveRecord::Base.allow_concurrency = true  # マルチスレッド対応
class Mail < ActiveRecord::Base
  validates_presence_of :recipients, :from, :subject, :body

  # 送信時に最大何個のスレッドを作るか
  cattr_accessor :max_threads
  @@max_threads = 20

  class << self

    # すべての未送信メッセージを送信
    # 送信に成功したメールモデルのインスタンスの配列を返す
    def send_all!
      sent_mails = []
      threads = []
      mails = find(:all, :conditions => ["sent = ?", false])  # 未送信のメールを取り出す
      # max_threads個のスレッドで並列処理
      max_threads.times do
        threads << Thread.new do
          Thread.pass
          while mail = mails.shift
            sent_mails << mail if mail.send!
          end
          clear_active_connections!  # 処理が終わったのでコネクションを切断
        end
      end
      threads.each { |t| t.join }  # すべてのスレッドの処理が終わるのを待つ
      sent_mails
    end

  end
 
  # メール送信
  # 送信前には行レベルロックを行い二重送信を防ぐ
  def send!
    self.class.transaction do
      if new_record? || find(:first, :select => 'status', :conditions => ["id = ?", id]).status
        false
      else
        sended_mail = Mailer.deliver_mail(self)
        update_attributes(:sent => true)
        true
      end
    end
  end
  
end
# ひとつだけ送信
mail = Mail.new(:from => 'hoge@mizincogrammer.com',
                :recipients => 'fuga@mizincogrammer.com',
                :subject => 'Hello',
                :body => 'Rails World!!')
mail.save
mail.send!
# CRON等でまとめて送信
Mail.send_all!

2008-07-29

[]ActionCacheについてきっちり理解する

FragmentCache等に比べ、活用されることの少ないActionCacheですが、正直よく理解してないという方も多いのではないだろうか。

Rails 2.1のActionCacheのコードをざっと眺めて、簡単に特徴をまとめたいと思う。

「ActionCacheはフィルタ

ActionCacheは、キャッシュが存在すれば、キャッシュの内容を表示してfalseを返す(処理を止める)」というフィルタとして実装されている。

具体的には、caches_actionを呼び出すと、around_filterでActionCacheFilterのインスタンスが設定される。

これがどういうことかというと、

  • キャッシュが存在すれば、アクションは実行されない。つまり、アクション内にボトルネックとなる処理がある場合、有効なキャッシュ手段となる。
  • フィルタは実行されるので、「ログイン済みのユーザのアクセスのみページ(キャッシュ)を表示」といった、フィルタを活用する処理は可能
  • ただし、caches_actionよりも前にbefore_filter等を呼び出すか、prepend_before_filterを使用することが必要。(キャッシュが見つかった時点で処理を中断してしまうので、その後に実行されるべきフィルタは実行されない)

2008-07-18

[]こんなキャッシュ機能が欲しい

パラメータクエリストリング)を考慮したアクションキャッシュが欲しい。

あるのかな?

ないのならつくろうかな。

2008-07-09

[][][]タグのバージョニング

acts_as_versionedは、モデルのバージョニングを手軽に実現できるプラグイン。

acts_as_taggable_on_steroidsは、タグを手軽に実現できるプラグイン。

併用したとき、モデルのバージョンを戻すと同時にタグもその時点のものに戻したいと思うのが人情というもの。

方法

acts_as_taggable_on_steroidsのタグリストのキャッシュ機能を利用すれば簡単に実装できる。

試した環境

Rails 2.0.2(Rails 2.1用のacts_as_versionedはまだ試したことない)

コード

class Item < ActiveRecord::Base
  acts_as_versioned
  acts_as_taggable

  # revert_toで、cached_tag_listからtag_listを復元する
  # (cached_tag_listがないとダメだよ)
  alias :revert_to_without_taggable :revert_to
  def revert_to_with_taggable(version)
    if revert_to_without_taggable(version)
      self.tag_list = cached_tag_list
      true
    else
      false
    end
  end
  alias :revert_to :revert_to_with_taggable
end

2008-07-07

[][]携帯絵文字をPCで表示するためのフィルタをつくってみた

id:darashiさんが開発している、Railsで携帯向けアプリケーションを構築する時に便利なRailsプラグイン「Jpmobile」の機能を拡張するプラグインです。

Jpmobileにおいて、携帯の絵文字はUnicodeの私的領域にマッピングされます。そしてアクセスがあるとユーザの使用する携帯端末に合わせて変換後、送出されます。(after_filterなので、もちろんフラグメントキャッシュは利用できます。)

しかしながら、現在のところPCでのアクセスのためのフィルタは用意されてなく、PCで閲覧した場合何も表示されないか、ブラウザによっては文字化けします。

そこで、このプラグインの出番です。(いいすぎだったらごめんなさい)

特徴

よいとおもうところ
  • after_filterの段階で変換を行うので、HTMLタグのエスケープで悩むことがない(「h」つけてOK)
  • TypePadがCC2.1で公開している絵文字画像を対象としているのでライセンス的にだいぶ安心
  • すでに導入済みのシステムに対してもビューの変更などが不要
よくないとおもうところ
  • after_filterの段階で変換を行うので、アクション毎に変換が必要→きっと重い

インストール

プラグインのインストール
script/plugin install http://taslam-plugins.googlecode.com/svn/trunk/jpmobile_emoticon_filter
絵文字画像の配置

SixApartが公開している絵文字画像を使います。

必ずライセンスに目を通して、それにしたがって利用してください。

解凍すると、emoticonsというフォルダができ、その中に絵文字画像が沢山入ってます。

これをそのままRAILS_ROOT/imagesにコピーします。(RAILS_ROOT/images/emoticons/*.gifという感じになります)

なお、今回一番時間がかかったし大変だったのは、この絵文字画像との変換テーブルの作成でした・・疲れた。

※TypeCastのconfにyamlで入ってました。なんという無駄な努力orz

コントローラにemoticon_filterを書き加える
class SampleController < ApplicationController
  emoticon_filter
end

実際には、mobile_filterを使用する携帯用コントローラで継承するための抽象クラスと別に、PC用コントローラで継承するための抽象クラスをつくり、そこで呼ぶことになるでしょう。適当に応用してください。

インストールは、これだけです。

emoticon_filterのオプション
:classname => 'emoticon'  # 絵文字IMG要素のクラス名
:path      => 'emoticons' # 絵文字画像のパス(imagesからの相対パス)

追記

少し修正した。

  • フィルタのクラス名を変更した。ついでに、Jpmobile::Filter::Baseのサブクラスにした。
  • 入力時に絵文字画像のIMGタグから、内部コードに変換するようにした。
  • 数値実体参照を絵文字画像に変換するようにした。(※hを使うと&が&amp;にエスケープされてしまうため、微妙。思案中。)
  • あんまり使い道なさそうだったし、入力の変換でややこしそうだったので:only,:exceptオプション削除。

追記 2008.8.8

  • html及びjavascript以外でのレスポンスの場合、フィルタを適用しないようにした。
  • javascriptでのレスポンスの際、ダブルクォーテーションをエスケープするように修正。

[]Filterクラスをつくってみる

class SampleFilter

  def initialize(options)
    # オプションのせっていなどなど
  end

  def before(controller)
    
  end

  def after(controller)
    controller.response.body.gsub!(/hoge/, 'fuga')
  end

end

class ActionController::Base #:nodoc:

  def self.sample_before_filter(options = {})
    before_filter SampleFilter.new(options)  # SampleFilter#before
  end

  def self.sample_after_filter(options = {})
    after_filter SampleFilter.new(options)  # SampleFilter#after
  end

  def self.sample_around_filter(options = {})
    around_filter SampleFilter.new(options)  # SampleFilter#before, SampleFilter#after
  end

end
class SampleController < ApplicationController
  sample_after_filter :only => [:index]
end