capistranoでssh越しのサーバーに設置する場合
sshでgatewayに接続した後、そこを踏み台として他の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
単純にメンテナンス画面を出したい場合
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)