BackgrounDRbを使ってみた(第8回Rails勉強会@東京ログ)

第8回Rails勉強会@東京において、この前調べておいた記事を実際にやってみた。
http://d.hatena.ne.jp/walf443/20060720/1153400540

動かす際に色々とハマりどころがあったようなのでうまくやるコツのようなものを書いておく。

rakeのタスクは基本的にproduction環境のときに使うコマンドが多い。BackgrounDRbのrakeにインストールされるタスクは基本的にproduction用。development環境の際は、script/backgroundrb/(start|stop)を使う。rakeのタスクはデーモン化し、scriptを使うとデーモン化しない。デーモン化するとlogを見る以外には反応しないのでプロセスをうっかり2・3個立ち上げたりするとうまくいかなくなってしまう。

workerを編集したら実行する前にbackgroundrbサーバーを再起動させること。RailsWEBrickにやっているみたいなトリック(開発時には再起動しなくても良いようにする)がBackgrounDRbサーバーにもできないかなとか思ったりした。

DRbの使い方を詳しく知らなくてもタスクに対してWorkerクラスにdo_work(args)メソッドを一つ書いておけば良いというのは便利だと思いました。

最近のアップデートだと、期限付きでWorkerを生成できるようになったようです。
http://brainspl.at/articles/2006/07/02/backgroundrb-updated

BackgrounDRbについて調べてみた

このページの情報は既にかなり古いです。私は現在あまり使っていませんし、追ってもいないのでこのページの情報が更新されることはあまり期待できないのでなるべく本家の情報なりを見た方がよいでしょう。2009年8月現在だと http://d.hatena.ne.jp/tech-kazuhisa/20090816/1250432286 とかで似たような情報をあつかってるみたいです。検索しても結構上にまだくるっぽいのでいちおう追記しておきました。

次回の第8回Rails勉強会@東京でセッション案に上がっていたBackgrounDRbというやつがなにやら面白そげなので調べてみた。

調べてみた感じ以下のページがよくまとまっている。
http://www.infoq.com/articles/BackgrounDRb
以下、この内容を大雑把に要約してみる。

Ruby on Railsは素晴らしいフレームワークですが、Webアプリケーションをどんどん拡張していくと、バックグラウンドで長い時間処理するようなタスクが必要になってくることがあります。このような処理をControllerとかに書いてしまうとWebサーバーの動作の妨げになります。Webサーバーをタイムアウトさせずにこれらの処理を手軽に行なえるようにしたRailsのプラグインがBackgrounDRbです。

BackgrounDRbサーバーはMiddleManオブジェクトを配信します。このオブジェクトはworkerクラスを管理します。このオブジェクトは

  • @jobs = {job_key => running_worker_object}
  • @timestamps = {job_key => timestamp }

というインスタンス変数を持ちます。MiddleManオブジェクトはDRbサーバーとRailsアプリケーションの仲立役として機能します。(ここら辺にある図は見ておいた方が良さそう。)

インストールは次のように行います。

script/plugin install svn://rubyforge.org//var/svn/backgroundrb

Pluginをインストールすると、generatorにworkerジェネレータが使えるようになります。

$ script/generate worker Foo

class FooWorker < BackgrounDRb::Rails
  def do_works(args)
  end
end

MiddleManオブジェクトを通じてRailsからFooWorkerオブジェクトがインスタンス化されると、do_worksメソッドが自動的にRailsとは独立したスレッドで動作します。そのため、Railsはdo_worksメソッドが終了するのを待たずに先に進めます。

BackgrounDRbを使えば、Ajaxリクエストを使うと新しいworkerオブジェクトを生成します。このとき、Viewではperiodically_call_remoteメソッドを利用すればジョブの進行状況を取得し、どうなっているか表示することが出来ます。FooWorkerオブジェクトを生成し、RailsのControllerから進行状況を表示するコードを以下に示します。

class FooWorker < BackgrounDRb::Rails
  attr_reader :progress
  def do_work(args)
    @progress = 0
    calculate_the_meaning_of_life(args)
  end
  def calculate_the_meaning_of_life(args)
    while @progress < 100
      # ここで計算します
      @progress += 1
    end
  end
end

コントローラは以下のようにします。

