[rails] ActiveRecord3の connection_pool の挙動を調べた

基本的にRailsでは単一のDBを使用するように設計されています。とはいえ負荷軽減のためだったり、あるいは様々なしがらみのために複数のDBに接続しなければいけない場合がたまにあったりすると思います。しかも残念な事に違うDBに同じ名前のテーブルがあったりしてどうすんだよコレとなることも無いとは言えないでしょう。

ActiveRecord が connection pool に対応したのは知識としては知っていましたが複数のDBに実際につなぎにいったばあい connection はどうなるの?と疑問に思ったので調べてみました。

# activerecord-3.0.3/lib/active_record/connection_adapters/abstract/connection_specification.rb 51行目 - 82行目
    def self.establish_connection(spec = nil)
      case spec
        when nil
          raise AdapterNotSpecified unless defined?(Rails.env)
          establish_connection(Rails.env)
        when ConnectionSpecification
          self.connection_handler.establish_connection(name, spec)
        when Symbol, String
          if configuration = configurations[spec.to_s]
            establish_connection(configuration)
          else
            raise AdapterNotSpecified, "#{spec} database is not configured"
          end
        else
          spec = spec.symbolize_keys
          unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end

          begin
            require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
          rescue LoadError => e
            raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e})"
          end

          adapter_method = "#{spec[:adapter]}_connection"
          if !respond_to?(adapter_method)
            raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter"
          end

          remove_connection
          establish_connection(ConnectionSpecification.new(spec, adapter_method))
      end
    end

ActiveRecord::Base.establish_connection の実体は lib/active_record/connection_adapters/abstract/connection_specification.rb にありました。

establish_connection に文字列やシンボルを渡した場合それに相当する configuration から ConnectionSpecifiction のインスタンスを作成し、再び establish_connection を呼び出します。 そして establish_connection が ConnectionSpecification を受けとりますと今度は connection_handler の establish_connection を読んでいます。途中の過程はさておき connection_handler の establish_connection を呼びたいようです。

じゃあ connection_handler って何よ?と深追いしてみます。

# activerecord-3.0.3/lib/active_record/connection_adapters/abstract/connection_specification.rb 10行目 - 14行目
    ##
    # :singleton-method:
    # The connection handler
    class_attribute :connection_handler, :instance_writer => false
    self.connection_handler = ConnectionAdapters::ConnectionHandler.new

同じファイルの14行めにそれはありました。ご丁寧にシングルトンですよとコメント付きです。そしてその実体は ConnectionAdapters::ConnectionHandler のインスタンスでした。
ということで舞台は ConnectionAdapters::ConnectionHandler クラスへ移ります。

# activerecord-3.0.3/lib/active_record/connection_adapters/abstract/connection_pool.rb 278行目 - 287行目
    class ConnectionHandler
      attr_reader :connection_pools

      def initialize(pools = {})
        @connection_pools = pools
      end

      def establish_connection(name, spec)
        @connection_pools[name] = ConnectionAdapters::ConnectionPool.new(spec)
      end

ConnectionAdapters::ConnectionHandler は new すると @connection_pools という変数をハッシュで初期化します。シングルトンインスタンスインスタンス変数なのでこれもシングルトンです。そして establish_conection メソッドは ConnectionAdapters::ConnectionPool のインスタンスを作成し、name と紐付けて @connection_pools に格納しています。


はて? name って何だっけ? ということで ActiveRecord::Base.establish_connection に戻ってみますと name は仮引数や変数としては使われていないのでメソッド呼び出しであろうことがわかります。調べてみても name というメソッドは定義されていないので、 Module#name のことでしょう。リファレンスマニュアルによると「モジュールやクラスの名前を文字列で返します。」とありますので、establish_connectionをcallしたレシーバ名前の文字列です。


ということでシングルトンの @connection_pools に establish_connection のレシーバの数だけ connection_pool ができることになるということがわかりました。


自分で establish_connection をしない限りは rails の初期化時に ActiveRecord::Base.establish_connection が呼ばれるので通常はすべてのテーブルのアクセスが ActiveRecord::Base の connection_pool を使用することになります。

で、それが本当にそうなのか検証してみました。

require 'rubygems'
require 'active_record'

yml = <<EOS
common: &common
  adapter: sqlite3
  pool: 5
  timeout: 5000
base:
  <<: *common
  database: base.sqlite3
one:
  <<: *common
  database: one.sqlite3
