Railsで複数DBのmigration

大規模なRailsアプリケーションだったり、旧来のシステムをRailsに置き換えたりしてると、どうしても複数DBを扱いたくなってくる。

その場合は例えば、以下のようなlib/tasks/database_foo.rakeを作成します。

namespace :foo do
  desc 'Configure the variables that rails need in order to look up for the db
    configuration in a different folder'
  task :set_custom_db_config_paths do
    ENV['SCHEMA']                                     = 'db_foo/schema.rb'
    ENV['DB_STRUCTURE']                               = 'db_foo/structure.sql'
    Rails.application.config.paths['db']              = ['db_foo']
    Rails.application.config.paths['db/migrate']      = ['db_foo/migrate']
    Rails.application.config.paths['db/seeds.rb']     = ['db_foo/seeds.rb']
    Rails.application.config.paths['config/database'] = ['config/database_foo.yml']
    ActiveRecord::Migrator.migrations_paths           = 'db_foo/migrate'

    # db_fooディレクトリを作りたくない場合は、以下のようにするとよい。
    # ENV['SCHEMA']                                     = 'db/schema_foo.rb'
    # ENV['DB_STRUCTURE']                               = 'db/structure_foo.sql'
    # Rails.application.config.paths['db']              = ['db']
    # Rails.application.config.paths['db/migrate']      = ['db/migrate_foo']
    # Rails.application.config.paths['db/seeds.rb']     = ['db/seeds_foo.rb']
    # Rails.application.config.paths['config/database'] = ['config/database_foo.yml']
    # ActiveRecord::Migrator.migrations_paths           = 'db/migrate_foo'
  end

  namespace :db do
    task drop: :set_custom_db_config_paths do
      Rake::Task['db:drop'].invoke
    end

    task create: :set_custom_db_config_paths do
      Rake::Task['db:create'].invoke
    end

    task migrate: :set_custom_db_config_paths do
      Rake::Task['db:migrate'].invoke
    end

    namespace :migrate do
      task up: :set_custom_db_config_paths do
        Rake::Task['db:migrate:up'].invoke
      end
      task down: :set_custom_db_config_paths do
        Rake::Task['db:migrate:down'].invoke
      end
    end

    task rollback: :set_custom_db_config_paths do
      Rake::Task['db:rollback'].invoke
    end

    task reset: :set_custom_db_config_paths do
      Rake::Task['db:drop'].invoke
      Rake::Task['db:create'].invoke
      Rake::Task['db:migrate'].invoke
    end

    namespace :schema do
      task dump: :set_custom_db_config_paths do
        Rake::Task['db:schema:dump'].invoke
      end
      task load: :set_custom_db_config_paths do
        Rake::Task['db:schema:load'].invoke
      end
    end

    namespace :structure do
      task dump: :set_custom_db_config_paths do
        Rake::Task['db:structure:dump'].invoke
      end
      task load: :set_custom_db_config_paths do
        Rake::Task['db:structure:load'].invoke
      end
    end

    task version: :set_custom_db_config_paths do
      Rake::Task['db:version'].invoke
    end

    task seed: :set_custom_db_config_paths do
      Rake::Task['db:seed'].invoke
    end

    namespace :test do
      task prepare: :set_custom_db_config_paths do
        Rake::Task['db:drop'].invoke
        Rake::Task['db:create'].invoke
        Rake::Task['db:migrate'].invoke
      end
    end
  end
end

すると、fooというDBに対して

  • rake foo:db:create
  • rake foo:db:drop
  • rake foo:db:migrate
  • rake foo:db:migrate:up
  • rake foo:db:migrate:down
  • rake foo:db:schema:dump
  • rake foo:db:schema:load

等々が実行できるようになります。

mail gemで、Content-Type: multipart/mixedとしたいけどならないときの対処法

mail gem使って、multipartメールでかつファイルが添付されたメールを送信したら、 Content-Type: multipart/mixedではなくContent-Type: multipart/alternativeになってしまい、 Thunderbirdでは添付ファイルが認識されず、MacメーラーではHTML本文が表示されない自体となってしまった。 別におかしなことはしていなくて、 https://github.com/mikel/mail のREADME通り、

@mail = Mail.new do
  to      'nicolas@test.lindsaar.net.au'
  from    'Mikel Lindsaar <mikel@test.lindsaar.net.au>'
  subject 'First multipart email sent with Mail'

  text_part do
    body 'Here is the attachment you wanted'
  end

  html_part do
    content_type 'text/html; charset=UTF-8'
    body '<h1>Funky Title</h1><p>Here is the attachment you wanted</p>'
  end

  add_file '/path/to/myfile.pdf'