class MyController < ApplicationController
  # FooWorkerを起動するクラス
  def start_background_task
    session[:job_key] = MiddleMan.new_worker(:class => :foo_worker, :args => "do_workメソッドに渡される引数") # 追記 「)」が抜けておりました。お試しになった方申し訳ありません。
  end
  
  # 進行状況を表示するクラス
  def get_progress
    if request.xhr?
      progress_percent = MiddleMan.get_worker(session[:job_key]).progress
      render :update do |page|
        page.call('progressPercent', 'progressbar', progress_percent)
        page.redirect_to(:action => 'done') if progress_percent >= 100
      end
    else
      redirect_to :action => 'index'
    end
  end
  
  # タスクが終わったときに表示するメソッド
  def done
    render :text => "FooWorker task has completed"
    MiddleMan.delete_worker(session[:job_key])
  end
end

start_background_task.rhtmlはpage.call('progressPercent', 'progressbar', progress_percent)が実行されたときのViewを指定します。*1

 <html>
	      <head>
	       <style type="text/css">
	         .progress{
	           width: 1px;
	           height: 16px;
	           color: white;
	           font-size: 12px;
	           overflow: hidden;
	           background-color: #287B7E;
	           padding-left: 5px;
	         }
	       </style>
               <%= javascript_include_tag :defaults %><!-- 本家にもないので注意! -->
	       <script type="text/javascript">
	         function progressPercent(bar, percentage) {
	           document.getElementById(bar).style.width =  parseInt(percentage*2)+"px";
	           document.getElementById(bar).innerHTML= "<div align='center'>"+percentage+"%</div>"
	         }
	       </script>
	      </head>
	       <body>
	        <div id='progressbar' class="progress"></div>
	        <%= periodically_call_remote(:url => {:action => 'get_progress'}, :frequency => 1) %>
	       </body>
    </html>

MiddleMan.new_workerメソッドはランダムなジョブキーを持つジョブを作成し、あとからセッションに格納します。ジョブキーに特定の名前を付けたい場合は以下のようにします。

MiddleMan.new_worker(:class => :foo_worker, 
                                      :job_key => :my_worker, 
                                      :args => "do_workerメソッドの引数")

MiddleMan.get_worker :my_worker

BackgrounDRbの設定はRAILS_ROOT/config/backgroundrb.ymlで行います。load_railsオプションをtrueに設定すると、workerクラスにActiveRecordオブジェクトを使うことが出来ます。BackgrounDRbサーバーを起動する際にdatabase.ymlの情報を読んでデータベースに接続します。

このプラグインはActiveRecordのオブジェクトを含む巨大なオブジェクトをキャッシュする機能があります。レンダリングされたViewや巨大なクエリもMarshallダンプできるオブジェクトであれば何でもキャッシングすることが出来ます。以下のようにして行います。

# キャッシュするには次の2通りのやり方がある
@posts = Post.find(:all, :include => :comments)
MiddleMan.cache_as(:post_cache, @posts)

@posts = MiddleMan.cache_as :post_cache do
  Post.find(:all, :include => :comments)
end

# 次のようにしてキャッシュを取り出す
@posts = Middleman.cache_get(:post_cache)

@posts = MiddleMan.cache_get(:post_cache){ Post.find(:all, :include => :comments) }

MiddleMan.cache_getがブロックをとるかとらないかは:post_cacheキーが存在しないときにnilを返すか、ブロックを実行するかである。

現在の実装だと役目を終えたworkerオブジェクトやキャッシュは手動で無効にしてやる必要がある。これにはMiddleMan.delete_worker(:job_key)とMiddleMan.delete_cache(:cache_key)を使う。Timeオブジェクトを取り、それよりもタイムスタンプが古ければ削除するというMiddleMan.gc!メソッドもある。次の例は、30分前のジョブは無効にするスクリプトである。これをcronで定期的に実行することで残った不要なジョブを自動的に消すことが出来る

#!/usr/bin/env ruby
require 'drb'
DRb.start_service
MiddleMan = DRbObject.new(nil, "druby://localhost:22222")
MiddleMan.gc!(Time.now - 60 * 30)

将来的にはオブジェクトを作成したときにGCが実行される時間をスケジュールしておけるようにするつもりのようです。

BackgrounDRbサーバーを起動したり停止したりするRake タスクも自動的にインストールされます。

$rake backgroundrb:start
$rake backgroundrb:stop

次のような用途にBackgrounDRbは使われているそうです。

多少端折りましたが、重要なところはだいたい網羅できてると思います。なんか変なところとかあればコメントとかTBでどうぞ。

追記

yamazさんに具体例の訳を手伝っていただきました。

*1:ちょっとマズいところがあったので本家のソースと差し替えて少し書き加えました

Rails::Initializerを読む。

目次はこちらです。

まずは、rails-stable/railties/lib/initializer.rbから読みます。このファイルではRails全体の初期化を行なっています。

