odeの開発メモ日記

プログラマーやってます。今までの道のり ポケコンbasic→C(DirectX5 ネットやろうぜ)→Perl(ちょろっと)→Java→C#→Ruby→Android(Java)

capistranoでssh越しのサーバーに設置する場合

sshgatewayに接続した後、そこを踏み台として他のpc(Webサーバー,DBサーバー等)に繋いで作業する場合があると思います。
(設置先が自社サーバーではなく、外部のサーバーを使う場合やレンタルサーバー等で仮想環境を使用している場合等でしょうか)


capistranoのデフォルトでは直接接続できるサーバーを対象にしてますが、設定することで上記のようなssh越しの設置も可能です。

やり方

下記の2つのオプションを指定ですればOKです。

deploy.rb

# sshのgateway(踏み台)のIPを指定
set :gateway, "203.182.58.106"

# ファイルの転送にsvnでなく、ファイルコピーを使用する。大抵はssh越しのサーバーから開発のsvnは参照できないため。
set :deploy_via, :copy

注意点

  • gatewayになるサーバーと奥にある設置対象サーバーとでパスワードは同じにしなければならない。
  • idは基本一緒がいいが、一応変えられる。
    • ipの前に@で区切りでユーザー名を付けれる。
    • set :gateway, "root@203.182.58.106"
調査メモ

各サーバーごとに個別にパスワードを設定したいのでいろいろ調べたんですが最終的にソース見た限りできなそうでした。
(パスワードはそろえる方向で運用さんにお願いして解決しました。)

  • パスワードは設定ファイルのoptions[:password]を使っていた。
  • options[:password]が空の場合は入力プロンプトを表示して入力させるが、その値を保存して次に使用しているために違うサーバーでも同じパスワードが使用されてしまう。


cli/options.rb

        password = options.has_key?(:password)
        options[:password] = Proc.new { self.class.password_prompt } #パスワード入力を受け付けるProcを作成
        options[:password] = options[:password].call if password # このpassword変数は設定ファイルのではなく、コマンドラインの引数で -p, --passwordが指定された場合のもの

capistrano/ssh.rb

      password_value = nil

   ・・・・

      begin
        connection_options = ssh_options.merge(
          :password => password_value,
          :auth_methods => ssh_options[:auth_methods] || methods.shift
        )

        yield host, user, connection_options
      rescue Net::SSH::AuthenticationFailed
        raise if methods.empty? || ssh_options[:auth_methods]
        password_value = options[:password] #←ここで何故かパスワード入力Procが発動する。そんな発動の仕方あるの?Rubyの黒魔術でもあるのかな?
        retry
      end
    end

実行環境

rails(2.1.2)
capistrano (2.5.5)

cronで秒単位でアプリを起動させたい。

ご存知の通りcronでは一番短い単位で1分ごとにしかアプリを実行できない。

というわけで秒単位で起動できないかをググったら
30秒おきに稼動させたいならこうするらしい。

* * * * * myapp & sleep 30; myapp

けどこれだと5秒おきとかはcronの設定が大変なことに、、ましてや1秒おきの場合なんて、、


というわけで人生初のシェルスクリプトを書いてみました。
とてつもなくへっぽこなのは堪忍してくださいまし。
Rubyistを目指してるものとしてはわざわざシェルスクリプト勉強せずにrubyで書けばよかったと思うが、rubyを入れてない環境でも使えるのでちょこっと便利?)

スクリプト

myappという実行ファイルがある場合
同一フォルダに下記のスクリプトを用意する

最初のほうにある変数exeとsecondを適当に書き換えて使って下さい。


myapp_cron_second.sh

#!/bin/sh

# 起動させるアプリファイル
exe="`dirname $0`/myapp"

# 起動間隔(秒)
second=5


loopMaxCount=`expr 60 / $second - 1`
count=1
while [ $count -le $loopMaxCount ];
do
        echo "$count"
        if [ $count != $loopMaxCount ]
        then
                $exe & sleep $second;
        else
                $exe
        fi
        count=`expr $count + 1`
done

cronには下記で登録

* * * * * /usr/local/myapp_cron_second.sh
2重起動

なお、秒単位のアプリ稼動だと2重起動してしまう可能性が高くなります。
もしアプリの性質上2重起動がNGの場合は
下記URLの方の2重起動防止スクリプトが使えそうです。
http://d.hatena.ne.jp/zenpou/20080715/1216133151