end 

のようにしてたのだけど、なぜかダメだった。 で、issues漁っていたら、 https://github.com/mikel/mail/issues/590 が見つかって、そこで紹介されている https://gist.github.com/steve500002/bfc4b027d93c0c04f126 のようにしたら、Content-Type: multipart/mixed で送信されるようになった。

mail=Mail.new do |mail|
  to      'reipient@example.com'
  from    'Fred Flintstone <fred@flintstones.com>'
  subject 'Multipart email with attachment'
end

# Remember to end the plain text with a few blank lines. iOS devices often display the content of the attachments so you want to force them to go below the text

# Create the text and html as separate mail parts
text_body = Mail::Part.new do
    body "This is the message\n\n"
    content_type 'text/plain; '
end
html_body  = Mail::Part.new do
    body "<h1>This is HTML</h1>"
    content_type 'text/html; charset=UTF-8'
end

# Create a mail part to hold the html and plain text
bodypart = Mail::Part.new
bodypart.text_part = text_body
bodypart.html_part = html_body

# Add the html and plain text to the email
mail.add_part bodypart

# Add the attachment(s)
attachment = "../path/to/doc.pdf"
mail.attachments["#{File.basename(attachment)}"] = File.read(attachment)

いったん、 bodypart = Mail::Part.new として、 その後、 mail.add_part bodypartするのがポイント。

mail gemで、「Non US-ASCII detected and no charset defined. Defaulting to UTF-8, set your own if this is incorrect.」と出た場合

mail gemを使っていて

Non US-ASCII detected and no charset defined. Defaulting to UTF-8, 
set your own if this is incorrect.

の様な警告が出る場合は、

mail.text_part do
  body 'あああ'
  content_type 'text/plain; charset=UTF-8'
end
mail.html_part do
  body 'あああ'
  content_type 'text/html; charset=UTF-8'
end

とするか、もしくは

mail.text_part =  Mail::Part.new(body: 'あああ', charset: 'UTF-8')
mail.html_part =  Mail::Part.new(body: 'あああ', charset: 'UTF-8')

とすると、警告が出なくなる。

http://docs.komagata.org/4879 のようなやり方がネットで載っているが、mail gemのバージョンアップによってそれでは警告は消えなくなったもよう。

perfectqueueを動かしてみる

Fluentdの作者の古橋さんが作って、Treasure Dataで使われているというメッセージキュー、 perfectqueue。 動かしてみようと思ったら全然ネットに資料がなかったので頑張って動かしてみました。 大きな特徴としてはMySQL上でキューイングシステムを構築するところでしょうか。

インストール

$ git clone https://github.com/treasure-data/perfectqueue.git
$ cd perfectqueue
$ bundle install

設定

$ mysql -h localhost -u root -e 'create database perfectqueue;'

$ mkdir config
$ cat config/perfectqueue.yml #以下のような内容でファイル作成
development:
  type: rdb_compat
  url: mysql2://root:@localhost:3306/perfectqueue
  table: queues

$ bundle exec perfectqueue init # テーブルを初期構築

実装

$ cat app/workers/dispatch/test_handler.rb
class TestHandler < PerfectQueue::Application::Base
  # implement run method
  def run
    # do something ...
    puts "#### acquired task: #{task.inspect}"

    # call task.finish!, task.retry! or task.release!
    task.finish!
  end
end
$ cat app/workers/dispatch/dispatch.rb
$:.unshift File.dirname(__FILE__)
require 'test_handler.rb'
class Dispatch < PerfectQueue::Application::Dispatch
  # describe routing
  route "type1" => TestHandler
  route /^regexp-.*$/ => :TestHandler  # String or Regexp => Class or Symbol
end

キューにメッセージを詰めてみる

$ bundle exec perfectqueue submit k1 type1 '{"uid":1}' -u user_1
$ bundle exec perfectqueue list
                           key            type               user             status                   created_at                      timeout   data
                            k1       user_task             user_1            waiting    2016-03-10 22:15:12 +0900    2016-03-10 22:15:12 +0900   {"uid"=>1}

perfectqueueを起動させる