File.join, File.dirname, __FILE__

require File.join(File.dirname(__FILE__), 'railties_path')

Rubyのライブラリではよく出てくる表現なので一応解説しておく。

File.joinはディレクトリ区切り文字で引数を連結した文字列を返す。この場合、Unixであれば"#{File.dirname(__FILE__)}/railties_path"、Windowsであれば"#{File.dirname(__FILE__)}\railties_path"を返す。

File.dirname(filename)はfilenameの一番後ろのスラッシュより前の文字列を返す。もしスラッシュがなければカレントディレクトリ(つまり".")を返します。

__FILE__はRubyの組み込み変数で現在のソースファイル名です。似たようなので$0というのもありますがこちらは実行中のスクリプトファイルを表します。Rubyではよく出てくるので違いをしっかり覚えておきましょう。

つまり、ここのコードでは、このファイル(initializer.rb)と同じディレクトリにあるrailties_path.rbをrequireしています。

RAILS_ENVの設定

RAILS_ENV = (ENV['RAILS_ENV'] || 'development').dup unless defines?(RAILS_ENV)

定数RAILS_ENVが定義されていなければ定義する。環境変数RAILS_ENVがセットされていればそれを用い、それがなければdevelopmentをセットする。

Object#dupを使って複製しているのがちょっとよく分からない。複製していないとRails起動中にENV['RAILS_ENV']が代えられてしまった場合、内部動作に整合性が保てなくなってしまうからだろうか。なかなかどういうときにdupすればよいかというのは難しそうだ。というよりうっかり書き忘れてしまいそうだ。

Rails::Initialize.run

def self.run(command = :process, configuration = Configuration.new)
  yield configuration if block_given?
  initializer = new configuration
  initializer.send(command)
  initializer
end

ブロックでRailsの設定が出来ます。ConfigurationクラスはRailsの設定に関わるクラスです。Railsアプリケーションの作成時にはconfig/environment.rbでこれらの設定を行います。

この中の

  initializer = new configuration

というのがビックリする。Javaっぽいけどこれはどういうことなのだろうか?

クラスメソッドの中なので、同じクラスメソッドのnewのレシーバが省略できるらしい。

つまり、

  initializer = self.new configuration

ということのようだ。

ここでInitializerにせずselfにしたのはひょっとするとクラス名を変更したい、あるいはaliasしたいといった場合があるからである。クラス定義文の中ではなるべくクラス名は書かずにselfとしておいた方が継承したときなどにいろいろ厄介ごとがなくて済む。

個人的にはどこで定義されているのか混乱してしまうのでクラスメソッドのレシーバ省略はあまりしたくないのですが、省略する方が一般的なのでしょうか?やっぱり派閥に分かれているのかも。まぁできるだけ省略したくないと思うのはそれだけ使い込んでないということなんだろうと思う。今回すぐにクラスメソッドの省略が見抜けなかったのも経験不足ゆえだろう。

次の行はInitializerの任意のインスタンスメソッドをObject#sendを使って実行させている。デフォルトではprocessメソッドが実行されるらしい。

確か1.8以降だとObject#__send__を使わないと警告が出るんだったような気がする。ここらあたりはどうやって回避しているのだろうか。もしかすると再定義されたメソッドなのかもしれない。自分で探してみた感じどうもそれっぽいのが分からないのですがどうなんでしょうか?

Configurationクラス

ここで先にConfigurationクラスを呼んだ方が理解しやすいので一旦Configurationクラスの基本的なところを押さえておく。

コンストラクタ

アクセサにズラーと初期値を代入している。初期値は"default_" + アクセサ名のメソッドで定義してある。

以下気になったところのみ定義されている順番にピックアップ。

default_load_paths

500行目:

 paths = ["#{root_path}/test/mocks/#{environment}"]

カッコが省略されているのでわかりづらいがそれ以前にroot_pathやenvironmentといったローカル変数が定義されていないのでこれはメソッドだ。ということで探してみると案の定あった。

def root_path
  ::RAILS_ROOT
end

def environment
  ::RAILS_ENV
end

定数を参照しているだけなのにメソッドを作ってるのは何故?

RAILS_ROOT = ::RAILS_ROOT

とかだといけないのか?

503行目:

paths.concat(Dir["#{root_path}/app/models/[_a-z]*"])

書き方からしてDir.glob()のaliasのようだなと思って探してみたらRuby標準で定義されてた。ActiveSupport拡張だと思ってた。今まで知らずに全部Dir.globとか書いてたよ…。ワンライナーとか全部Dirに変更だな。