備考

ちなみに秒単位起動を実現するのにcronexecというアプリがスマートそうなんですが
安定度もわからないのとメンテナンスされてなさそうなのでやめときました。

更新履歴
  • 2012/1/23 はてブコメントでa1102012さんからcrontabの*の数が違う指摘があったのを修正。

セッションがタイムアウトしてる状態でpostした場合

セッションをdbに格納してる場合は注意しないといけなそうです。

例えば検索フォームを表示したままで放置して
その後タイムアウトしてるのに検索をした場合(postする)
InvalidAuthenticityTokenになりました。

挙動

セッションのストア先により動作が変わるみたいです

  • dbにセッションを格納
    • InvalidAuthenticityToken発生
  • memcacheにセッションを格納
    • 特に問題なくactionに進む。処理がかけるのでここでタイムアウトしました画面を表示できるので問題なし。
  • クッキーセッションを格納
    • 使ってないのですが、ブラウザ閉じるまでタイムアウトしないので問題ないはず。

InvalidAuthenticityTokenをどうするか

コントローラーに制御がうつらないからどうしようもなさそうな気がする。
railsに手をいれればできるだろうけど)
422.htmlにタイムアウトしました系の文言表示するしかなさそう。(微妙ですが)
本来の目的のメッセージではないですが、そもそも普通の人は見ることはないってことで。。

実行環境

rails(2.1.2)

willcomの高速化サービスでIP spoofing attackエラー

問題

rails2.1のサイト(リバースプロクシ使用)をwillcomの高速化サービスのプロクシ(フロントプロクシ)を使うと
IP spoofing attack?!
というエラーになってしまいました。
(恐らくこのエラーを出すようになったのは2.1からっぽい。)

理由

ソースをおっていくとHTTPのリクエストヘッダーに
HTTP_CLIENT_IPとHTTP_X_FORWARDED_FORの
両方がある場合でかつIPがどちらも違う場合に起きるみたい。
(どちらもプロクシ経由時の要求元IPを返すヘッダー。リバプロ経由だとREMOTE_ADDRがリバプロのIPになるためこちらを参照する必要がある)
それで一体どっちのIPを使えばいいんだ、このインチキ野郎とエラーを出しているらしい。

恐らくHTTP_CLIENT_IPがフロントプロクシの要求元IPで
HTTP_X_FORWARDED_FORがリバースプロクシの要求元IPになっていると思われる。


HTTP_X_FORWARDED_FORの仕様として調べた限り

  • 複数のプロクシを経由するとカンマ区切りで複数くる
  • ただしリバースプロクシ(mod_proxy)を経由するとフロントプロクシで付与したHTTP_X_FORWARDED_FORは消される。

 (そのようなドキュメントは見つからなかったのですがうちのリバプロではこんな挙動でした。
  消えない動作の場合もあるみたいなのですが、何故消えるのかは不明。ver?)


そしてwillcomの仕様としては下記のどちらかと思われる

  • HTTP_CLIENT_IPとHTTP_X_FORWARDED_FORの両方を送ってきている。(元のHTTP_X_FORWARDED_FORはリバプロで削除)
  • HTTP_CLIENT_IPのみ送ってきている。(HTTP_X_FORWARDED_FORはリバースプロクシが付与)


このためHTTP_CLIENT_IPとHTTP_X_FORWARDED_FORが違くなると思われます。


同様の挙動をするプロクシを経由した場合でもこの問題が起きるでしょう。


HTTP_CLIENT_IPなんていうどこで改ざんされるかわからない信用できない値は使わなくてよいと思うのだけど
HTTP_CLIENT_IPを返すリバースプロクシがあった場合への対応かな?
そうだとすればモード切替があったほうがいいと思う。
でもってデフォルトはメジャーなHTTP_X_FORWARDED_FORみたいな。

解決策

  • apacheレベルでClient-IPヘッダーを削除する。
    • 下記の設定を追加?(試してはないです)
    • RequestHeader unset Client-IP
  • railsにモンキーパッチをあてる。
    • config/enviroment.rb
