classをカスタマイズするために再定義じゃなくてaliasしないとだめな場合がある話
当たり前といえば当たり前なaliasの挙動の話です。
わかってたはずなのに、とある状況だと気づかなかったのでメモります。
今回はまった部分は以下の部分の修正
・railsでdb保存した場合に自動でcreated_at列に現在の時間が入る機能。
これををレガシーDBの列名がdt_creationなのでそれに対応したいと思いました。
(列名に型のprefix名がついちゃってるとか本当かんべんしてー)
オリジナルのコード
module ActiveRecord module Timestamp def self.included(base) #:nodoc: base.alias_method_chain :create, :timestamps --- 省略 --- end --- 省略 --- private def create_with_timestamps #:nodoc: if record_timestamps current_time = current_time_from_proper_timezone write_attribute('created_at', current_time) if respond_to?(:created_at) && created_at.nil? write_attribute('created_on', current_time) if respond_to?(:created_on) && created_on.nil? --- 省略 --- end create_without_timestamps end --- 省略 --- end end
これを下記のようにcreate_with_timestampsメソッドを再定義しても動きません。
再定義ver(NG)
module ActiveRecord module Timestamp def create_with_timestamps #:nodoc: if record_timestamps current_time = current_time_from_proper_timezone write_attribute('dt_creation', current_time) if respond_to?(:dt_creation) && dt_creation.nil? end end end end
aliasで元のcreateを上書きすればOK
alias ver(OK)
module ActiveRecord class Base def create_with_timestamps_regacy #:nodoc: current_time = current_time_from_proper_timezone write_attribute('dt_creation', current_time) if respond_to?(:dt_creation) && dt_creation.nil? create_without_timestamps_regacy end alias_method_chain :create, :timestamps_regacy end end
これはaliasした場合、古いメソッド名のほうを再定義しても新しいメソッド名には反映されないからです。
簡単なtestコード
def test "test" end def test_new "test_new" end alias test test_new def test_new "test_new_override" end p test # test_newと表示(test_newの再定義は反映されない)
よくよく考えるといつもよくやる下記の動作ができてるってことはそりゃそうだと思った。
http://www.ruby-lang.org/ja/man/html/_A5AFA5E9A5B9A1BFA5E1A5BDA5C3A5C9A4CEC4EAB5C1.html#alias
# メソッド foo を定義 def foo "foo" end # 別名を設定(メソッド定義の待避) alias :_orig_foo :foo # foo を再定義(元の定義を利用) def foo _orig_foo * 2 end p foo # => "foofoo"
もし、古いメソッド名のほうを再定義して新しいメソッドにも影響あったら上記のやりたいことできないもんね。
aliasの内部的な仕組みを想像するとメソッド定義のコピーであってメソッドのポインタをコピーしてるわけではなさそう。
もしくは再定義するとオリジナルのメソッドポインタとは違う、別のメソッドポインタで作られているのかもしれない。
まとめ
カスタマイズするには再定義ではだめな場合がある(aliasされて実装されてる機能の場合)
目には目を、aliasにはaliasをw
set_primary_keyではまった。というかRailsレシピのあほー。set系メソッドでやっちゃった話
昔のアプリのDBにActiveRecordで繋げる必要がでたので
プライマリキーの名前を変更するset_primary_keyというメソッドを使いました。
そしたら動かない。。idはない列ですとでる。
なんでだろ。。
Railsレシピの通りに書いてたのに。
(間違い版)
set_primary_key = :hoge_id
通りすがりの同僚がチェック。
あれ、これsetなんだから=いらなくない?
あー、そういえば。
(正しい版)
set_primary_key :hoge_id
と書いてみると。。
動いたーーーー!!!
railsレシピのあほーー。
しかしなんでエラーでないんだ!
と、ソースを見たらこういうことだった。
set_primary_keyの定義 def set_primary_key(value = nil, &block)
引数なしでも動くじゃん。
ってことは
set_primary_keyが引数なしで実行され、その戻り値に=で代入するという
意味のないことをしていたのね。。
これは今回以外の場合でも気づかずにやっちゃいそうだ。。
おいら以外に犠牲者がでないことを祈ります。
複数プロセスからのファイル書き込み(追記の場合)における注意点
実行条件
100プロセス同時に5000行の書き込みを行う。
テストコード
100.times do pid = fork do open(File.join(File.dirname(File.expand_path(__FILE__)),"aaaaaaaaa.log"), 'a') do |f| f.sync = true # syncしないと文字列が途中でおかしくなる場合がある。 5000.times do time = Time.now f.write "[#{time.strftime("%Y/%m/%d %H:%M:%S")}][#{$$}]test\n" #OK #f.puts "[#{time.strftime("%Y/%m/%d %H:%M:%S")}][#{$$}]test" #NG #f.puts "[#{time.strftime("%Y/%m/%d %H:%M:%S")}][#{$$}]test\n" #これはOK end end end Process.detach(pid) end
気をつけること
railsのLogは?
ちゃんと上記対策やってましたので問題なく複数プロセスで使っていいと思います。
これ調べるまで少し不安な部分がありましたけど安心しました(^^;
putsは使わないでwriteを使う理由
どうやら本文と改行を別々に書きこんでるみたいです。
そのため下記のような症状が出ます。
2行のはずが1行にまとまってしまったパターン
[2009/11/17 16:45:48][32628]test[2009/11/17 16:45:48][32626]test
上記で消えた改行がよそにいって1行空いてしまうパターン
[2009/11/17 16:45:46][32614]test [2009/11/17 16:45:46][32614]test [2009/11/17 16:45:46][32612]test [2009/11/17 16:45:46][32612]test
(たまにあるぐらいです、よく中身を見ましょう。大体10行ぐらいでしょうか。)
File#putsのソースを見て確認しました。(これは1.8.6のですが現時点の最新版のruby1.9.1-p243でも同様でした)
io.c
rb_io_puts(argc, argv, out) int argc; VALUE *argv; VALUE out; { int i; VALUE line; /* if no argument given, print newline. */ if (argc == 0) { rb_io_write(out, rb_default_rs); return Qnil; } for (i=0; i<argc; i++) { if (NIL_P(argv[i])) { line = rb_str_new2("nil"); } else { line = rb_check_array_type(argv[i]); if (!NIL_P(line)) { rb_protect_inspect(io_puts_ary, line, out); continue; } line = rb_obj_as_string(argv[i]); } rb_io_write(out, line); // 本文を書き込んでる if (RSTRING(line)->len == 0 || RSTRING(line)->ptr[RSTRING(line)->len-1] != '\n') { rb_io_write(out, rb_default_rs); // 改行がない場合に改行を書き込んでる } } return Qnil; }
syncをする理由
下記の症状になります。
文字の途中に混ざっちゃいます。
[2009/11/17 16[2009/11/17 16:57:22][318]test
(たまにあるぐらいです、よく中身を見ましょう。大体5行ぐらいでしょうか。)
rubyでデコメのパース
前書き
受信したデコメをパースしたいと思います。
- パーサーにはTMailを使用します。
- Railsは使用しません。
- 空メの受け取り部分については書いてません。空メの内容を受け取った後の処理になります。
- 別にデコメに限らずただのテキストメールのパース、携帯でないHTMLメールのパースにも使えると思います。
取得する内容は下記のとおり
- from,toアドレス
- 件名
- テキスト本文
- HTML本文
- 添付画像(ファイル名、CID、バイナリ)
つまづいた点(解決済み)
- 添付ファイルのCID(content-id)の取得
- ドコモだけcidがとれなかった。
- subjectの文字コード問題
そのほかの問題への対策としてMbMailを使わせて頂きました。
下記の対策がなされています。
- 携帯等のRFC違反のドットが連続するアドレスが扱えるよう、TMail::Parser を置き換え。
- 機種依存文字の対応(絵文字等)、文字コード変換器に NKF を使用するように変更。デフォルトで使われるiconvだと対応していない。
multipartのパースのためのutilとしても使いました。(privateメソッドだったので若干ソース修正してます)
http://tmty.jp/tag/mbmail/
これらの問題を解決するためにTMailにパッチをあてます。
私の場合はMbMailに含まれるTMailのソースに上書きしました。
また、MbMailがRailsに依存しないで動くように修正してあります。
ソース
メイン文
$KCODE = 'u' require 'rubygems' require 'tmail' require 'tmtysk-mbmail/lib/mb_mail.rb' def parse_mail(file_data) puts "---------------------" mail = MbMail::DMail.parse(file_data) mail.organize_mail_parts p "to: "+mail.to.to_s # toは配列 p "from: "+mail.from.to_s # fromは配列 p "subject: #{mail.subject}" p "text: "+mail.text_part.body html = "" unless mail.html_part.nil? html = mail.html_part.body end p "html: "+html mail.in_lined_image_parts.each_with_index do |ip, index| # なぜかドコモだけcidがとれなかった。内部の@bodyには入ってたので無理やり取得する。 #p "cid: "+ip["content-id"].id.to_s content_id_ip = ip["content-id"] def content_id_ip.inner_body @body end content_id = "" unless content_id_ip.inner_body.nil? content_id = content_id_ip.inner_body.strip # cidは<>で囲まれてるので除去する。 if content_id[0, 1] == "<" content_id = content_id[1, content_id.length-1] end if content_id[content_id.length-1, 1] == ">" content_id = content_id[0, content_id.length-1] end end file_name = (ip['content-location'] && ip['content-location'].body) || ip.sub_header("content-type", "name") || ip.sub_header("content-disposition", "filename") file_name = TMail::Unquoter.unquote_and_convert_to(file_name, 'utf-8') p "file #{index+1}" p "file_name: #{file_name}" p "cid: #{content_id}" #p ip.body #ファイルのバイナリ end puts "" puts "" end file_names = ["/tmp/test1.eml", "/tmp/test2.eml"] file_names.each do |file_name| file = File.open(file_name) do |f| parse_mail f.read end end
tmtysk-mbmail/lib/mb_mail/dmail.rb
# ode改良 start railsでなくなった対応 require 'base64' # ode改良 end # 携帯メール対応モジュール module MbMail # HeaderField 操作用に TMail から移管 class HeaderField < TMail::HeaderField; end # デコメールクラス class DMail < TMail::Mail def []=( key, val ) dkey = key.downcase if val.nil? @header.delete dkey return nil end case val when String header = new_hf(key, val) when HeaderField # HeaderField が与えられた場合、そのままヘッダに代入する header = val when Array ALLOW_MULTIPLE.include? dkey or raise ArgumentError, "#{key}: Header must not be multiple" @header[dkey] = val return val else header = new_hf(key, val.to_s) end if ALLOW_MULTIPLE.include? dkey (@header[dkey] ||= []).push header else @header[dkey] = header end val end # docomo のデコメールフォーマットに変換する def to_docomo_format converted_for_carrier(:docomo) end # au のデコレーションメールフォーマットに変換する def to_au_format converted_for_carrier(:au) end # softbank のデコレメールフォーマットに変換する def to_softbank_format converted_for_carrier(:softbank) end protected # 指定された content-type のパーツを取得する def get_specified_type_parts(mimetype) specified_type_parts = [] specified_type_parts << self if Regexp.new("^#{mimetype}$", Regexp::IGNORECASE) =~ self.content_type if /^multipart\/(.+)$/ =~ self.content_type then self.parts.each do |p| specified_type_parts += p.get_specified_type_parts(mimetype) end end specified_type_parts end private # 指定のキャリアのデコメールフォーマットに変換する # 現時点では、:docomo, :au, :softbank のみ対応 def converted_for_carrier(carrier = :docomo) organize_mail_parts dm = MbMail::DMail.new self.header.each do |key,value| next if key == 'content-type' # content-type は引き継いだらダメ dm[key] = value.to_s end dm.body = "" dm.content_type = 'multipart/mixed' # text/plain パートの作成 tp = MbMail::DMail.new case carrier when :docomo tp.content_type = 'text/plain; charset="Shift_JIS"' tp.transfer_encoding = 'Base64' tp.body = Base64.encode64(Jpmobile::Emoticon::unicodecr_to_external(NKF.nkf('-m0 -x -Ws', @text_part.body), Jpmobile::Emoticon::CONVERSION_TABLE_TO_DOCOMO)) when :au tp.content_type = 'text/plain; charset="Shift_JIS"' tp.transfer_encoding = 'Base64' tp.body = Base64.encode64(Jpmobile::Emoticon::unicodecr_to_external(NKF.nkf('-m0 -x -Ws', @text_part.body), Jpmobile::Emoticon::CONVERSION_TABLE_TO_AU)) when :softbank tp.content_type = 'text/plain; charset="UTF-8"' tp.transfer_encoding = 'Base64' table = Jpmobile::Emoticon::CONVERSION_TABLE_TO_SOFTBANK emoticon_converted = @text_part.body.gsub(/&#x([0-9a-f]{4});/i) do |match| unicode = $1.scanf("%x").first case table[unicode] when Integer [(table[unicode].to_i-0x1000)].pack('U') when String table[unicode] else match end end tp.body = Base64.encode64(emoticon_converted) else tp.content_type = 'text/plain; charset="UTF-8"' tp.transfer_encoding = 'Base64' tp.body = Base64.encode64(@text_part.body) end # text/html パートの作成 # ode改良 start テキストメールのみの場合対応 if @html_part.nil? hp = nil else hp = MbMail::DMail.new case carrier when :docomo hp.content_type = 'text/html; charset="Shift_JIS"' hp.transfer_encoding = 'Base64' hp.body = Base64.encode64(Jpmobile::Emoticon::unicodecr_to_external(NKF.nkf('-m0 -x -Ws', @html_part.body), Jpmobile::Emoticon::CONVERSION_TABLE_TO_DOCOMO)) when :au hp.content_type = 'text/html; charset="Shift_JIS"' hp.transfer_encoding = 'Base64' hp.body = Base64.encode64(Jpmobile::Emoticon::unicodecr_to_external(NKF.nkf('-m0 -x -Ws', @html_part.body), Jpmobile::Emoticon::CONVERSION_TABLE_TO_AU)) when :softbank hp.content_type = 'text/html; charset="UTF-8"' hp.transfer_encoding = 'Base64' table = Jpmobile::Emoticon::CONVERSION_TABLE_TO_SOFTBANK emoticon_converted = @html_part.body.gsub(/&#x([0-9a-f]{4});/i) do |match| unicode = $1.scanf("%x").first case table[unicode] when Integer [(table[unicode].to_i-0x1000)].pack('U') when String table[unicode] else match end end hp.body = Base64.encode64(emoticon_converted) else hp.content_type = 'text/plain; charset="UTF-8"' hp.transfer_encoding = 'Base64' hp.body = Base64.encode64(@html_part.body) end end # ode改良 end # キャリアによって multipart 構成を分岐 alt_p = MbMail::DMail.new alt_p.body = "" alt_p.content_type = 'multipart/alternative' alt_p.parts << tp # ode改良 start テキストメールのみの場合対応 if hp alt_p.parts << hp end # ode改良 end case carrier when :au dm.parts << alt_p @in_lined_image_parts.each do |ip| dm.parts << ip end @attached_image_parts.each do |ap| dm.parts << ap end else rel_p = MbMail::DMail.new rel_p.body = "" rel_p.content_type = 'multipart/related' rel_p.parts << alt_p @in_lined_image_parts.each do |ip| rel_p.parts << ip end dm.parts << rel_p @attached_image_parts.each do |ap| dm.parts << ap end end dm end # オリジナルのメール構成を解析し、各パーツに分離する def organize_mail_parts @text_part = get_specified_type_parts('text/plain').first @html_part = get_specified_type_parts('text/html').first # いったん全ての画像をインライン扱いとし、 # その後本文から参照されていない画像を添付扱いとする # au でいずれの添付タイプについても同等に扱われているため @in_lined_image_parts = get_specified_type_parts('image/(gif|jpeg|jpg)') @in_lined_image_parts.map do |ip| ip.content_disposition = 'inline' end @attached_image_parts = [] @in_lined_image_parts.delete_if do |ip| if /TMail\:\:MessageIdHeader\s\"<([\.@_0-9a-zA-Z]+)>/ =~ ip["content-id"].inspect then unless Regexp.new("src=['\"]cid:#{$1}", Regexp::IGNORECASE) =~ @html_part.body then ip.content_disposition = 'attachment' @attached_image_parts << ip true end else false end end end # ode改良 start 外部から使えるようにする。 public :organize_mail_parts public attr_accessor :text_part, :html_part, :in_lined_image_parts # ode改良 end end end
tmtysk-mbmail/lib/mb_mail/mb_mailer.rb
# ode改良 start rails依存のを除去 module MbMail # http://www.kbmj.com/~shinya/rails_seminar/slides/#(33) # JapaneseMailer 実装より class MbMailer Dir[File.join(File.dirname(__FILE__), '../tmail/**/*.rb')].sort.each { |f| require f } public # ヘッダに日本語を含める場合に用いる base64 カプセリング # http://wiki.fdiary.net/rails/?ActionMailer def base64(text, charset="iso-2022-jp", convert=true) if convert if charset == "iso-2022-jp" text = NKF.nkf('-j -m0', text) end end text = [text].pack('m').delete("\r\n") "=?#{charset}?B?#{text}?=" end end end # ode改良 end
tmtysk-mbmail/lib/tmail/unquoter.rb
# ode改良 start alias_method_chainを使えるようにした。 module ActiveSupport module CoreExtensions module Module # Encapsulates the common pattern of: # # alias_method :foo_without_feature, :foo # alias_method :foo, :foo_with_feature # # With this, you simply do: # # alias_method_chain :foo, :feature # # And both aliases are set up for you. # # Query and bang methods (foo?, foo!) keep the same punctuation: # # alias_method_chain :foo?, :feature # # is equivalent to # # alias_method :foo_without_feature?, :foo? # alias_method :foo?, :foo_with_feature? # # so you can safely chain foo, foo?, and foo! with the same feature. def alias_method_chain(target, feature) # Strip out punctuation on predicates or bang methods since # e.g. target?_without_feature is not a valid method name. aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1 yield(aliased_target, punctuation) if block_given? with_method, without_method = "#{aliased_target}_with_#{feature}#{punctuation}", "#{aliased_target}_without_#{feature}#{punctuation}" alias_method without_method, target alias_method target, with_method case when public_method_defined?(without_method) public target when protected_method_defined?(without_method) protected target when private_method_defined?(without_method) private target end end # Allows you to make aliases for attributes, which includes # getter, setter, and query methods. # # Example: # # class Content < ActiveRecord::Base # # has a title attribute # end # # class Email < Content # alias_attribute :subject, :title # end # # e = Email.find(1) # e.title # => "Superstars" # e.subject # => "Superstars" # e.subject? # => true # e.subject = "Megastars" # e.title # => "Megastars" def alias_attribute(new_name, old_name) module_eval <<-STR, __FILE__, __LINE__+1 def #{new_name}; self.#{old_name}; end # def subject; self.title; end def #{new_name}?; self.#{old_name}?; end # def subject?; self.title?; end def #{new_name}=(v); self.#{old_name} = v; end # def subject=(v); self.title = v; end STR end end end end class Module include ActiveSupport::CoreExtensions::Module end # ode改良 end require 'nkf' # convert_to で機種依存文字や絵文字に対応するために # Unquoter 内で NKF を使用するようにしたもの module TMail class Unquoter class << self def unquote_and_convert_to(text, to_charset, from_charset = "iso-8859-1", preserve_underscores=false) return "" if text.nil? text.gsub(/(.*?)(?:(?:=\?(.*?)\?(.)\?(.*?)\?=)|$)/) do before = $1 from_charset = $2 quoting_method = $3 text = $4 # p "#{before} #{from_charset} #{quoting_method} #{text}" before = convert_to(before, to_charset, from_charset) if before.length > 0 before + case quoting_method when "q", "Q" then unquote_quoted_printable_and_convert_to(text, to_charset, from_charset, preserve_underscores) when "b", "B" then unquote_base64_and_convert_to(text, to_charset, from_charset) when nil then # will be nil at the end of the string, due to the nature of # the regex used. "" else raise "unknown quoting method #{quoting_method.inspect}" end end end # http://www.kbmj.com/~shinya/rails_seminar/slides/#(30) def convert_to_with_nkf(text, to, from) # p text, to, from if text && to =~ /^utf-8$/i && from =~ /^iso-2022-jp$/i NKF.nkf("-Jw", text) elsif text && from =~ /^utf-8$/i && to =~ /^iso-2022-jp$/i NKF.nkf("-Wj", text) else convert_to_without_nkf(text, to, from) end end alias_method_chain :convert_to, :nkf end end end # ode改良 start subjectがJISでデコードされる不具合のため対応。 module TMail class Decoder def self.decode( str, encoding = nil ) #何もしない。実際のデコードはUnquoter#convert_toで行われるため。 return str end end end #ode改良 end # 下記のバージョンでutf8としてデコードするでもOK。(TMail.KCODEをUTF8にしておく必要あり) # だけど後で実際のデコードをUnquoter#convert_toでもっと汎用的に行うのでここでデコードに必要な情報をかけさせるのも微妙かと思う。 # あとここのデコードはJIS、EUC、SJISだけという日本専用になっている。多分ここはTMailの管理が日本のときのなごりなのかも!? #module TMail # class Decoder # OUTPUT_ENCODING = { # 'EUC' => 'e', # 'SJIS' => 's', # 'UTF8' => 'w', #utf8を追加 # } # def self.decode( str, encoding = nil ) # encoding ||= (OUTPUT_ENCODING[TMail.KCODE] || 'j') # opt = '-mS' + encoding # str.gsub(ENCODED_WORDS) {|s| NKF.nkf(opt, s) } # str # end # # end #end
実行環境
ruby (1.8.6)
tmail (1.2.3.1)
MbMail (verはないっぽい。2008-12-24のもの)
rubyのバージョンアップでcapistranoの設置がうまくいかなくなった
最近rubyを1.8.6p369にアップしたんですが
capistrano2.5.5+ruby1.8.6p369
だと複数の設置先がある場合に固まりました。
(パスワード入力までいかなかったり、パスワード入れれてもその後で固まったり。設置先が1箇所の場合は問題なくできました)
rubyをruby1.8.6p287に戻したら直りました。
うむむ。
最近でたcapistrano2.5.7にしても解決はしませんでした。
しかもcapistrano2.5.7にしたら今まで動いてたものが動かなくなる始末。。
設置後に再起動してくれない状況。
恐らく
namespace :deploy do task :restart, :roles=>:app do
などで上書きしてたのが動いてない気がします。
(上記の書き方あんま良くないやり方で動かなくなった説もありますが。。)
というわけでしばらくはcapistrano2.5.5+ruby1.8.6p287でいこうと思います。。
ちなみにruby1.8.7p174では問題でませんでした。
けどrails1.2.6のアプリもまだあるし今はまだやめておこう。
railsバッチの作り方
作り方
例えば検索インデックス登録用バッチとしてcreate_search_indexというバッチを作る場合
ファイル構成
app/ batch/ ← このフォルダを追加 create_search_index/ create_search_index_batch.rb - railsバッチを定義する create_search_index.sh.sample - 実行用シェルスクリプトのサンプル create_search_index.sh - 実行用シェルスクリプト。sampleをコピーして使う。svn除外にする。 ・・・
create_search_index.sh.sample
#!/bin/sh #staging ruby `dirname $0`/../../script/runner `dirname $0`/create_search_index_batch.rb --fetch_size 50 #--fetch_size 50はバッチへの引数です。 #production #ruby `dirname $0`/../../script/runner `dirname $0`/create_search_index_batch.rb -e production --fetch_size 50
create_search_index_batch.rbに自由にバッチのスクリプトを書いていきます。
もちろんrailsが適用されているのでWebアプリ側で作ったmodelを扱えます。
起動にはbatch/create_serach_index/create_serach_index.shを使用して下さい。これをcronに登録する感じですね。
設置の際
1バッチごとに1つのrailsアプリを設置します。
例えばbatch1とbatch2がある場合
batch1/app /batch ・・・ batch2/app /batch ・・・
のように2つのrailsアプリを設置するイメージにします。
(capistranoで設置する際はbatch1/current/batchですね)
メリット
- 更新の際に他のバッチに影響を与えない。
注意点
webアプリでenviroments/xx.rbの設定値を追加した場合等は
次回バッチを更新する際に設定値を追加する必要があります。
他の方法と比べて
通常のrubyスクリプト+ActiveRecordを単体で使う方法等があると思います。
但しWebアプリのModelを共有で使ったりする場合は
Modelの中でenviroments/xx.rbの中で定数を使った設定を使った場合や
plugin、libを使うとなると面倒なことになると思います。
実行環境
rails(2.1.2)
capistranoで複数のデプロイをしたい場合
設置の単位を分けたい場合があります。
例えばステージングと本番環境(プロダクション)や、ウェブアプリとバッチ等です。
やり方
deploy.rbファイルを用途別に作成する。
- config/deploys/staging_web.rb(ステージングweb)
- config/deploys/production_web.rb(本番web)
- config/deploys/production_batch_create_search_index.rb(各種バッチごと)
上記の各ファイルは通常のdeploy.rbと同じ記述をしてもらうとして、追加で先頭に下記の2行を追加する
load 'deploy' if respond_to?(:namespace) # cap2 differentiator Dir["#{File.dirname(__FILE__)}/../../vendor/plugins/*/recipes/*.rb"].each { |plugin| load(plugin) }
RAILS_ROOT/Capfileは使用しないので削除しても構わない。
デプロイ時のコマンド
cap -f config/deploys/production_batch_create_search_index
.rb deploy
副次的なメリット
railsアプリのパスでなくても実行可能になります。
今までは
cd /usr/local/rails/hogeapp/
cap deployとしていたのが
cap -f /usr/local/rails/hogeapp/config/deploys/production_batch_create_search_index
.rb deploy
と1行で実行可能になります。
解説
-fオプションによりdeploy.rbだけでない任意のファイルを指定可能になります。
但しこの場合RAILS_ROOT/Capfileが読まれなくなるためにデフォルトのタスクのdeploy等が使用できなくなります。
-Fというオプションを渡すことでCapfileを読むように指定できるのですが、またこれが曲者で-fで指定したタスクの後にCapfileをロードするために
-fで指定したdeploy:start(任意のサーバー thinやpassngerを起動するために上書きする)等をデフォルトのタスク(script/reaper等)に戻してしまいます。
そのためCapfileの中身を少し修正したものをdeploy.rbの先頭に追加することで回避しました。
調査メモ
-fでのファイルの後にCapfileが呼ばれるのをソースを見て確認しました。
option_parserで-fで指定したrecipeを追加し、その後にlook_for_default_recipe_fileで
デフォルトのrecipe(RAILS_ROOTのCapfile)をロードしている。
capistrano/cli/options.rb
option_parser.parse!(args) coerce_variable_types! # if no verbosity has been specified, be verbose options[:verbose] = 3 if !options.has_key?(:verbose) look_for_default_recipe_file! if options[:default_config] || options[:recipes].empty?
その他の方法
capistrano-extというgemを使う方法があるらしいです。
実行環境
capistrano (2.5.5)