というより考えてみたら当たり前なのだけど、クラスメソッドに(args)といったメソッドを定義すれば組み込みメソッドみたいに使えるのね。最初はDirといった定数にハッシュを入れておいてそれにアクセスだとか、Kernel#Dir(array)といったメソッドを定義しておいてそれのカッコの省略なんじゃないかと疑った。

default_log_level

log_levelはinfo、debugという2種類のモードがあるようだ。development, testのときはdebugモードで作動する。

default_controller_paths

controllerは"#{root_path}/app/controllers/"だけではなく、"#{root_path}/components/"や"#{RAILTIES_PATH}/builtin/controllers/" に置くことが出来る。

RAILTIES_PATHはrailties_path.rbで定義されている。
railties_path.rb:

RAILTIES_PATH = File.expand_path(File.join(File.dirname(__FILE__), '..'))

File.expand_pathは相対パスの文字列を絶対パスへ展開する。その際、第2引数を基準にする。第2引数が無ければカレントディレクトリとみなす。~の展開もしてくれるらしい。

つまり、Railsライブラリの置いてあるディレクトリのbuiltin/controllers/にコントローラを置くことも出来る。

普通は"app/controllers"を使うけど、"components/"とbuiltin/controllers/はどう使い分けるのだろうか?



他のメソッドは今はちょっとよく分からないので置いておく。

コンストラクタの最後では、フレームワークそれぞれのアクセサメソッドにOrderedOptionsクラスのインスタンスを渡している。

database_configuration

少し読みづらいので改行した。

def database_configuration
  YAML::load(
    ERB.new(
      IO.read(database_configuration_file)
    ).result)
end

"config/database.yml"からYAMLファイルを読んできてRubyのデータ構造へ戻す。興味深いのはあいだにERBを挟むことでYAMLファイル内でRubyコードを入れることが出来るようにしている点である。

ERB#resultはERBのコード部分を展開して文字列として返す。

OrderedOptions, OrderedHash

OrderedOptions、OrderedHashは実はActiveSupportでも定義されている。ActiveSupportをrequireする前に必要であるためにDRYじゃないがこちらでも定義せざるを得なかったようだ。OrderedOptionsやOrderedHashを別ライブラリにするという手もあったのではないかと思ってみる。

そうしてOrderedOptionsクラスを見てみると、OrderedHashクラスを継承している。ということでOrderedHashクラスを先に読む。

OrderedHashクラスはArrayを継承している。名前どおり順番を保持したHashを実装している。実装は特に難がなさそうなので飛ばす。順序つき配列を使いたいケースは結構あるので一応こんなのがあったと頭にとめておくと参考に出来る。

ということでOrderedOptionsを読む。

ここで注意することは、OrderedOptionsがキーとして文字列を取るのに対し、OrderedHashはキーとしてシンボルをとる。ActiveSupportをまだrequireしていないのでシンボル・文字列間の自動変換はやってくれない。

method_misingを用いてキーのアクセサっぽくアクセスできるようにしている。

  def method_missing(name, *args)
    if name.to_s =~ /(.*)=$/
      self[$1.to_sym] = args.first
    else    
      self[name]
    end
  end

このテクニックは色んなところで使えそうかも。

Initializer#process

ようやくInitializerクラスへ戻ってこれた。次は、Initializer#processを読む。

    def process
      check_ruby_version
      set_load_path
      set_connection_adapters

      require_frameworks
      load_environment

      initialize_database
      initialize_logger
      initialize_framework_logging
      initialize_framework_views
      initialize_dependency_mechanism
      initialize_breakpoints
      initialize_whiny_nils
      initialize_temporary_directories

      initialize_framework_settings
      
      load_environment
      
      add_support_load_paths

      load_plugins

      initialize_routing
      after_initialize
    end

メソッド名が分かりやすいので何の処理を行なっているかが読んだだけで大体は分かる。分かりにくいところ・気になるところだけ以下補足。

check_ruby_version, ruby_version_check.rb

check_ruby_versionはruby_version_check.rbをrequireしているだけなので、ruby_version_check.rbをみてみる。

どうもRubyの1.8.2と1.8.4以降ではRailsは動作して、1.8.3は動かないらしい。どうしてかよく分からない。安定版と開発版が交互だったのだろうか。でもここ最近は1.9.1が開発版だし、1.8.3がそんな前とも思えない。

load_environment
    def load_environment
      silence_warnings do
        config = configuration
        constants = self.class.constants
        eval(IO.read(configuration.environment_path), binding)
        (self.class.constants - constants).each do |const|
          Object.const_set(const, self.class.const_get(const))
        end
      end
    end