module ActionController
  class AbstractRequest
    def remote_ip
      remote_addr_list = @env['REMOTE_ADDR'] && @env['REMOTE_ADDR'].split(',').collect(&:strip)

      unless remote_addr_list.blank?
        not_trusted_addrs = remote_addr_list.reject {|addr| addr =~ TRUSTED_PROXIES}
        return not_trusted_addrs.first unless not_trusted_addrs.empty?
      end
      remote_ips = @env['HTTP_X_FORWARDED_FOR'] && @env['HTTP_X_FORWARDED_FOR'].split(',')

      # HTTP_CLIENT_IPを返すリバースプロキシの場合はtrueにする。railsっぽいconf化したいけどわからない。。
      if false
        if @env.include? 'HTTP_CLIENT_IP'
          return @env['HTTP_CLIENT_IP']
        else
          raise ActionControllerError.new("no header. HTTP_CLIENT_IP.")
        end
      end

      if remote_ips
        while remote_ips.size > 1 && TRUSTED_PROXIES =~ remote_ips.last.strip
          remote_ips.pop
        end

        return remote_ips.last.strip
      end

      @env['REMOTE_ADDR']
    end
  end
end

remote_ipメソッドの挙動

ちなみにこのエラーを出しているremote_ipメソッドを眺めてて思ったのが
HTTP_CLIENT_IPやHTTP_X_FORWARDED_FORを
書き換えればIP偽装できんでねって疑問。

結論からいくと問題なさそうです。

最初にREMOTE_ADDRがローカルIPかどうかのチェックをし、
グローバルIPならそのまま返します。

リバースプロクシ経由の場合はREMOTE_ADDRにリバースプロクシのローカルIPが入るので
HTTP_X_FORWARDED_FORか、HTTP_CLIENT_IPを使います。

HTTP_X_FORWARDED_FORは複数になる可能性があるので
最後のIPがリバースプロクシに繋いできたIPになるため最後のIPを使用。
ただし多段のリバースプロクシの可能性もあるためお尻のローカルIPのものを削除してから。

元ソース
actionpack-2.1.2\lib\action_controller\request.rb

    # Which IP addresses are "trusted proxies" that can be stripped from
    # the right-hand-side of X-Forwarded-For
    TRUSTED_PROXIES = /^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i

    # Determine originating IP address.  REMOTE_ADDR is the standard
    # but will fail if the user is behind a proxy.  HTTP_CLIENT_IP and/or
    # HTTP_X_FORWARDED_FOR are set by proxies so check for these if
    # REMOTE_ADDR is a proxy.  HTTP_X_FORWARDED_FOR may be a comma-
    # delimited list in the case of multiple chained proxies; the last
    # address which is not trusted is the originating IP.
    def remote_ip
      remote_addr_list = @env['REMOTE_ADDR'] && @env['REMOTE_ADDR'].split(',').collect(&:strip)

      unless remote_addr_list.blank?
        not_trusted_addrs = remote_addr_list.reject {|addr| addr =~ TRUSTED_PROXIES}
        return not_trusted_addrs.first unless not_trusted_addrs.empty?
      end
      remote_ips = @env['HTTP_X_FORWARDED_FOR'] && @env['HTTP_X_FORWARDED_FOR'].split(',')

      if @env.include? 'HTTP_CLIENT_IP'
        if remote_ips && !remote_ips.include?(@env['HTTP_CLIENT_IP'])
          # We don't know which came from the proxy, and which from the user
          raise ActionControllerError.new(<<EOM)
IP spoofing attack?!
HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect}
HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}
EOM
        end

        return @env['HTTP_CLIENT_IP']
      end

      if remote_ips
        while remote_ips.size > 1 && TRUSTED_PROXIES =~ remote_ips.last.strip
          remote_ips.pop
        end

        return remote_ips.last.strip
      end

      @env['REMOTE_ADDR']
    end

参考HP

IP spoofing attack関連の情報(英語でよくわかってないです)
http://iprog.com/posting/2008/08/rails_500_error_ip_spoofing_attack
http://rails.lighthouseapp.com/projects/8994/tickets/322

プロクシのヘッダについて
http://www.nurs.or.jp/~sug/homep/proxy/proxy7.htm

実行環境

rails(2.1.2)
apache(2.2.10)

単純にメンテナンス画面を出したい場合

routesで切り替えられます。(本当は上流のリバプロとか、アプリに機能を組み込むのがいいと思いますが。簡易的な技ということで)

