production環境ではexception_notificationでエラーをメールしてる人も多い思います。(AirBreakとかも)

怖話でもそうやってエラーメールが一通も来ない状態にしたいんですが、例えば下記のような”バグじゃないエラー”は皆様どうやって対応してるんでしょう?

A ActionView::MissingTemplate occurred in home#index:

 Missing template home/index, application/index with {:handlers=>[:erb, :builder, :haml], :formats=>["*/*;q=0.01"], :locale=>[:ja, :ja]}. Searched

formatsに*/*;q=0.01なんて文字列を送ってくるのは単なるスパム。使ってるformatだけに限定するように何か書く?うーん・・・。

怖話をruby1.8.7 & rails3.0.8からruby1.9.2 & rails3.1.1の最新状態に更新しました。引っかかったところメモ。

yamlのエラー

$ rake db:migrate
rake aborted!
couldn't parse YAML at line 267 column 42

rake db:migrateがエラー。

require 'yaml' YAML::ENGINE.yamler= 'syck'

config/boot.rbの2行目に下記を追加。

require 'yaml'
YAML::ENGINE.yamler = 'syck'

日本語

db/seeds.rbで日本語を使ってたので先頭に下記magick commentを追加。

# coding: utf-8

何でもかんでもmagick commentを入れるのは良くないので必要なところにだけ。rubyのm17nは素晴らしいと思います。

Array#choice

ruby1.9.2ではArray#choiceが無くなってる。Array#sampleを使えばいい。

Fixtures

FixturesがActiveRecord::Fixturesになりました。

db/seeds.rbでfixtureを読み込む場合(test用のとは別に用意してる場合とか)

require 'active_record/fixtures'
Fixtures.create_fixtures("#{Rails.root}/db/seed", 'stories')

こんな感じだったんですが、

require 'active_record/fixtures'
ActiveRecord::Fixtures.create_fixtures("#{Rails.root}/db/seed", 'stories')

こうなりました。

Asset Pipeline

developmentで余裕こいてるとproduction世界でひっそり幕を閉じる。(via ブロントさん)

Asset Pipelineを大きく下記の4つに分けて考えるとわかりやすいです。

  • preprocessing(coffee -> jsなど)
  • concatenation(複数のファイルを一つにする)
  • compression(圧縮)
  • fingerprinting(ファイル名にハッシュ値を付ける)

対象は下記3つ

  • javascript
  • stylesheet
  • image

productionではrake assets:precompileで上記4つを対象3つに施してpublic/assetsフォルダーに入れ、nginx等のWebサーバーに配信させてrailsは関与しません。アプリ起動前からpublic/assets以下に必要な全てのファイルが揃って準備完了している必要があります。

precompileされるにはファイルがsprocketsの記法= require tree .等やconfig.assets.precompile += %w()のリストに入ってる必要があります。ここから漏れているファイルはpublic/assetsに無いことになるので表示されません。

jsの中でひっそり使われてる画像や特定のページでだけ読み込んでるcss、そういえば使ってたjqueryプラグインなど、とても見落としやすいので気を付けなければなりません。

それを乗り越えればnginxでcache期限無限のフィーバー状態です。でも既存アプリのバージョンアップなどでは上記見落としを無くすのはとても難しいと思います。もし見逃しそうならば・・・

config.assets.compileをtrueにするんだな。
おまえにもかぞくがいるだろう…

怖話でコメント欄がスパムで埋め尽くされて困ったので日本語が入ってないコメントは弾くようにしました。

「日本語が入ってない場合弾く」というのはMovableTypeの時からあるやり方ですが、イマイチ綺麗に実装できる感がしなくて控えてました。しかしmojiという素晴らしいgemを使ったら簡単に出来ました。

class Comment < ActiveRecord::Base
  validates_format_of :body,
    :with => Moji.regexp(Moji::ZEN_ALNUM | Moji::ZEN_KANA | Moji::ZEN_KANJI),
    :message => I18n.t('errors.messages.not_a_zenkaku')
end

mojiは色々な文字種の正規表現を持っていて手軽に組み合わせたりできます。最初はMoji::ZENで全角文字があればOKにしました。しかし実データ(スパム)にはギリシャ文字やキリル文字が存在していたのでそれも除外するようにしました。

$ rails c -E production
>> Comment.all.each {|c| c.destroy unless c.valid? }

これで今までの全てのスパムが消滅。今のところ新しいスパムも防げてるようでスッキリ。

カウンターキャッシュのリセット

以前、update_countersメソッドを使ってカウンターキャッシュを設定したつもりになっていました。しかし、update_countersメソッドは与えた数だけカウンターが増えるという動作だったようです。代わりにreset_countersメソッドを使えばcomments.countの数にresetしてくれるようです。

Story.all.each do |story|
  Story.reset_counters(story.id, :comments)
end

Facebookページ・アプリでハマるところシリーズ。

RailsでFacebookアプリ作ります。FacebookアプリはIFRAME内のページをPOSTメソッドで呼び出します。エントリー一覧(/entries)のようなものをFacebookアプリのトップページにしようとしたらアレ?となります。(POSTしたらcreate呼ばれるから)

オーマイゴッド!じゃあ/entries/newをPOSTメソッドも受け入れるようにしてこれをトップにしよう。しかしFacebookアプリは/で終わるURLしか指定できません。仕方ないので/entries/new/を受け入れるようにしよう・・・。

しかしログインしたはずが情報が引き継がれません。おかしいな。

WARNING: Can't verify CSRF token authenticity

ログを見るとCSRF tokenのWARNING。考えてみりゃ当然ですが、FacebookからPOSTで呼ばれる時にはCSRF tokenなんぞ付いてないのでWARNINGが出ます。そして危ないのでsessionは一旦破棄されるわけです。自動ログアウト。

paperclipを使ってて「縦長の画像をアップしたら横長になる」という問題。

これはpaperclipが悪いんじゃなくて、元のファイルもMacのPreviewで見ると縦長なんだけど、ブラウザで見ると横長。MacのPreview他、ExifのOrientationタグに対応したソフトで見ると縦長に見える。

Webアプリ的にはちょっと困る。「縦長の画像をアップしたら横長になった!」と言われても見てるツールの違いで最初から横長なんだから。

imagemagickのconvertにはExifのOrientationタグの内容に合わせて画像データを回転させてくれる超便利な-outo-orientオプションがあるのでそれを使えばいい。

paperclipはrmagickを使わず、convertコマンドを呼び出すだけで、好きなオプションが渡せるというイカシタ作りになっているのでこんな感じでOK。

class User < ActiveRecord::Base
  has_attached_file :picture, convert_options: {all: '-auto-orient'}
end

allは複数のstyleが合った場合に全部にこのオプションを付けるという指定。originalも残るから安心。

# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  if Rails.env.production?     
    provider :twitter, 'xxxxxxxxxxx', 'xxxxxxxxxxxxxxxxxxxx'
  else
    provider :twitter, 'ooooooooooo', 'oooooooooooooooooooo'
  end
end

今まで通りだと動きません。test/test_helper.rbに下記を追加。

require 'shoulda/rails'

今βのshoulda 3.xなら大丈夫なのでもうすぐこれも必要無くなるみたいです。

rails3.1でMySQLからやってくる文字列がASCII-8BITになっているのでto_jsonすると壊れる(to_jsonがencodingを見て処理するので)。sqlite3では起こらない。

環境はSnow Leopard、ruby1.9.2-p290、homebrewで入れたmysql 5.1.54。

% rails new foo
% cd foo
% vi Gemfile
(...)
gem 'mysql'
(...)
% bundle
% vi config/database.yml
(...)
development:
  adapter: mysql
  encoding: utf8
  database: foo_development
  pool: 5
  username: root
  password: 
  host: localhost
  socket: /tmp/mysql.sock
(...)
% rails g model post title:string
% rake db:create
% rake db:migrate
% vi db/seeds.rb
Post.create!(title: 'うんk')
% rake db:seed
% rails c
ruby-1.9.2-p290 :001 > puts Post.first.title
うんk
 => nil 
ruby-1.9.2-p290 :002 > Post.first.title.encoding
 => #<encoding:ascii-8bit>
ruby-1.9.2-p290 :003 > puts Post.first.title.to_json
"\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd"
 => nil

CentOS 5.6でも同じ。

解決:

mysql gemはruby1.9.1からのencodingに対応してない。だからmysql2を使えば解決でした。

% vi Gemfile
(...)
  gem 'mysql2'
(...)
% vi config/database.yml
(...)
  adapter: mysql2
(...)
% rails c
ruby-1.9.2-p290 :001 > Post.first.title.encoding
 => #<Encoding:UTF-8>

adapterにmysql2と書けるというところが盲点でした・・・。

% node -v  
v0.4.8
% rails -v
Rails 3.1.0
% rails new foo
% cd foo
% vi app/assets/stylesheets/foo.css.scss
body {
  background: image-url("rails.png");
}
% rake assets:precompile
rake aborted!
rails.png isn't precompiled
  (in /Users/komagata/tmp/foo/app/assets/stylesheets/foo.css.scss)

Tasks: TOP => assets:precompile
(See full trace by running task with --trace)

何故だろう?

とりあえずはconfig/environments/production.rbconfig.assets.compile = true

にしてLive compileして凌いでいる。

追記:

rails3.1のバグでした。3.1.1を待て。

先週、怖話.jpのランキングを重いサブクエリを書いて実装しましたが、Rails3レシピブック(Recipe 093 カウンタキャッシュを利用する)を見ていたらcounter_cacheというので簡単に速くなりそうだったのでやってみた。

ASCIIcasts - “Episode 23 - Counter Cache Column”

要はfoo has many barsという関係の時にbarのbelongs_toに:counter_cache => trueと書いて、fooのDBにbars_countというカラムを追加すればいいらしい。

怖い話(story)に付いている怖い(scare)とコメント(comment)の集計にcounter_cacheを使ってみた。

class Scare < ActiveRecord::Base
  belongs_to :story, :counter_cache => true
end
class Comment < ActiveRecord::Base
  belongs_to :story, :counter_cache => true
end
class AddCounterCacheToStories < ActiveRecord::Migration
  def self.up
    add_column :stories, :comments_count, :integer, :null => false, :default => 0
    add_column :stories, :scares_count, :integer, :null => false, :default => 0

    Story.all.each do |story|
      Story.update_counters(story.id, :comments_count => story.comments.count)
      Story.update_counters(story.id, :scares_count => story.scares.count)
    end
  end

  def self.down
    remove_column :stories, :comments_count
    remove_column :stories, :scares_count
  end
end

counter_cacheはfooのcreateとdestroyのコールバックとして数を増減するというだけの動作なので、後付けする場合はupdate_countersメソッドで集計し直してやる必要がある。

class Story < ActiveRecord::Base
  scope :order_by_ranking, joins(:user).order("view + (select count(*) from scares where scares.story_id = stories.id) * #{Scare::RANKING_WEIGHT} + (select count(*) from comments where comments.story_id = stories.id) * #{Comment::RANKING_WEIGHT} desc, stories.id desc").includes(:user)
end

お陰でこんな醜悪なクエリが、

class Story < ActiveRecord::Base
  # scares_count and comments count is using counter_cache.
  scope :order_by_ranking, joins(:user).order("view + scares_count * #{Scare::RANKING_WEIGHT} + comments_count * #{Comment::RANKING_WEIGHT} desc, stories.id desc").includes(:user)
end

ホッとするシンプルなクエリに。

しかし、キャッシュ系によくあることですが、カウンタキャッシュを使っているということをちゃんと意識してないと端からみたらわかり辛いことになるので、現在は一人開発ですが、将来の自分を含めた他人に向けて注意を喚起するコメントを残しておくことにしました。