silence_warningsというメソッドが定義されている。名前だけでも十分機能を想像できるが一応読んでおく。

silence_warningsはactivesupport/core_ext/kernel/reporting.rbで定義されている。

  def silence_warnings
    old_verbose, $VERBOSE = $VERBOSE, nil
    yield   
  ensure  
    $VERBOSE = old_verbose
  end

多重代入を用いて$VERBOSEを一旦old_verboseに非難しておいて$VERBOSEにnilを代入する。今まであまり多重代入は使ってこなかったけど、こういうケースだと意味も明確になるので良さそうな気がする。

このメソッドの難点は警告が使っている間全く表示されないことだろう。Rubyのバージョンが変わって警告が出るようになっても気づかない恐れもある。警告は煩わしいが、出すには出すなりの理由があると思う。

例えば、先ほども出てきたObject#sendは使うと警告が出る。何故かといえば、sendはよくありそうなメソッド名だからだ。だからObject#__send__を推奨するようにメッセージが出てくる。うっかりユーザがsendメソッドを追加してしまったらどこかでハマりになる恐れがあるような気もする。

load_environmentへ戻る。

(self.class.contants - constants).each do |const|
  Object.const_set(const, self.class.const_get(const))
end

Array#- は集合の差を返すメソッドである。2行前でconstをself.class.constantsとしたあとに、"config/environment.rb"を読み込んでいることから、"config/environment.rb"はInitializer::定数名を定義することを期待されているようだ。変更されたものだけ定数をセットする。こんな回りくどいやり方をしているのは定数に再代入しようとしたら例外が起こるからだろう。

evalを使っても出来るが、Module.const_getやModule.const_setを使った方が作法としては良いだろう。

initialize_logger

RAILS_DEFAULT_LOGGERが定義されていたら、何もせずに抜けるようだ。つまり独自のloggerを使いたければコンストラクタRAILS_DEFAULT_LOGGERを定義しておけば良いようです。でも、その処理はどこに書けば良いのだろう・・・。

initialize_dependency_mechanism

Configuration#cache_classesの値によって毎回読み込むか、一度のみ読み込むかを操作しているらしい。

development環境だと:loadモードでproduction環境だと:requireモードになるようだ。

これによりブラウザをリロードするだけでコードの変更を反映できるという開発の快適性を実現している。

Dependenciesクラスはaction_controller/dependencies.rbで定義されている。

詳しく読むのはまた次の機会にする。

initialize_whiny_nils

珍しくメソッド名から意味がよく分からない。activesupportのwhiny_nil.rbをrequireしているのでそちらへ飛ぶ。

このファイルをのぞいてみると、NilClassを拡張している。コメントを読むと、どうやらnilに伴うエラーの際にデバッグしやすくするための拡張のようだ。

  WHINERS = [ ::ActiveRecord::Base, ::Array ]
  
  @@method_class_map = Hash.new
  
  WHINERS.each do |klass| 
    methods = klass.public_instance_methods - public_instance_methods
    methods.each do |method|
      @@method_class_map[method.to_sym] = klass 
    end
  end

まず、あるクラスからObjectクラスに定義されているパブリックインスタンスメソッドの配列の差集合を取ることで独自に定義されているメソッド名を割り出している。

その後、メソッド名をキー、クラス名を値としたハッシュとしてキャッシュしておくことでメソッドを呼び出そうとしたがnilクラスのためメソッドが定義されてないと例外を発生させる以上のことをすることが出来る。

method_missingでraise_nil_warning_forを呼び出すことでユーザがどのクラスを使おうとしていたのかということまで教えてくれる。

ActiveRecord::BaseとArrayに対してこの機能を有効にしているのだけど、これが拡張できるとうれしいケースが他にもあるかもしれない。(ちょっと今は思いつかないけど)

initialize_temporary_directories

セッションとキャッシュを保存するディレクトリを初期化する。

initialize_framework_settings

それぞれのフレームワークの設定を初期化する。

    def initialize_framework_settings
      configuration.frameworks.each do |framework|
        base_class = framework.to_s.camelize.constantize.const_get("Base")

        configuration.send(framework).each do |setting, value|
          base_class.send("#{setting}=", value)
        end
      end
    end

frameworkはシンボルなので定数形式に変換し、〜::Baseとする。(:active_record → ActiveRecord::Base)

Object#__send__を利用してそれぞれのアクセサにアクセスする。それぞれのアクセサの値には前に見たようにOrderedOptionsクラスのインスタンスがセットされているので、eachを使うことが出来る。