routes.rbの上のほうに下記追加(上に書くのは一番最初に判定してもらうため)

  #map.connect "*anything", :controller=>"maintenance", :action=>"index"

これで
http:/hogehoge/123
http:/hogehoge/asdgabaswesew
のような存在しないページもメンテナンス画面に飛ばされます。

実行環境

rails(2.1.2)

クッキーを使わないでリクエストパラメータ(params)でセッションIDを渡す場合

諸事情でセッションIDがクッキーで渡されなく、リクエストパラメータ(クエリーストリングやpost等)
で渡される場合がたまーにあります。
(外部アプリとの連携をする場合等に発生するケースが多いかなと思います。)


こういう場合はsessionメソッドで:cookie_only=>falseを指定すれば
クッキーだけでなく、リクエストパラメータにある場合でも取得できるはずでした。。
ええ、過去形です。
rails2からは:cookie_only=>falseにしてもリクエストパラメータを参照しないように
変更されてました。。バグ?


問題の箇所
query_extension.rb

class CGI #:nodoc:
  module QueryExtension
    # Remove the old initialize_query method before redefining it.
    remove_method :initialize_query

    # Neuter CGI parameter parsing.
    def initialize_query
      # Fix some strange request environments.
      env_table['REQUEST_METHOD'] ||= 'GET'

      # POST assumes missing Content-Type is application/x-www-form-urlencoded.
      if env_table['CONTENT_TYPE'].blank? && env_table['REQUEST_METHOD'] == 'POST'
        env_table['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
      end

      @cookies = CGI::Cookie::parse(env_table['HTTP_COOKIE'] || env_table['COOKIE'])
      @params = {}
    end
  end
end

@params = {}としているのが原因です。
ここが実際にセッションIDを特定するのに参照している部分だからです。
標準ライブラリのセッションクラスはセッションIDを特定するのに最初にparamsを、なかったらクッキーを見に行く実装になってます。
なので@paramsが消されるとパラメータで見ることはできなくなってしまいます。

cgi/session.rb

	if request.key?(session_key)
	  session_id = request[session_key]
	  session_id = session_id.read if session_id.respond_to?(:read)
	end
	unless session_id
	  session_id, = request.cookies[session_key]
	end
	unless session_id
	  unless option.fetch('new_session', true)
	    raise ArgumentError, "session_key `%s' should be supplied"%session_key
	  end
	  session_id = create_new_id
	end




皆さんこの問題にはまっているみたいで特にjpmobileでは
携帯のためクッキーが使えないので常に:cookie_only=>falseを内部的に使用していました。
そのためrails2が出た当初はうまくうごかなかったようです。
すぐにパッチが出て、直ったみたいですが。
とうことで、このパッチを適用すれば大体は解決できます。

trans_sid.rb

        unless ::CGI::Session.private_method_defined?(:initialize_without_session_key_fixation)
          ::CGI::Session.class_eval do
            alias_method :initialize_without_session_key_fixation, :initialize
            def initialize(cgi, options = {})
              key = options['session_key']
              if cgi.cookies[key].empty?
                session_id = (CGI.parse(cgi.env_table['RAW_POST_DATA'])[key] rescue nil) ||
                  (CGI.parse(ENV['QUERY_STRING'] || cgi.query_string)[key] rescue nil)
                cgi.params[key] = session_id unless session_id.blank?
              end
              initialize_without_session_key_fixation(cgi, options)
            end
          end
        end

大抵は問題ないんですが、私はひっかかりました。。
ファイルアップロード等のmultipartなpostに対応してないみたいですorz
いろいろ対応してみたのですが(railsによるinitialize_queryの上書きをやめたり、rails1のにしてみたり)結局すぐは直し方はわからなかったのであきらめました。
(時間かければなおせそうですが)



なので取り急ぎの対応的には
直接sessionライブラリを使用して取得しちゃいました。

      session_options = HashWithIndifferentAccess.new
      session_options.merge!({
        :session_id => params[:session]
      }).merge!(ActionController::Base.session_options)
      session_tmp = CGI::Session.new(CGI.new, session_options )
      session_tmp[:hoge] = "hogehoge value"
      session_tmp.update
      session_tmp.close

(独自クラスをセッションにいれている場合は
ロード時にクラスがないという感じのエラーになる場合がありました。
その場合は手前でHoge.newとかで一度クラスを認識させると問題なくなります。)



multipartに対応しなくていいならjpmobileのパッチ適用後に下記が使えます。
特定のactionのみで、かつ特定のパラメータが来てた場合のみparamsの値をsession_idにする指定の仕方
(例:actionはhogeのみ。paramsにくるsession_idのkeyはsession_id_dayo。paramsのmodeがtestの場合のみ)

  session :only=>:hoge, :cookie_only=>false, :session_key => 'session_id_dayo',
  :if => Proc.new { |req|
    req.parameters[:mode] == "test"
    }

実行環境

rails(2.1.2)

dbのマスタースレーブに対応するためにacts_as_readonlyableを使ってみる

日本最大規模のrailsを使ったサイトのクックパッドで改良して
使ってるとのことなので、そこそこ使えるのだろうとのことで
改良するの覚悟でチャレンジしてみました。

下記がオフィシャルページ?(ただのblogの1ページだけども)
http://revolutiononrails.blogspot.com/2007/04/plugin-release-actsasreadonlyable.html

使い方

インストール

script/plugin install svn://rubyforge.org/var/svn/acts-as-with-ro/trunk/vendor/plugins/acts_as_readonlyable


database.yml

development:
  adapter: mysql
  database: hoge_development
  username: root
  password: hoge
  host: 192.168.xxx.xxx #マスターDBのIP
  encoding: utf8

  #下記を追加
  read_only:
    adapter: mysql
    database: hoge_development
    username: root
    password: hoge
    host: 192.168.xxx.xxx #スレーブDBのIP
    encoding: utf8

model(適用したいdbごとに)

class Fruit < ActiveRecord::Base
  acts_as_readonlyable :read_only #引数はdatabase.ymlで作った名前
end

これで下記のような動作になる。

r = Fruit.find(:first) # スレーブが読まれる。基本はスレーブ。rails2.1からは更新したフィールドの値しかupdateされないから問題はないと思われる。
r.field = 'value'
r.save! # 書き込みのときはマスターに書かれる。

r = Fruit.find(:first, :readonly => false) # マスターから読まれる。明示的にマスターから読みたい場合はオプション渡しで可能

r.reload # ドキュメントにはスレーブが読まれるように書いてあるが、実際はマスターから読まれる。ソース見ると途中で変更したくさい。
r.reload(:readonly => false) # スレーブから読まれる。
r.reload(:readonly => true) # マスターから読まれる。

tips

・スレーブを複数台にする場合はdbのロードバランサーを使用する。
 もし、プログラム側で行いたい場合は
 modelに
 acts_as_readonlyable [:first_read_only, :second_read_only]
 と複数のスレーブを指定できる。(database.ymlにも項目を追加する)

・全モデルにacts_as_readonlyableを適用したい場合は
config/environment.rbに下記を追加する

class << ActiveRecord::Base
  def read_only_inherited(child)
    child.acts_as_readonlyable :read_only
    ar_inherited(child)
  end
  
  alias_method :ar_inherited, :inherited
  alias_method :inherited, :read_only_inherited
end

ちなみに最初、↑のソースが何をするかわからなかったので
別のやりかたをrubyの黒魔術を使って書きました。。
以下に一応黒魔術テクニックとしてのせておきます。

#一度modelのクラスを全部requireし、認識できる状態にする。
Dir["#{RAILS_ROOT}/app/models/*.rb"].sort.each do |path|
  filename = File.basename(path)
  require "#{filename}"
end
#ActiveRecord::Baseを継承してるクラスを抽出し回して
#class_evalを使ってacts_as_readonlyableを実行させる。
Object.subclasses_of(ActiveRecord::Base).each do |klass|
  klass.class_eval do
    acts_as_readonlyable :read_only
  end
end

トランザクションの中ではマスターを見に行く。

・なんのためかはわからないが
 内部的にjoinsを渡していて且つselectを渡していない場合はreadonlyをtrueにしている。
 つってもreadonlyを渡していない場合はスレーブ見にいくから意味ないと思う。。
 けどなんかある気もするがわからない。。

注意点

・データを更新した後にすぐに更新後の値を見せる場合には
 スレーブへのデータ反映が遅くて古い値を表示してしまう可能性がある。

改良した点

・注意点で書いた「データを更新した後にすぐに更新後の値を見せる場合」への対応として
 DBのデータ更新後の一定時間はマスターを見るようにした。

controllers/application.rb

  acts_as_readonlyable_update_after_fix :expires=>10.second

・findするときにオプション渡しでスレーブをoffにする機能があるが
 デフォルトがスレーブはちょいと危険と思われるのでデフォルトでslaveをoffにし、通常はマスターを見るようにしてオプション渡しでslaveをonにさせた。
 負荷のかかる部分に明示的に適用させるスタイル。

・コントローラーごとにslave on/offをできるようにした。オプションでonly、exceptの指定も可能。

controllers/xxx.rb

  acts_as_readonlyable :only=>:index

・全モデルにacts_as_readonlyableを適用したい場合のメソッドを用意



改良版acts_as_readonlyableのソース。(acts_as_readonlyable.rbを直接更新してます。)

# Copyright (c) 2007 Revolution Health Group LLC. All rights reserved.

module ActiveRecord; module Acts; end; end

module ActiveRecord::Acts::ActsAsReadonlyable

  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods

    def acts_as_readonlyable(*readonly_dbs)
      readonly_dbs = readonly_dbs.flatten.collect(&:to_s)
      readonly_models = readonly_classes(readonly_dbs)
      if readonly_models.empty?
        logger.warn("Read only mode is not activated for #{ self }")
      else
        define_readonly_model_method(readonly_models)
        self.extend(FinderClassOverrides)
      end
      self.send(:include, FinderInstanceOverrides)
    end

    private

    def readonly_classes(dbs)
      dbs.inject([]) do |classes, db|
        if configurations[RAILS_ENV][db]
          define_readonly_class(db) unless ActiveRecord.const_defined?(readonly_class_name(db))
          classes << ActiveRecord.const_get(readonly_class_name(db))
        else
          logger.warn("No db config entry defined for #{ db }")
        end
        classes
      end
    end

    def readonly_class_name(db)
      "Generated#{ db.camelize }"
    end

    def define_readonly_class(db)
      ActiveRecord.module_eval %Q!
        class #{ readonly_class_name(db) } < Base
          self.abstract_class = true
          establish_connection configurations[RAILS_ENV]['#{ db }']
        end
      !
    end

    def define_readonly_model_method(readonly_models)
     (class << self; self; end).class_eval do
        define_method(:readonly_model) { readonly_models[rand(readonly_models.size)] }
      end
    end

    module FinderClassOverrides

      def find_every(options)
        run_on_db(options) { super }
      end

      def find_by_sql(sql, options = nil)

        # Called through construct_finder_sql
        if sql.is_a?(Hash)
          options = sql
          sql = sql[:sql]
        end

        run_on_db(options) { super(sql) }

      end

      def count_by_sql(sql, options = nil)
        run_on_db(options) { super(sql) }
      end

      #KmAdd comment out
      #何故必要なのだろう?validates_uniquness_ofでバグる原因になってるのでコメントアウトする。
      #      def construct_finder_sql(options)
      #        options.merge(:sql => super)
      #      end

      def set_readonly_option!(options) #:nodoc:
        # Inherit :readonly from finder scope if set.  Otherwise,
        # if :joins is not blank then :readonly defaults to true.
        unless options.has_key?(:readonly)
          if scoped?(:find, :readonly)
            options[:readonly] = true if scope(:find, :readonly)
            #KmAdd comment out
            #          elsif !options[:joins].blank? && !options[:select]
            #            options[:readonly] = true
          end
        end
        p options
      end

      def calculate(operation, column_name, options = {})
        run_on_db(options) do
          options.delete(:readonly)
          super
        end
      end


      private

      def run_on_db(options)
        if ((Thread.current['open_transactions'] || 0) == 0) and with_readonly?(options)
          run_on_readonly_db { yield }
        else
          yield
        end
      end

      def with_readonly?(options)
        #KmAdd start
        ext_readonly = Thread.current['acts_as_readonlyable_readonly']
        if ext_readonly != nil
          return ext_readonly
        end
        #KmAdd end

        #KmAdd comment out
        #        (! options.is_a?(Hash)) || (! options.key?(:readonly)) || options[:readonly]

        #KmAdd start
        if options.is_a?(Hash) && options.key?(:readonly)
          return options[:readonly]
        elsif Thread.current['acts_as_readonlyable_controller_readonly']
          return Thread.current['acts_as_readonlyable_controller_readonly']
        else
          return false
        end
        #KmAdd end
      end

      def run_on_readonly_db
        klass_conn = connection
        begin
          self.connection = readonly_model.connection
          self.clear_active_connection_name
          yield
        ensure
          self.connection = klass_conn
          self.clear_active_connection_name
        end

      end

    end

    module FinderInstanceOverrides

      # backport from 1.2.3 for 1.1.6 + disable readonly by default for reload - replication lag
      def reload(options = {})
        options[:readonly] = false unless options.has_key?(:readonly)
        clear_aggregation_cache
        clear_association_cache
        @attributes.update(self.class.find(self.id, options).instance_variable_get('@attributes'))
        self
      end

    end

  end

end

ActiveRecord::Base.send(:include, ActiveRecord::Acts::ActsAsReadonlyable)














#KmAdd start

module KmActsAsReadonlyable
  def self.apply_all_model
    #acts_as_readonlyableを全モデルに適用するオフィシャル方法
    class << ActiveRecord::Base

      def read_only_inherited(child)
        child.acts_as_readonlyable :read_only
        ar_inherited(child)
      end

      alias_method :ar_inherited, :inherited
      alias_method :inherited, :read_only_inherited
    end
  end

  module Controller
    def self.included(base)
      base.extend(ClassMethods)
    end

    module ClassMethods
      # コントローラーレベルでのスレーブ使用をONにする。
      def acts_as_readonlyable(options={})
        before_filter :only => options[:only], :except => options[:except] do |c|
          c.__send__ :acts_as_readonlyable_action, options
        end
      end

      # dbのデータを更新後、一定秒以内ならマスターを見に行くようにする。
      def acts_as_readonlyable_update_after_fix(options={})
        before_filter :only => options[:only], :except => options[:except] do |c|
          c.__send__ :bf_acts_as_readonlyable_update_after_fix, options
        end
        after_filter :only => options[:only], :except => options[:except] do |c|
          c.__send__ :af_acts_as_readonlyable_update_after_fix, options
        end
      end
    end

    private

    def acts_as_readonlyable_action(options)
      Thread.current['acts_as_readonlyable_controller_readonly'] = true
    end

    def bf_acts_as_readonlyable_update_after_fix(options)
      # dbのデータを更新後、一定秒以内ならマスターを見に行くようにする。
      #session[:last_db_update_at] = Time.now
      last_db_update_at = session[:last_db_update_at]
      expires = options[:expires] || 10.second
      p "-"*100
      p expires
      if last_db_update_at && (last_db_update_at + expires) > Time.now
        Thread.current['acts_as_readonlyable_readonly'] = false
      end
    end

    def af_acts_as_readonlyable_update_after_fix(options)
      if last_db_update_at = Thread.current['acts_as_readonlyable_last_db_update_at']
        session[:last_db_update_at] = last_db_update_at
      end
    end
  end
end

ActionController::Base.class_eval do
  include KmActsAsReadonlyable::Controller
end

# acts_as_readonlyable_update_after_fixのためのdb更新を検知する部分
Object.subclasses_of(ActiveRecord::ConnectionAdapters::AbstractAdapter).each do |klass|
  klass.class_eval do
    def execute_with_acts_as_readonlyable(sql, name = nil)
      val = execute_without_acts_as_readonlyable(sql, name)

      sql = sql.strip.upcase
      if sql.starts_with?("SET") || sql.starts_with?("SHOW") || sql.starts_with?("SELECT") || sql.starts_with?("EXPLAIN") || sql.starts_with?("BEGIN") || sql.starts_with?("COMMIT") || sql.starts_with?("ROLLBACK")
        return val
      end

      Thread.current['acts_as_readonlyable_readonly'] = false
      Thread.current['acts_as_readonlyable_last_db_update_at'] = Time.now
      #      p "set acts_as_readonlyable_last_db_update_at. #{sql}"

      return val
    end
    alias_method_chain :execute, :acts_as_readonlyable
  end
end



#KmAdd end

改良verを使った適用の仕方

・全モデルにacts_as_readonlyableを適用
・applicationコントローラーにacts_as_readonlyable_update_after_fixを適用。
・トップページ等、負荷のかかるスレーブを使いたいページにコントローラーレベルのacts_as_readonlyableを適用する。

実行環境

rails(2.1.2)