EOS

ActiveRecord::Base.configurations = YAML.load(yml)


class CreateTable < ActiveRecord::Migration
  def self.up
    unless  connection.table_exists?(:users)
      create_table :users do |t|
        t.string :name
        t.integer :name

        t.timestamps
      end
    end
  end
end

[:base, :one].each{|env|
  ActiveRecord::Base.establish_connection(env)
  ActiveRecord::Migration.verbose = false
  CreateTable.up
}


class User < ActiveRecord::Base
end

class Ua < ActiveRecord::Base
  set_table_name 'users'
  establish_connection(:one)
end

class Ub < ActiveRecord::Base
  set_table_name 'users'
  establish_connection(:one)
end

class One < ActiveRecord::Base
  establish_connection(:one)
end

class U1 < One
  set_table_name 'users'
end

class U2 < One
  set_table_name 'users'
end

[User, Ua, Ub, U1, U2].each{|klass| p klass.name + ": "  + klass.connection_pool.object_id.to_s }
p ActiveRecord::Base.connection_handler.connection_pools.keys
p Hash[ActiveRecord::Base.connection_handler.connection_pools.map{|k, v| [k, v.object_id]}]

実行結果

$ ruby connection_pool.rb
"User: 75816690"
"Ua: 75730560"
"Ub: 75717530"
"U1: 75799790"
"U2: 75799790"
["ActiveRecord::Base", "One", "Ua", "Ub"]
{"ActiveRecord::Base"=>75816690, "One"=>75799790, "Ua"=>75730560, "Ub"=>75717530}

establish_connection を行わない場合は ActiveRecord::Base の connection_pool を使用します。
クラスで直接 establish_connection を行った場合はそのクラスが独自に connection_pool を持つことになります。 establish_connection を行ったクラスを継承した場合は establish_connection を行ったスーパークラスの connection_pool を使うことになります。

ということで各クラスで establish_connection をしてしまうと connection_pool の最大値(デフォルトでは5) x クラスの数 だけconnectionが保持できてしまうので非常によろしくない状態になります。
何かしらのやんごとない理由で別DBに接続しなければいけない場合は connection_pool 用のクラスを用意してそれを継承する形で使ったほうがよいです。

はぁ疲れた。

[rails] 任意の位置にエラーメッセージを表示するViewヘルパー

ActionView::Helpers::FormBuilder.module_eval do
  def error_message(attribute)
    @object.errors[attribute].map{|error| '<span class="error">' + error + '</span>'}.join("<br>").html_safe
  end
end

これを使うと

<%= form_for(@user) do |f| %>
  <%= f.label :name %>
  <%= f.text_field :name %>
  <%= f.error_message :name %>
<% end -%>

みたいに書ける

[rails] Rails3からSinatraを呼ぶ

Rails3からはSinatraとか他のRackベースアプリケーションが呼べるというのは様々な記事に載っているのですが、実例が見つからないので実際にやってみました。

1. railsプロジェクトを作成する

$ rails new rinatra

2. sinatraアプリケーションを作成する

$ mkdir sinatra
$ emacs sinatra/app.rb

作成したsinatraアプリケーションは以下のとおり。っていうか単なるハローワールド

require 'rubygems'
require 'sinatra'

get '/hello' do
  "Hello Sinatra"
end

3. rails側でsinatraをbundleしておく

$ cd rinatra
$ vi Gemfile
source 'http://rubygems.org'

gem 'rails', '3.0.0'
gem 'sinatra' #<= これを書き足す

# Bundle edge Rails instead:                                                                        
# gem 'rails', :git => 'git://github.com/rails/rails.git'                                           

gem 'sqlite3-ruby', :require => 'sqlite3'
(以下略)
$ bundle install vendor/bundle

4. sinatraアプリケーションをrequireする

$ vi config/initializers/sinatra.rb
require "#{Rails.root}/../sinatra/app"

5. routesを設定してsinatraアプリケーションを呼べるようにする(ここ本命)

$ emacs config/routes.rb
Rinatra::Application.routes.draw do
  mount Sinatra::Application, :at => 'sinatra' #<= これを書き加える

(以下略)

とここまでやったった上で rails server を起動して
http://localhost:3000/sinatra/helloにアクセスすると

sinatra のハローワールドが表示されました。

ちなみに rake routes はこんな感じ

$ rake routes
(in /home/yalab/project/rinatra)
sinatra  /sinatra {:action=>"sinatra", :to=>Sinatra::Application}