eachの内部でまたObject#__send__を使っているが、OrderedOptionsクラスには引数のようなメソッドはなく、method_missingで処理を振り分けていることを思い出して欲しい。

after_initialize, after_initialize_block

Configurationクラスのafter_initializeメソッドで登録したブロックをコールバックする。

Configurationクラスのafter_initializeメソッドをみてみる。

    def after_initialize(&after_initialize_block)
      @after_initialize_block = after_initialize_block
    end
   
    def after_initialize_block
      @after_initialize_block
    end

Rubyでコールバック関数を定義するテクニックっぽい。

定義部分で

def method_name(&block)

と受けることでブロックはProcオブジェクトへ変換される。(yieldで受けた場合は変換されない。そのためyieldを使う方が若干速いという説がある)

ブロックは単なるProcオブジェクトになったのでインスタンス変数に代入できる。その結果、変数名.callするだけでコールバックすることが可能。(コールバックの意味合いがちょっと違うような気もする。)

これでとりあえずinitializer.rbはこれで終了。色々なテクニックが駆使されていてかなり面白かった。さすがにこれだけの量の文章を書くのは大変だったけど。

次はどこを読むかまだ決めていない。何かリクエストかお勧めがあればコメント・TBで教えてくださいな。

はじめに・目次

はじめに

Railsソースコードを読みながら個人的に引っかかったところをメモしていきます。コメント・TB大歓迎です。

飽きっぽい性分のためいつまで続くか分かりませんが頑張ってみようかと思います。

いちおう大げさなタイトルを付けて自分にプレッシャーをかけておきました。

とはいえ卒論やら、期末レポートやらいろいろやらなければならないことに追われていますので比較的マイペースに進めていく予定です。

目次は随時追加していきます。

解説しているソースについて

ここで解説しているコードはbrancheのstableです。同じコードを読みたい方は以下のようにしてソースコードをダウンロードしてください。

svn co -r 4519 http://dev.rubyonrails.org/svn/rails/branches/stable rails-stable

環境としては以下のような感じです。

このドキュメントに書かれているものはあくまで私個人の解釈であり、正しさを保障できるものではない点をご了承ください。

したがってこのドキュメントのことでRuby on Rails開発者の方に直接問い合わせることは避けてください。

また[RailsHackingGuide]カテゴリのドキュメントに出てくるソースコードのライセンスはすべてRailsの開発者に帰属し、MITライセンスです。

第7回Rails勉強会@東京

第7回Rails勉強会@東京へ行ってきました。

第2回以来の久々の参加でした。何人か顔を覚えてくださっていた方もいらっしゃって前回よりも緊張せずに済みました。

パフォーマンスに関してのページを読む。

セッションの提案が滞っていたので一応他の人の意見を聞いてみようかなと印刷してきていたやつをセッションとして提案してしまいました。

InfoQ - Tracking change and innovation in the enterprise software development community
http://www.infoq.com/articles/Rails-Performance

Railsアプリで遅くなりがちな点について次のようなものがあげられています

  • choosing a slow session container
  • doing things on a per request basis, which could have been done once at startup
  • repeating identical computations during request processing
  • reading too often and too much from the database (especially in conjunction with associations)
  • relying too much on inefficient helper methods

From: http://www.infoq.com/articles/Rails-Performance

  • 遅いセッションコンテナを使っている
  • アプリを立ち上げた際に出来ることをリクエストのたびに行なっている
  • リクエストの処理中に同じようなことを何度もやっている
  • DBへのアクセスの回数が多すぎる
  • helperの使いすぎで遅い
遅いセッションコンテナを使っている
  • 速いセッションコンテナといえばSQLSessionStoreとMemCacheStoreがある。
  • SQLSessionStoreを使えばActiveRecordStoreのトランザクションやcreate_on、update_atといったイベントを避けることが出来る。
  • MemCacheStoreは30%ほどSQLSessionStoreよりも速い
  • ただしEric Hodelさん作のmemcache cliantを使う必要がある。(Ruby-Memcacheは遅すぎる)
  • このページの作者の人は開発の際にMemcacheは大げさだし色々めんどくさいのでDBベースのセッションストレージを使っているそうです。
アプリを立ち上げた際に出来ることをリクエストのたびに行なっている
  • 恋塚さんによれば、ここで出ているコード見たいな感じのやつをlib/以下に書いておけばRailsプロセスを立ち上げた際に読むだけなので良いとのことです。Helper依存しすぎはよくないと。
リクエスト処理中に同じようなことを何度もやっている
  • 例として挙げられているdetecting installed Ruby classes/modules using ObjectSpace.eachの意図がちょっと不明