$ bundle exec perfectqueue run -I. -rapp/workers/dispatch/dispatch.rb Dispatch
I, [2016-03-10T22:21:11.842687 #3933]  INFO -- : PerfectQueue 0.8.45
I, [2016-03-10T22:21:11.852560 #3933]  INFO -- : Worker process started. pid=3934
I, [2016-03-10T22:21:11.972423 #3934]  INFO -- : acquired task task=k1 id=1
#### acquired task: #<PerfectQueue::AcquiredTask @key="k1" @attributes={:status=>:running, :created_at=>1457616062, :data=>{"uid"=>1}, :type=>"type1", :user=>"user_1", :timeout=>1457616062, :max_running=>nil, :message=>nil, :node=>nil}>
I, [2016-03-10T22:21:11.973519 #3934]  INFO -- : finished task=k1
I, [2016-03-10T22:21:11.974812 #3934]  INFO -- : completed processing task=k2 id=1:

$ bundle exec perfectqueue list
                           key            type               user             status                   created_at                      timeout   data
                            k1           type1                              finished                                 1984-07-02 20:39:31 +0900   {"uid"=>1}

起動中にキューを追加

$ bundle exec perfectqueue submit k5 type1 '{"uid":5}' -u user_5
I, [2016-03-10T22:26:43.535768 #4234]  INFO -- : acquired task task=k5 id=1
#### acquired task: #<PerfectQueue::AcquiredTask @key="k5" @attributes={:status=>:running, :created_at=>1457616402, :data=>{"uid"=>5}, :type=>"type1", :user=>"user_5", :timeout=>1457616402, :max_running=>nil, :message=>nil, :node=>nil}>
I, [2016-03-10T22:26:43.536079 #4234]  INFO -- : finished task=k5
I, [2016-03-10T22:26:43.544347 #4234]  INFO -- : completed processing task=k5 id=1:

ちゃんと処理されてますね。

MIMEエンコードされたメールのデコード方法

メール本文のデコード

まずgmailの"メッセージのソースを表示する"をクリックする。

encodingがbase64でcharsetがUTF-8の場合

Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: base64

pbpaste | nkf -mB

encodingが7bitで、charsetISO-2022-JPの場合

Content-Type: text/plain; charset="ISO-2022-JP"
Content-Transfer-Encoding: 7bit

pbpaste | nkf -J でデコード

encodingがquoted-printableで、charsetがUTF-8の場合

Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: quoted-printable

pbpaste | nkf -mQ でデコード

encodingがbase64で、charsetがISO-2022-JPの場合

charset=ISO-2022-JP
Content-Transfer-Encoding: base64

pbpaste | base64 -D | nkf -Jpbpaste | nkf -mB でデコード

encodingが8bitで、charsetがShift_JISの場合

content-type:text/plain; charset="Shift_JIS"
content-transfer-encoding:8bit

gmailエンコードShift_JISにする。

Subject(件名)のデコード

ISO-2022-JP、quoted-printable

Subject: =?ISO-2022-JP?Q?iTunes_Movie_=1B$B%K%e!<%j%j!<%9$HCmL\:nIJ=1B(B?=

echo "=?ISO-2022-JP?Q?iTunes_Movie_=1B$B%K%e!<%j%j!<%9$HCmL\:nIJ=1B(B?=" | nkf -m でデコード

ISO-2022-JPBASE64

Subject: =?ISO-2022-JP?B?GyRCJUElMSVDJUg5WEZ+JE4bKEI=?=
 =?ISO-2022-JP?B?GyRCJCpDTiRpJDshSiVmJUolJCVGJUMlSSEmJTclTSVeGyhCIA==?=
 =?ISO-2022-JP?B?GyRCJTAlayE8JVchSxsoQg==?=

echo "Subject: =?ISO-2022-JP?B?GyRCJUElMSVDJUg5WEZ+JE4bKEI=?==?ISO-2022-JP?B?GyRCJCpDTiRpJDshSiVmJUolJCVGJUMlSSEmJTclTSVeGyhCIA==?==?ISO-2022-JP?B?GyRCJTAlayE8JVchSxsoQg==?=" | nkf -m でデコード

改行を含んでいる状態で pbpaste | nkf -m でもデコードできる。

UTF-8BASE64

Subject: =?UTF-8?B?44CQ44OX44Op44Kk44Og5Lya5ZOh44Gu5pa5?=
 =?UTF-8?B?44G444GK55+l44KJ44Gb44CRUHJp?=
 =?UTF-8?B?bWUgTm93IOOCqOODquOCouaLoeWkpyjmnbHkuqzjg7vljYPokYkp?=

pbpaste | nkf -m でデコード

UTF-8、quoted-printable

Subject: =?utf-8?Q?Distributed=20TensorFlow=E3=81=AE=E8=A9=B1?=

pbpaste | nkf -m でデコード

telnetからSMTPを使ってメール送信

yahooのメールにtelnetからSMTPを使ってメール送信してみます。

$ telnet smtp.mail.yahoo.co.jp 25
Trying 183.79.29.237...
telnet: connect to address 183.79.29.237: Connection refused
telnet: Unable to connect to remote host

はい。Connection refusedされましたね。 これはプロバイダがOutbound Port 25 Blockingをかけているからです。 なので、サブミッションポートの587を指定して繋ぎます。

$ telnet smtp.mail.yahoo.co.jp 587
Trying 183.79.29.237...
Connected to smtp.mail.g.yahoo.co.jp.
Escape character is '^]'.
220 smtp526.mail.kks.yahoo.co.jp ESMTP
EHLO localhost
250-smtp526.mail.kks.yahoo.co.jp
250-AUTH LOGIN PLAIN XYMCOOKIE
250-PIPELINING
250 8BITMIME
MAIL FROM:自分のメールアドレス
530 Sorry, please use SMTP-AUTH instead - for help go to http://www.yahoo-help.jp/app/answers/detail/a_id/79799/p/622
Connection closed by foreign host.

はい。Connection closedされました。 エラーメッセージからもわかるように、SMTP-Auth(SMTP認証)が必要です。 なので、以下のコマンドを使って認証用の文字列を生成します。

printf "%s\0%s\0%s" メールアドレス メールアドレス パスワード | openssl base64 -e | tr -d '\n'; echo

で、出力された認証用文字列をコピーしておきます。

telnet smtp.mail.yahoo.co.jp 587
Trying 183.79.29.237...
Connected to smtp.mail.g.yahoo.co.jp.
Escape character is '^]'.
220 smtp530.mail.kks.yahoo.co.jp ESMTP
EHLO localhost
250-smtp530.mail.kks.yahoo.co.jp
250-AUTH LOGIN PLAIN XYMCOOKIE
250-PIPELINING
250 8BITMIME
AUTH PLAIN 認証用文字列
235 ok, go ahead (#2.0.0)
MAIL FROM:送信元メールアドレス
250 ok
RCPT TO:送信先メールアドレス
250 ok
DATA
354 go ahead
Subject: hoge
From: 送信元メールアドレス
To: 送信先メールアドレス

hello!
.
250 ok 1454173930 qp 24032
quit
221 smtp531.mail.kks.yahoo.co.jp
Connection closed by foreign host.

で、yahooメールを見てみるとメールが届いているのが分かります。

RedisClusterにslaveを追加したとき、masterとslaveでキーの数が違う

以前、Redis Clusterにslaveを追加したときmasterとslaveでキーの数が違う現象に合いました。

masterの方がキーが多くslaveの方がキーが少ない。 その時はどうしてそんなことになるのか分からなかったのですが、調査して分かりました。

結論は、

slaveを追加するとき、全てのキーのexpireチェックをかけて期限が切れているキーは除かれた状態でslaveとして追加される。 しかしmasterは期限が切れたキーも保持している(可能性がある)。

なのでmasterとslaveでキー数が異なることがある。

期限切れキーの削除のされ方

さて、そもそもRedisはキーの期限が切れた瞬間にキーを削除しているわけではありません。

またdbsizeコマンドをうった結果も期限が切れたキーも含まれた結果が返って来ます。

Redisの期限切れキー削除の仕方の仕様は

http://redis.io/commands/expire#how-redis-expires-keys

に書いてあります。

簡単にまとめると、

  1. 0.1秒間に一回、expireがセットされているキーをランダムに20個取得する。
  2. その中で期限が切れているキーを削除する
  3. step2で削除したキーが25%以上(つまり20個の25%なので5個以上)だった場合、step1を再度実行する

といったアルゴリズムで削除しています。 また、もちろん、値が取得された時にキーの期限が切れていた場合も削除します。

強制的に期限切れキーを削除する方法

とは言っても、masterとslaveでキーの数が大きく違うのは気持ち悪いし不安だという場合は、 https://ivan-site.com/2014/06/force-evict-expired-keys-from-redis/ に書いてあるように、SCAN commandを実行すると期限切れキーは削除されます。

なので上記のブログのあるように全キーをscanするプログラムをかけば、その時点で期限が切れているキーを削除することができます。