ただ http://localhost:3000/sinatra にアクセスして sinatra の get '/' を呼ぶことはなぜかできませんでした。この辺り詳しい方いましたらコメントで教えていただけるとうれしいです。

[rails] Rails3.0 beta3のmailでiso-2022-jp

世の中だいぶUTF-8が浸透して文字化けもあまり見なくなった昨今ですが、
未だUTF-8化してない悩ましいものの一つに日本語メールがあります。
rails3のActionMailer(というよりかはmail gem)はだいぶ良くなったのですが、
まだそれができなかったのでモンキーパッチを書いちゃいました。

# encoding: utf-8
require 'mail'
Mail::UnstructuredField.module_eval do
  def encode_with_fix(value)
    encode_without_fix(value.encode(charset))
  end
  alias_method_chain :encode, :fix
end

Mail::Message.module_eval do
  def charset=(value)
    @defaulted_charset = false
    @charset = value
    @header.charset = value
    @body.charset   = value
  end
end

Mail::Body.module_eval do
  def encoded_with_fix(transfer_encoding = '8bit')
    dec = Mail::Encodings::get_encoding(encoding)
    if multipart? ||  transfer_encoding == encoding and dec.nil?
      encoded_without_fix(transfer_encoding)
    else
      enc = Mail::Encodings::get_encoding(get_best_encoding(transfer_encoding))
      enc.encode(dec.decode(raw_source).encode(charset))
    end
  end
  alias_method_chain :encoded, :fix
end

このファイルをconfig/initializers以下に例えばmail_encoding.rbとでもして置いておいて
mailerで

class Notifier < ActionMailer::Base
  default :from => ADMIN_EMAIL, :charset => 'iso-2022-jp'

  def signup(account)
    @account = account
    mail :to => @account.email, :subject => t('mail_subject_notifier_signup')
  end
end

のようにしてやるとiso-2022-jpエンコードされたメールが送信できます。
っていうかMail::Bodyにcharsetを渡してない時点でそれなりに酷いバグじゃないかとか思ったりもするわけですが。

ちなみにmultipartなメールのテストはしてないのでアシカラズ。

[android] Softbank MMS On Froyo with ?.vodafone.ne.jp

モペログさんがせっかくMms.apkのUser-Agentを書き換えてSoftbankで使えるようにしてくれたのだけれど。未だに@k.vodafone.ne.jpのアドレスを使っている僕は残念ながら上手くMMSを使うことができなかった。
仕方がないので「ChimCity: Nexus Oneをアップデートしたら銀SIMでMMS送受信が出来たでござる」の記事を参考に自分でUserAgentを書き換えてやった。
書き換えたものはこちら
適用方法はモペログさんの記事を参考に

$ sudo adb remount
$ sudo adb push Mms_V702NK.apk /system/app/Mms.apk

もちろんご使用は自己責任でお願いします。

[server] nginxのinitスクリプト

httpdのinitスクリプトを参考にnginxのinitスクリプトを書いた。
環境はCentOS 4.6なのでRedhat系なら使えます。

#!/bin/bash

# Startup script for the Nginx Web Server
#
# chkconfig: - 85 15
# description: Nginx is a Light weight World Wide Web server.  It is used to serve \
#              HTML files and CGI.
# processname: nginx
# pidfile: /var/run/nginx.pid
# config: /etc/nginx/nginx.conf

. /etc/rc.d/init.d/functions

nginx='/usr/local/nginx/sbin/nginx'
prog=nginx
RET=0

start () {
  echo -n $"Starting $prog: "
  daemon $nginx
  RET=$?
  echo
  return $RET
}

stop () {
  echo -n $"Stopping $prog: "
  killproc $nginx
  RET=$?
  echo
  return $RET
}

reload() {
  echo -n $"Reloading $prog: "
  killproc $nginx -HUP
  RET=$?
  echo
  return $RET
}

case "$1" in
  start)
    start
  ;;
  stop)
    stop
  ;;
  restart)
    stop
    start
  ;;
  reload)
    reload
  ;;
  *)
    echo $"Usage: $prog {start|stop|restart|reload}"
    exit 1
  ;;
esac
exit $RET

上のスクリプトを/etc/init.d/nginxとして保存して自動起動するようにする。

# chkconfig --add nginx
# chkconfig nginx on
# chkconfig httpd off //ついでにapacheが自動起動しないようにする