クエリの最適化
  • アソシエーションがある場合に子から親へとか親から子へなどfindで検索する際に二度もSQL文を発行するのは良くないよと。
  • だからSQL埋め込みで何とかした方が良いよ。
  • でもそれってBKじゃね?(その場にいた一同)
  • Davidにはがっかりだよ。
Helper使いすぎ
  • デフォルトのルーティングは最適化の余地があるのでよく使うものほど上におく方が少し速くなるそうな。
ActiveController::Routing::Routes.draw do |map|
  map.connect '', :controller => 'welcome"
  map.connect ':controller/sevice.wsdl', :action => 'wsdl'
  map.connect ':controller/:action/:id'
end
  • link_toは遅いのでHTMLで直接書いた方が良いよ。
    • メンテコストが高くなるしBKくさいのでやらないよなぁ。

といった感じでした。id:ogijunさんによれば、こういうコードの最適化よりはスケーラビリティの面でのパフォーマンス云々の話の方が重要ということです。

自分がセッションを提案したのに全然オーナーとして知識も経験も不足していて参加された方には申し訳なかったです。

ARモデリング入門

セッションがなかなか出なかったので、Railsは使いやすいけど経験があまりないので実際どのようにモデルを作るかわからないと言ったらセッションになりました。

Yuguiさんとid:moroさんによるABDによるモデリング入門と言った感じでした。題材としてはソーシャルブックマークをモデリングしてみようという話でした。

まずPageとUserがいてそれらはリソース系のモデル。UserがPageをBookmarkするのでその間にイベント系のモデルBookmarkが出来る。テーブルの定義はこんな感じ。

  create_table :users do |t|
  end

  create_table :pages do |t|
    t.column :url, :string, :null => false
    t.column :title, :string
  end

  create_table :bookmarks do |t|
    t.column :page_id, :integer, :null => false
    t.column :user_id, :integer, :null => false
    t.column :create_on, :timestamp, :null => false
  end

モデルは次のようになる。

class User < ActiveRecoard::Base
  has_many :pages, :through => :bookmarks
  has_many :bookmarks
end

class Page < ActiveRecoard::Base
  has_many :users, :through => :bookmarks
  has_many :bookmarks
end

class Bookmark < ActiveRecoard::Base
  has_many :users, :pages
end
追記(06/06/20 08:17)

上の定義に誤りがありました。訂正します。

class User < ActiveRecord::Base
  has_many :pages, :through => :bookmarks
  has_many :bookmarks
end

class Page < ActiveRecord::Base
  has_many :users, :through => :bookmarks
  has_many :bookmarks
end

class Bookmark < ActiveRecord::Base
  belongs_to :user
  belongs_to :page
end

まずリソース系のモデルを作成してテストをし、テストが通ったらイベント系のモデルを作成して実装していくといった感じにすると良いそうです。たしかにどこをどう組み立てたら良いのかが少しクリアになった気がするし、この書き方であれば一つのテーブルの定義は比較的単純で済むので拡張しやすいのかなと思いました。

ちょっと頭をひねったのが自己参照をする場合。UserがUserをお気に入りに加えるといった場合、Userの間にイベント系のFavoriteモデルを置いて、それをRailsで書くときにどうするのってことになりました。確かhabtm使うしかないんじゃないか的な話になったような気がするんですが、詳しいところは他の人の調べを待とうかと思います。

一次会

たまたま自分の卒論のテーマと似たようなことをやろうとしてる方がいてその人と色々話をしました。高橋さんの執筆経験の話はそこらへんにとても参考になりました。やっぱりモチベーションを保つことが重要で、それにはシステムだけにはどうしようもないところがあるよねってことのようです。

それから国内外でRails本ラッシュが起きそうだけど、実際初心者の人がそれを取って色々アプリを使っても簡単に動かせる環境がないんじゃあダメだよね。と話をして、国内のRailsホスティングサービスがやっぱりほしいなぁという話になりました。

二次会

先延ばしにされたはぶさんへの返答をみんなで考えました。いろいろネタが出ましたが、そこらへんはid:yad-ELさんの感想を参照してください。
http://d.hatena.ne.jp/yad-EL/20060618/1150648909

id:moroさんの結婚披露宴は生い立ちを高橋メソッドで紹介というのに吹いた。

全体を通してのまとめ

  • Keep

今回自分は積極的に関われたと思う

  • Problem

なかなかセッションが上がらない。セッションを提案した人がオーナーになってしまうみたいなところが提案しづらいところではないか?

  • Try

もうちょっとRailsを使い倒してネタを持っていけるようになる。ネタについていけるようになる。

ひさびさにRailsをやる

ちょっと卒論で作ろうと思ってるシステムをRailsで作りたいなぁと思ってひさびさにRailsをやる。

題材は[Ruby][Rails] Ruby on RailsでWiki
http://d.hatena.ne.jp/muscovyduck/20060511#1147307789

以下少し違ったところと要点をまとめる。

開発&実行環境

walf443@ruribita:railswiki% script/about
About your application's environment
Ruby version 1.8.2 (i386-linux)
RubyGems version 0.8.11
Rails version 1.1.2
Active Record version 1.14.2
Action Pack version 1.12.1
Action Web Service version 1.1.2
Action Mailer version 1.2.1
Active Support version 1.3.1
Application root /home/walf443/railswiki
Environment development
Database adapter mysql
Database schema version 2

データベース周り

自分はMySQL4.0を使った。

後々FrontPageがないとハマることになるので、自分はマイグレーションスキーマを更に次のように変更した。

# db/migrate/002_initial_schema.rb

class Contents < ActiveRecord::Base
end

class InitialSchema < ActiveRecord::Migration
  def self.up
    Contents.create :title => 'FrontPage', :body => 'This is FrontPage.'
  end
  
  def self.down
    frontpage = Contents.find_by_title 'FrontPage'
    frontpage.destroy
  end
end

Contentsのアイテムを作成するので事前にクラスだけ定義しておくのがポイント。

$rake migrate

スキーマを上げたり戻したり出来るのでファイルは分けておいた方が良いと思われる。

self.upメソッドには今回追加する処理を記述。self.downメソッドには前回のスキーマに戻すためのメソッドを記述する。

Helper

Helperにはscript/generate helperとかないのかな。うまく動作しなかった。

題材のページでは一つのファイルにHelperを記述しているけど、こちらでは動かず。ということで以下のようにした。

walf443@ruribita:railswiki% cat app/helpers/railswiki_helper.rb
module RailswikiHelper
end

walf443@ruribita:railswiki% cat app/helpers/railswiki_helper/to_html_with_links.rb 
require 'rdoc/markup/simple_markup'
require 'rdoc/markup/simple_markup/to_html'

module RailswikiHelper
  class ToHtmlWithLinks < SM::ToHtml
    def handle_special_WIKIWORD(special)
      %Q|<a href="/railswiki/show/#{special.text}">#{special.text}</a>|
    end
    
    def handle_special_HTTP(special)
      %Q|<a href="#{special.text}">#{special.text}</a>|
    end
    
    def handle_special_BRACKET(special)
      %Q|<a href="/railswiki/show/#{special.text[2..-3]}">#{special.text[2..3]}</a>|
    end
  end
end

walf443@ruribita:railswiki% cat app/helpers/railswiki_helper/railswiki_template.rb 
require 'rdoc/markup/simple_markup'
require 'rdoc/markup/simple_markup/to_html'

module RailswikiHelper
  class RailswikiTemplate
    def initialize(view=nil)
      @view = view
    end

    def render(template, assigns)
      markup = SM::SimpleMarkup.new
      markup.add_special /\b([A-Z][a-z]+[A-Z]\w+)/, :WIKIWORD
      markup.add_special %r|(https?://\s+)|, :HTTP
      markup.add_special /(\[\[.+?\]\])/, :BRACKET
      markup.convert template, ToHtmlWithLinks.new
    end
  end
end

SM::Simpleがなかなか認識されない(Const missing)ので仕方なく両方のファイルにrequireした。こういうときはどこにrequireするものなのだろうか。

Controller・View

hoge :action => ... とかいう感じのメソッドってどちらかというとSymbolを渡したくなるのは自分だけだろうか。特に:actionはメソッド名を指しているのでSymbolの方がそれっぽい気がする。エディタ的には色がつかないので文字列の方が見やすいのだけど。

その他

WEBrickでサーバを起動したとき、.irbrcでプロンプトの設定をしているとActiveSupportの拡張によりエラーが発生するようです。具体的には以下のような記述。

  IRB.conf[:PROMPT][:SIMPLEST] = {
    :PROMPT_I => "",
    :PROMPT_S => "",
    :PROMPT_C => "",
    :RETURN => "  #=> %s\n\n"
  }  

エラーメッセージを頼りにunless IRB.conf[:PROMPT].nil? .. end で囲むと大丈夫になった。

まとめ

簡単にできると思っていたが割といろんな場面でハマった。というわけで色々作りながら覚えていこうかと思う。