Rust で Web アプリを作った感想

2017年03月03日
区分
Rust
報告者:
tahara

こんにちは、tahara です。

弊社のメインは Ruby on Rails ですが、 簡単な周辺システムは担当者の好きな言語を使って作ったりします。

今回は Rust を使って MySQL(Amazon Aurora)のレコードや MongoDB に入っいるログを集計して表示する Web アプリを Rust で 作った感想です。

とりあえず動くだけの稚拙なコードですが、 ソースはこちら outing-mining-rust

使っている主なライブラリ

  • iron
    • Web フレームワークです
    • 別ライブラリになっていますがルーティングやクッキーセッショも使えます。
  • handlebars
    • ビューです
  • mysql
    • MySQL 接続
  • mongo_driver
    • MongoDB 接続
    • mongodbの方は MongoDB 2.x に対応してないので注意

苦労したところ

  • コンパイルがとおるライブラリの組み合わせがなかなか見付からない
    • ライブラリによって依存している openssl が違ったりして、ライブラリの更新待ちしたり…
    • これが一番苦労したと思います。
  • トライアンドエラーのサイクルの時間がかかる
    • コンパイルが必要なので修正から動作確認するまでの時間がもどかしいですね。
    • Scala などでも同様の事情かと思いますが、みんなどうしているんでしょう?
    • Java だと実行前にリンクせず動的に class ファイルをロードし、 最適化も実行時なのであまり問題にならないのかも。

よかったところ

  • だいたんに修正できる
    • コンパイラが修正もれを拾ってくれるので修正に対してだいたんになれます!
  • ビューの修正はコンパイル不要
    • ビューには handlebars-iron を使っていますが、 開発時だけ Live reload 機能でリコンパイル不要にできたりします。
  • 楽しい
    • 他の言語にはない新しい概念がいろいろあってRust 楽しいです。

Rust での Web 開発もわるくないですよ。

rustup

2016年12月11日
区分
advent-calender
報告者:
tahara

こんにちは、tahara です。

この記事はアクトインディ Advent Calendar 2016 11日目です。

rustup: the Rust toolchain installer について書いてみたいと思います。

rustup は Rust における Ruby での rbenv や RVM みたいなものです。 以前は、multirust がありましたが、rustup 使いましょう、ということになっているようです。

rustup のインストールは簡単です。 https://www.rustup.rs/ に書いてあるとおりです。

curl https://sh.rustup.rs -sSf | sh

そうすると次のようきいてくるので、そのまま Enter でいいと思います。

1) Proceed with installation (default)
2) Customize installation
3) Cancel installation

これで rustc, cargo などが ~/.cargo/bin にインストールされます。 ~/.profile も編集され PATH~/.cargo/bin が追加されます。

そして最後に表示されるメッセージ

To configure your current shell run source $HOME/.cargo/env.

をシェルから実行すると、rustc が使えるようになります。

~$ source $HOME/.cargo/env
~$ rustc --version
rustc 1.13.0 (2c6933acc 2016-11-07)

処理系インストールできたら、とりあえず

~$ vi /tmp/hello.rs
fn main() {
    println!("Hello, world!");
}
~$ rustc /tmp/hello.rs
~$ ./hello
Hello, world!

あとはおまけで Emacs まわりの設定を簡単に書きておきます。

cargo を使ってコード整形のための rustfmt とコード保管のための racer をインストールします

cargo install rustfmt
cargo install racer

~/.emacs は次の感じで。flycheck は好きでないので外してあります。

(unless (package-installed-p 'rust-mode)
  (package-refresh-contents)
  (package-install 'rust-mode)
  (package-install 'cargo)
  ;;(package-install 'flycheck-rust)
  (package-install 'racer)
  (package-install 'company)
  (package-install 'rustfmt))
(add-hook 'rust-mode-hook #'racer-mode)
(add-hook 'rust-mode-hook #'rustfmt-enable-on-save)
(add-hook 'racer-mode-hook #'eldoc-mode)
(add-hook 'racer-mode-hook #'company-mode)
;;(add-hook 'after-init-hook #'global-flycheck-mode)
;;(add-hook 'flycheck-mode-hook #'flycheck-rust-setup)
;;(global-set-key (kbd "TAB") #'company-indent-or-complete-common)
(setq company-tooltip-align-annotations t)
;; Reduce the time after which the company auto completion popup opens
(setq company-idle-delay 0.2)
;; Reduce the number of characters before company kicks in
(setq company-minimum-prefix-length 1)

以上になります。

弊社でもごく一部の社内ツールに Rust を使い始めました。 来年も Rust がよりいっそう盛り上がるといいですね!

メールの Subject が読めない

2016年10月03日
区分
ruby
報告者:
tahara

こんにちは、tahara です。

Rails でメールを送信するとログに次のような感じで出力されますよね。

Sent mail to user@example.com (8.6ms)
Date: Mon, 03 Oct 2016 14:32:16 +0900
To: user@example.com
Message-ID: <57f1ed60d1544_656931b0dc404e7@yarn.mail>
Subject: =?UTF-8?Q?Rails?=
 =?UTF-8?Q?_=E3=82=A8=E3=83=B3=E3=82=B8=E3=83=8B=E3=82=A2=E5=8B=9F=E9=9B=86=E4=B8=AD?=
Mime-Version: 1.0
Content-Type: text/plain;
 charset=UTF-8
Content-Transfer-Encoding: base64

これ Subject が読なめないw

nkf にくわせてやるとデコードしてくれます。 シェルで nkf Enter ペースト Enter C-d です。

~% nkf
Subject: =?UTF-8?Q?Rails?=
 =?UTF-8?Q?_=E3=82=A8=E3=83=B3=E3=82=B8=E3=83=8B=E3=82=A2=E5=8B=9F=E9=9B=86=E4=B8=AD?=
Subject: Rails エンジニア募集中

まあ Rails コンソールからもできます。

[13] pry(main)> Mail.new(subject: <<EOT).subject
[13] pry(main)* =?UTF-8?Q?Rails?=
[13] pry(main)*  =?UTF-8?Q?_=E3=82=A8=E3=83=B3=E3=82=B8=E3=83=8B=E3=82=A2=E5=8B=9F=E9=9B=86=E4=B8=AD?=
[13] pry(main)* EOT
=> "Rails エンジニア募集中 "

メールの Subject 読めました。

act-fluent-logger-rails の Rails5 対応をリリース

2016年07月12日
区分
Rails
報告者:
tahara

こんにちは、tahara です。

act-fluent-logger-rails の Rails5 対応をリリースしました。

https://rubygems.org/gems/act-fluent-logger-rails/versions/0.3.0

Paperclip を使っている Rails アプリのテスト高速化

2016年07月11日
区分
Rails
報告者:
tahara

こんにちは、tahara です。

テストでも Paperclip を使っているモデルを FactoryGirl.create(:facility) とかすると、 ImageMagick の convert コマンドなどが走ってしまいます。 これがなかなか重くてテストを遅くしちゃっています。

そこで Paperclip のサムネイル作成をスキップしてテストを高速化してみました。

spec/support/paperclip_stub.rb

module PaperclipStub
  #21966 いこーよ: spec を Paperclip のサムネイル作成処理をスキップすることで高速化する
  def self.included(spec)
    spec.before do
      allow_any_instance_of(Paperclip::Attachment).to receive(:post_process_file).and_return(nil)
      # CMYKテストを飛ばす
      allow(Paperclip).to receive(:run).and_return(nil)
    end
  end
end

各 spec で include します。

describe Facility do
  include PaperclipStub
  ...

これで Summary (4 workers in 507.3806s) > Summary (4 workers in 351.2692s) となり 44.4% の高速化となしました。

(‘∇’)/゚・:【祝】:・゚\(‘∇’)

allow(Paperclip).to receive(:run).and_return(nil) のとこは http://tech.actindi.net/3474323393 のスキップです。

Rinari + RVM + Bundler 環境での rails console

2016年07月04日
区分
Emacs
報告者:
tahara

こんにちは、tahara です。

いつもコンソールで rails c していましたが Emacs にひきこもることにしました。

(defun rinari--wrap-rails-command (command)
  (concat (expand-file-name "~/.rvm/wrappers/default/bundle") " exec rails " command))

Pry 使っている場合は

(add-hook 'rinari-minor-mode-hook
  (lambda ()
    (setq inf-ruby-first-prompt-pattern
      (setq inf-ruby-prompt-pattern
        "^\\(ircb([^)]+)\\|\\[[0-9]+\\] pry([^)]+)\\|\\(jruby-\\|JRUBY-\\)?[1-9]\\.[0-9]\\.[0-9]+\\(-?p?[0-9]+\\)?\\) ?\\(:[0-9]+\\)* ?[\]>*\"'/`]>? *"))))

delivery_method を smtp にしたら 2.5 倍速くなった

2016年05月23日
区分
Rails
報告者:
tahara

こんにちは、tahara です。

Rails で config.action_mailer.delivery_method:sendmail から :smtp にかえたら、44 分かかっていたメール送信バッチが 17 分で完了するようになりました。 ( ^-^)/:★*☆オメデト♪

なのでみなさん :smpt にしましょう! という話でもないです。

メールを 10 通送信する処理時間をローカルの開発環境と本番環境で計測してみました。

  • ローカルの開発環境
    • :sendmail
      • 0.2306702116秒
    • :smpt
      • 0.639353717秒
  • 本番環境
    • :sendmail
      • 1.713197344秒
    • :smpt
      • 0.840070078秒

ローカルの開発環境と本番環境では反対の結果になりました。 その原因はあまりよくわかっていませんが、 本番環境でメール送信バッチサーバが TLS 経路でメールサーバにリレーしているのが原因なのかもしれません。

あたりまえですが、実測が大切ですね。

MySQL の GROUP_CONCAT

2016年02月08日
区分
MySQL
報告者:
tahara

こんにちは、tahara です。

よくあるグループ毎の最大値を持つレコードを取得する SQL の書き方です。 ただし、MySQL 限定。

次のようなテーブルとレコードがあるとします。

create table foo (
  id int(11),
  group_id int(11),
  value int(11),
  position int(11),
  primary key (id)
);
insert into foo(id, group_id, value, position) values
(1, 1, 11, 3),
(2, 1, 10, 1),
(3, 1, 11, 2),
(4, 2, 10, 1);
mysql> select * from foo;
+----+----------+-------+----------+
| id | group_id | value | position |
+----+----------+-------+----------+
|  1 |        1 |    11 |        3 |
|  2 |        1 |    10 |        1 |
|  3 |        1 |    11 |        2 |
|  4 |        2 |    10 |        1 |
+----+----------+-------+----------+
4 rows in set (0.00 sec)

やりたいのは、次の条件でレコードを取得することです。

  • group_id 毎に最も value が大きいレコードを取得したい。
  • ただし同じ group_id 毎に最も value が大きいレコード複数あが場合は position が最も小さいレコードを取得する。
  • 上の例では id 3 と 4 のレコードを取得する SQL を書きたい。
  • そしてパフォーマンスのために相関サブクエリは使いたくない。

MySQL ではこう書けます。

select foo.* from foo
inner join (
  select
    cast(
      substring_index(
        group_concat(distinct id order by value desc, position),
        ',',
        1)
      as signed) id
  from foo group by group_id
) t on foo.id = t.id;

group_concat がいい仕事をしてくれてます。 group_id 毎に id を value 降順、position 昇順にカンマ区切りの文字列にします。

mysql> select group_concat(distinct id order by value desc, position) as x
    -> from foo group by group_id;
+-------+
| x     |
+-------+
| 3,1,2 |
| 4     |
+-------+
2 rows in set (0.01 sec)

substring_index でカンマ区切り id の最初の1つを取得します。

mysql> select
    -> substring_index(
    ->   group_concat(distinct id order by value desc, position),
    ->   ',', 1) x
    -> from foo group by group_id;
+------+
| x    |
+------+
| 3    |
| 4    |
+------+
2 rows in set (0.00 sec)

substring_index で取得したものは文字列なので join でインデックスが効きません。 なので数値にキャストします。

mysql>   select
    ->     cast(
    ->       substring_index(
    ->         group_concat(distinct id order by value desc, position),
    ->         ',',
    ->         1)
    ->       as signed) id
    ->   from foo group by group_id;
+------+
| id   |
+------+
|    3 |
|    4 |
+------+
2 rows in set (0.00 sec)

欲しいレコードの id がとれたので、あとは自分自身に join します。

mysql> select foo.* from foo
    -> inner join (
    ->   select
    ->     cast(
    ->       substring_index(
    ->         group_concat(distinct id order by value desc, position),
    ->         ',',
    ->         1)
    ->       as signed) id
    ->   from foo group by group_id
    -> ) t on foo.id = t.id;
+----+----------+-------+----------+
| id | group_id | value | position |
+----+----------+-------+----------+
|  3 |        1 |    11 |        2 |
|  4 |        2 |    10 |        1 |
+----+----------+-------+----------+
2 rows in set (0.00 sec)

MySQL 最高だー、と思いました。

Delayed::Job で絶対にやっておいた方がいいたった1つの設定

2015年12月25日
区分
Rails
報告者:
tahara

こんにちは、tahara です。

アクトインディ Advent Calendar 2015 の25日目の記事です。

これです。

Delayed::Worker.max_attempts = 0

Delayed::Job は実行に失敗した場合、デフォルトで25回リトライします。 気のきいた機能なのですが、メール一斉送信を Delayed::Job でおこなう場合に悲劇をまねきます。 はい、昨日やらかしちゃいました ヘ(゚∀゚ヘ)アヒャ

例えば最初の3人にメールを送ったところで4人目でエラーとなったとします。 Delayed::Job はリトライをおこない、また最初の3人にメールを送り4人目でエラー、を繰り返します。 最初の3人に何度も同じメールが... すみませでした!

リトライしないように

Delayed::Worker.max_attempts = 0

やっておきましょう。

以下は、おまけです。

config/initializers/delayed_job_config.rb

# 失敗したジョブを消さない
Delayed::Worker.destroy_failed_jobs = false
# リトライしない
Delayed::Worker.max_attempts = 0
# ActFluentLoggerRails::Logger がバッチでも
# ちゃんとログをはくよう flush_immediately: true を指定
Delayed::Worker.logger = ActFluentLoggerRails::Logger.new(flush_immediately: true)

# エラーがあった場合に Exception Notification で通知するプラグインを作る
class Actindi::DelayedJobExceptionNotificationPlugin < Delayed::Plugin
  callbacks do |lifecycle|
    lifecycle.around(:invoke_job) do |job, *args, &block|
      begin
        block.call(job, *args)
      rescue Exception => exception
        # ActiveRecord::RecordNotFound などでも通知するために
        ignored_exceptions = ExceptionNotifier.ignored_exceptions
        ExceptionNotifier.ignored_exceptions = []
        begin
          ExceptionNotifier.notify_exception(exception,
                                             data: { job: job.inspect })
        ensure
          ExceptionNotifier.ignored_exceptions = ignored_exceptions
        end
        raise
      end
    end
  end
end
# プラグインを登録する
Delayed::Worker.plugins << Actindi::DelayedJobExceptionNotificationPlugin

これくらいとおくと安心かな。

Common Lisp コードの方が多い gem

2015年12月08日
区分
Rails
報告者:
tahara

こんにちは、tahara です。

アクトインディ Advent Calendar 2015 の8日目の記事です。

Common Lisp コードの方が多い gem をリリースしました。 前回はたった2行でしたが、今回は200行以上あります。 ちなみに Ruby のコードは40行くらいです。

lisp-rails-view

S式で Rails のビューを書けるようになる gem です。

インストール方法

まず SBCL をインストールしてください。

sudo apt-get install sbcl

Gemfile に次を追記し bundle を実行してください。

gem 'lisp-rails-view'

使い方

S式でビューを書きます。 index.html.lisp

(:div#main
 (:h3.title "title")
 (:p#foo.bar "Hello"))

これで次のように出力されます。

<div id="main">
  <h3 class="title">title</h3>
  <p id="foo" class="bar">Hello</p>
</div>

詳細はこちらを。

その他

まだいろいろ出来ないことが多いですが、なんかかS式で書ける実装にはなってるかな。

ところで、https://github.com/rails/rails/blob/master/actionview/lib/action_view/context.rb#L33 このせいで String の Array ではなく String を返さないといけないんですよね。 せっかく Rack の body は Array で ok になっているのに、パフォーマンス的にどうなんだろう? と思います。

また機会があればそのあたりも掘り下げていきたいと思います。

guard-notifier-stumpwm

2015年12月04日
区分
Rails
報告者:
tahara

こんにちは、tahara です。

アクトインディ Advent Calendar 2015 の4日目の記事です。

ふだんいこーよを開発しているとき bundle exec guard ってやってます。 むかしは script/autospec でしたよね。

その当時は autotest-stumpwm でした。

このたび Gurad に対応した guard-notifier-stumpwm をリリースしましたー

Gemfile に gem 'guard-notifier-stumpwm' を書いて bundle します。

Guardfile に require 'guard/notifier/stumpwm' を書いて Gurad を実行します。

これでテストが実行されるたびに結果を表示してれます。

コードはこれだけです。

require 'guard/notifier'

module Guard
  module Notifier
    class Stumpwm < Base
      VERSION = '0.0.2'

      def self.available?(opts={})
        !!`stumpish version`
      end

      def notify(message, opts={})
        super

        color =
          case opts[:type]
          when :success
            2                   # green
          when :failed
            1                   # red
          else
            3
          end                   # yellow

        form = <<EOT
(let ((*executing-stumpwm-command* nil))
  (message "^#{color}#{opts[:title]}~%~%#{message}"))
EOT
        `stumpish eval '#{form}'`
      end
    end
  end
end

Guard::Notifier::NOTIFIERS << { stumpwm: ::Guard::Notifier::Stumpwm }

いちおう self.available? で stumpish コマンドが動くか確認。 notify で stumpish を使ってメセッージを表示するだけ。

たった2行ですがコードの中に S 式あると落ち着きますね。

RSpec での悲観ロックのテスト

2015年11月09日
区分
Rails
報告者:
tahara

こんにちは、tahara です。

コードに account とか withdraw とか出てきたら悲観ロック(select for update)が必要ですね。

そのテストをするには次のものが必要になるかと思います。

  • コミットが機能する
  • 複数スレッドがそれぞれ別々のコネクションでトランザクション処理をする

RSpec で書くとこんな感じでしょうか。

describe 'Listing Transaction' do
  # ちゃんとコミットが機能するために
  self.use_transactional_fixtures = false

  describe '悲観ロックのテスト' do
    it 'お金消えたり増えたりしてないよね' do
      10.times.map { |i|
        Thread.new do
          # スレッド毎に別々のコネクションを使う
          ActiveRecord::Base.connection_pool.with_connection do
            # テスト対象の悲観ロックをおこなう処理(foo.lack!)
            ....
          end
        end
      }.each(&:join)

      # お金消えたり増えたりしてないことを確認する
      expect(...).to eq(...)
    end
  end
end

以上、エンジニアとかプログラマとか募集中 です!

requests spec を書こう

2015年10月26日
区分
Rails
報告者:
tahara

こんにちは、tahara です。

ちかごろ Rails のテストは requests spec がいいな、と思ってます。

メリットは

  • リクエストの view → controller → model → レスポンスの view と、ひととおりでテストできる。
    • 実運用にそくしたエンドツーエンドテストがあると安心感高い。
  • それでいて capybara-webkit などを使った features テストより速い。
  • config.use_transactional_fixtures = true のままで ok
  • ふつうに binding.pry が使える。

デメリットは

  • capybara-webkit などを使った features テストでは可能な js のテストができない。

では requests spec でのコツみたいなものを2つほど。

User-Agent を指定する

page.driver.header('User-Agent', 'Mozilla/5.0 (iPhone; ...')

foo.js, foo.coffee などの js ビューを返すリクエスト

ふつうにやると Accept ヘッダが js を受け付つけないもになっているようで js ではなく html のビューをレンダリングしようとして ActionView::MissingTemplate になってしまいます。 なので次のように Accept ヘッダを変えてやります。

def with_js_accept
  page.driver.header(
    'Accept',
    '*/*;q=0.5, text/javascript, application/javascript'
  )
  yield
ensure
  page.driver.header('Accept', nil)
end

it '...' do
  with_js_accept { click_on 'OFF' }
end

というわけで requests spec を書いていきたいと思います。

最後に、エンジニアとかプログラマとか募集中 です!

StumpWM で好きな画像を使って LGTM

2015年08月17日
区分
StumpWM
報告者:
tahara

こんにちは、tahara です。

StumpWM は弊社エンジニア内で最も人気のあるウィンドウマネージャです。 そのシェアは 50% (3/6) もあります!?

コードレビューにかかせない LGTM 画像も簡単に用意できちゃいます。

  1. お好みの画像をブラウザに表示する。
  2. StumpWM のプレフィクス : lgtm
  3. マウスセンターボタンで貼り付け
LGTM
![LGTM](http://lisperati.com/lisplogo_alien_256.png)

~/.stumpwmrc

(defcommand lgtm () ()
  "LGTM"
  (send-fake-key-seq "C-l")
  (sleep 0.1)
  (send-fake-key-seq "C-c")
  (sleep 0.1)
  (let ((url (get-x-selection)))
    (set-x-selection (format nil "LGTM~%![LGTM](~a)" url))))

コードレビューはかどる!?

拡張子なし、application/octet-stream のアップロード

2015年07月13日
区分
Rails
報告者:
tahara

こんにちは、tahara です。

Android からのファイルアップロードはエントロピーの神々に支配されているのでしょうか? 拡張子なし、Content-Type: application/octet-stream で jpeg ファイルが ブラウザから POST されたりします。

そんな POST に対応するには(Paperclip 使用)...

class TempFile < ActiveRecord::Base
  has_attached_file :temp
  before_post_process :ensure_temp_file_name_extension

  protected

  def ensure_temp_file_name_extension
    return true unless File.extname(self.temp_file_name) == ''

    unless VALID_IMAGE_CONTENT_TYPE.include?(self.temp_content_type)
      file = Paperclip.io_adapters.for(temp)
      self.temp_content_type =
        Paperclip::FileCommandContentTypeDetector.new(file.path).detect
    end

    extension = MIME::Types[self.temp_content_type].first.extensions.first rescue nil
    if extension
      self.temp_file_name = self.temp_file_name + '.' + extension
    end
    true
  end
end

エンジニアとかプログラマとか募集中です。

第6回社内エンジニア勉強会 コードの最適化

2015年05月18日
区分
勉強会
報告者:
tahara

こんにちは、tahara です。

たぶん第6回目となる社内エンジニア勉強会を開催しました。

持ちまわりで社内勉強会を開催しており、先日は私の担当でしたので、 このブログにも書いておこうと思います。

よくあるフィボナッチ数を求める fib 関数をネタにコードの最適化について話しました。

まず Ruby で

def fib(n)
  if n < 2
    n
  else
    fib(n - 2) + fib(n - 1)
  end
end

require 'benchmark'

Benchmark.realtime do
  fib(37)
end
# => 4.012619736

これを Common Lisp で書くと

(defun fib (n)
  (if (<  n 2)
      n
      (+ (fib (- n 2))
         (fib (1- n)))))

(time (fib 37))
;; 0.722 seconds of real time

ここから、最適化していきます。

普通に declare と the で最適化するとだいたい2倍になります。

(defun fib-optimize (n)
  (declare (optimize (speed 3) (safety 0))
           (fixnum n))
  (if (<  n 2)
      n
      (the fixnum (+ (the fixnum (fib-optimize (- n 2)))
                     (the fixnum (fib-optimize (1- n)))))))

(time (fib-optimize 37))
;; 0.277 seconds of real time

ディスアセンブルしてみるとよくわかります。

(disassemble 'fib)
(disassemble 'fib-optimize)

次にコードウォークで最適化します。

#:g1: インライン展開手作り風味 からそのまま拝借させていただきました。

(defun-inline-self fib-inline 4 (n)
  (declare (optimize (speed 3) (safety 0))
           (fixnum n))
  (if (<  n 2)
      n
      (the fixnum (+ (the fixnum (fib-inline (- n 2)))
                     (the fixnum (fib-inline (1- n)))))))

(time (fib-inline 37))
;; 0.141 seconds of real time

こんな単純なコードでは関数呼び出しのコストは大きいですね。

ついでにコンパイラーマクロを使ってみます。

(defmacro はやくなるおまじない (f)
  `(define-compiler-macro ,f (&whole form &rest args)
     (if (every #'constantp args)
         (apply #',f args)
         form)))

(はやくなるおまじない fib)

(time (fib 37))
;; 0.000 seconds of real time

もちろん実行前に次のコードに変換するのに、(fib 37) したのと同じ時間かかってます。

(time 24157817)

最後によいアルゴリズムで。

def fib_good(n, a=0, b=1)
  if n == 0
    a
  else
    fib_good(n - 1, b, a + b)
  end
end

Benchmark.realtime do
  fib_good(1000)
end
# => 0.000527397

残念ながら Ruby から Common Lisp に乗り換える必要はなさそうです。 Common Lisp はいろいろ遊べておもしろいのに・・・

最後に、弊社ではデザイナエンジニアを募集しております。 まずはお話だけでも。よろしくお願いします!

Rails エンジニア枯渇していのでしょうか・・・

アクトインディ技術部隊報告書のソース公開

2015年03月16日
区分
CommonLisp
報告者:
tahara

こんにちは、tahara です。

この「アクトインディ技術部隊報告書」は Common Lisp で書かれていますが、このたびソースを公開しました。

https://github.com/actindi-dev/tech-blog

作りを簡単に説明します。

もうおわかりかと思いますが、
完全に私の道楽です m(_ _)m

社内では不評な気がしますが、
めげません(アヒャヒャヒャヒャ ヘ(゚∀゚ヘ)(ノ゚∀゚)ノ ヒャヒャヒャヒャ)

最後に、弊社ではデザイナエンジニアを募集しております。 まずはお話だけでも。よろしくお願いします!

バウンスメール処理

2015年01月26日
区分
Rails
報告者:
tahara

こんにちは、tahara です。

Rails システムでのバウンスメール処理ってどうするのがいいんでしょう? ベステプラクティスではないかもしれませんが、弊社でのバウンスメール処理方法を書いてみたいと思います。

まずメーラークラスで return_path にバウンスメール受信用のアドレスを指定します。

class Notifier < Jpmobile::Mailer::Base # ActionMailer::Base
  include BouncedMailFilter
  default(from: 'from@expamle.com', return_path: 'bounce@example.com')
  ...
end

上記メーラークラスで include しているクラスでは、 バウンスメール DB にメールアドレスが登録されているかチェックして、 登録済みのアドレスにはメールを送信しないようにしています。

module BouncedMailFilter

  def mail(*args)
    m = super
    return m unless m.to.size == 1

    email = m.to.first
    if not_deliver?(email)
      m.perform_deliveries = false
    end
    m
  end

  private

  def not_deliver?(email)
    return true if email =~ /@example.com\z/
    BouncedMail.where(email: email).exists?
  end
end

return_path に指定したメアドの ~/.forward で、バウンスメールを ruby に食べさせるようにします。 ここで rails runner とか使おうとすると遅すぎて話にならないので、こんな方式にしています。 またそれと同時に admin@example.com にも転信して、普通にバウンスメールを受信できるようにしておきす。

admin@example.com
"| ruby /var/www/outing/current/bin/mailforward.rb"

ruby に食べさせたメールは次のコードで Rails アプリに POST されます。

#!/usr/bin/env ruby

require 'net/https'

class MailForward

  def self.post(mail)
    http = Net::HTTP.new('localhost', 11223)
    http.use_ssl = true
    http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    request = Net::HTTP::Post.new('/bounced_mails')
    request.set_form_data({ mail: mail })
    response = http.start {
      http.request(request)
    }
    if response.code.to_i == 200
      return 0
    else
      return 1
    end
  end
end

exit(MailForward.post(STDIN.read))

Rails アプリは POST されたバウンスメールを読んで、 配信できなかったメールアドレスを上記の BouncedMailFilter でチェックしている DB に登録します。

「バウンスメールを読んで」のところが問題で、SMTP サーバによってバウンスメールの中味が違ったりします。 とりま、次のようなコードで処理しています。 完全に検出することはできませんが、運用上問題ないくらいには検出できていると思われます。

class BouncedMail < ActiveRecord::Base

  def self.receive(mail)
    analyzer = BounceAnalyzer.new(mail)
    if analyzer.bounced?
      unless BouncedMail.where(email: analyzer.email).exists?
        BouncedMail.create!(email: analyzer.email,
                            reason: analyzer.reason)
      end
    end
  end

  class BounceAnalyzer

    attr_reader :email

    def initialize(raw_mail)
      @mail = Mail.new(raw_mail)
      if @mail.parts.size == 3
        @summary_part = Mail.new(@mail.parts[0].body)
        @status_part = Mail.new(@mail.parts[1].body)
        @email = parse_email(@status_part.body.to_s)
      end
    end

    def bounced?
      case
      when !@email
        false
      when domain_reject?
        false
      when user_unknown?, host_not_found?, in_reply_to_rcpt_to?
        true
      else
        false
      end
    end

    def reason
      @status_part.body.to_s
    end

    private

    def domain_reject?
      @mail.body.to_s =~ /in\s+reply\s+to\s+end\s+of\s+DATA\s+Command/mi
    end

    def in_reply_to_rcpt_to?
      @summary_part.body.to_s =~ /in\s+reply\s+to\s+RCPT\s+TO\s+command/mi
    end

    def host_not_found?
      @status_part.body.to_s =~ /Host\s+or\s+domain\s+name\s+not\s+found/mi
    end

    def parse_email(body)
      case body
      when /Original-Recipient:\s*(?:rfc822;)?\s*(.+@.+)/i
        $1
      when /Final-Recipient:\s*(?:rfc822;)?\s*(.+@.+)/i
        $1
      else
        nil
      end
    end

    def user_unknown?
      case @status_part.body.to_s
      when /Status:\s*5\.1\.1/i, /User unknown/i, /Unknown user/i
        true
      else
        false
      end
    end
  end
end

うーん、きれいじゃないですね。 きれいな gem の出現を待ちます。 でも、こんなコードでもバウンスメールは大はばに減りました (^_^)v

最後に、弊社ではデザイナエンジニアを募集しております。 まずはお話だけでも。よろしくお願いします!

2014年のふりかえり

2014年12月26日
区分
2014年
報告者:
tahara

こんにちは、tahara です。

今日は仕事納めなので、この一年を(技師部隊的な観点で)ふりかえってみたいと思います。

いこーよ はサービス的にはいろいろ改善した(つもり)ですが、 システム的には大きく変っていません。 一方、開発プロセスの方は Docker のおかげで大きく変った印象です。 いま AngularJS を使った機能を実装中なので来年にはプロダクションに投入予定です。

セキュリティ的なものやネットワーク的なトラブルも多かった年でしたね。 みなさま、おつかれさまでした。

ということで、本来もありがとうございました。 よいお年を

Rails で LaTeX を使って帳票出力

2014年11月17日
区分
Rails
報告者:
tahara

こんにちは、tahara です。

システム開発で、もう15年くらい前からやってみたいことがありました。 Web アプリでの帳票出力に LaTeX を使うことです。 社内システムながらようやくその機会に恵まれました。

rails-latex といういい gem があり、めんどうなところは全部めんどうみてくれます。

おきまりの Gemfile

gem 'rails-latex'

platex と xdvipdfmx で PDF にするためのコマンドを作っておきます。

bin/platex_xdivpdfmx

#!/bin/sh

platex -halt-on-error -shell-escape -interaction=batchmode input && xdvipdfmx input

config/initializers/rails_latex.rb でそのコマンドを使うように指定します。

# platex のオプションは bin/platex_xdvipdfmx の中で指定している。
LatexToPdf.config.merge! :command => "#{Rails.root}/bin/platex_xdvipdfmx"

HTML と同じ感覚で ERB の使って LaTeX のコードを書きます。 レイアウも使えます。

app/views/layouts/application.pdf.erbtex

\documentclass{jsarticle}

\usepackage[T1]{fontenc}
\usepackage{textcomp}
\usepackage[utf8]{inputenc}
\usepackage{lmodern}
\usepackage[deluxe, multi]{otf}
\usepackage{tabularx}
\usepackage[dvipdfmx]{graphicx}
\usepackage{overpic}
\usepackage{float}
\usepackage{ascmac}

\pagestyle{empty}
<%= yield :preamble %>

\begin{document}

%均等割
\newcommand{\kintou}[2]{%
  \leavevmode
  \hbox to #1{%
    \kanjiskip=0pt plus 1fill minus 1fill
    \xkanjiskip=\kanjiskip
    #2}}

%セル内で中央揃えをする。
\newcolumntype{C}{>{\centering\arraybackslash}X}
%セル内で左揃え。
\newcolumntype{R}{>{\raggedright\arraybackslash}X}
%セル内で右揃え。
\newcolumntype{L}{>{\raggedleft\arraybackslash}X}

<%= yield %>
\end{document}

views/invoices/show.pdf.erb

<%# -*- latex -*- %>
<%= content_for :preamble do -%>
  \setlength{\voffset}{-33mm}
  \setlength{\textheight}{280mm}
  \setlength{\hoffset}{-10mm}
  \setlength{\textwidth}{180mm}
<% end -%>

\begin{flushleft}

  \begin{tabularx}{180mm}{|X|c|r|c|r|} \hline
    \multicolumn{1}{|c|}{契約内容} &
    掲載期間 &
    \multicolumn{1}{|c|}{単価} &
    数量 &
    \multicolumn{1}{|c|}{金額} \\ \hline
    <% @load_blancer.items.each do |item| %>
      <%=lesc item.ordered_product.name %> &
      <%=lesc range_start_end_date_with_formate(object: item, year_format_shortcuted: false) %> &
      <%=lesc number_to_currency_for_pdf(item.price) %> &
      <%=lesc number_to_currency(item.quantity, unit: '') %> &
      <%=lesc number_to_currency_for_pdf(item.quantity * item.price) %>
      \\ \hline
    <% end %>
    \multicolumn{4}{|r|}{税抜} &
      <%=lesc number_to_currency_for_pdf(@load_blancer.totaler.total_without_tax.to_i) %>
      \\ \hline
    \multicolumn{4}{|r|}{消費税(<%=lesc @load_blancer.totaler.consumption_tax %>\%)} &
      <%=lesc number_to_currency_for_pdf(@load_blancer.totaler.only_tax.to_i) %>
      \\ \hline
    \multicolumn{4}{|r|}{合計} &
      <%=lesc number_to_currency_for_pdf(@load_blancer.totaler.total_with_tax.to_i) %>
      \\ \hline
  \end{tabularx}

\end{flushleft}

そうそう HTML での h と同様に lesc で LaTeX 用のエスケープをおこなうために ApplicationHelper に lesc メソッドを作っておきます。

app/helpers/application_helper.rb

module ApplicationHelper
  def lesc(text)
    LatexToPdf.escape_latex(text)
  end
end

以上です。これで、夢が1つかないました。感謝。

最後に、弊社では デザイナーエジニア を募集しています。 まずはお話だけでも。よろしくお願いします!

SolrCloud で Solr を冗長化する

2014年10月20日
区分
Solr
報告者:
tahara

こんにちは、tahara です。

EC2 はとてもいいのですが、たまにインスタンスが勝手にリブートしたりするんですよね。 わかっています。Multi-AZ 配置にしましょう、ということですよね。

ということで SolrCloud で Solr を冗長化しました。

次のような構成で1シェード、2レプリカです。

  • ap-northeast-1a
    • サーバ1
      • Solr(レプリカ1)
      • ZooKeeper
  • ap-northeast-1c
    • サーバ2
      • Solr(レプリカ2)
      • ZooKeeper
    • サーバ3
      • ZooKeeper

ZooKeeper は多数決をおこなうので、サーバ3は Solr なしで ZooKeeper だけ動かしています。

設定は https://cwiki.apache.org/confluence/display/solr/SolrCloud に書いてあるとおりやれば特に問題なく簡単にできます。

Solr の schema.xml などを変更する場合、Solr の設定ファイルは ZooKeeper 管理になるのでちょっとしたコマンドをたたく必要があります。 次のような Capistrano タスクを作成しました。

set :solr_host, 'ec2-56-111-111-111.ap-northeast-1.compute.amazonaws.com'
namespace :solr do
  desc '設定ファイルのリロード'
  task :reload, hosts: solr_host do
    run "/opt/solr/scripts/cloud-scripts/zkcli.sh -zkhost localhost:2181 -cmd upconfig -confdir /opt/solr/solr/outing/conf -confname outing"
    run "curl -s 'http://localhost:8983/solr/admin/collections?action=RELOAD&name=outing&wt=json'"
  end
end

1つの Solr サーバに対して実行すれば他の Solr サーバにも反映されます。

ここで注意ずべきことは、必ず Solr の zkcli.sh を使うこと。 ZooKeeper の bin/zkCli.sh を使ってしまうと動かなくて半日無駄にしてしまいます。

あとひっかかったのが ZooKeeper が次のようなログが出して動いてくれないことでした。

2014-10-17 04:05:46,507 [myid:1] - INFO  [WorkerSender[myid=1]:QuorumCnxManager@193] - Have smaller server identifier, so dropping the connection: (2, 1)

EC2 特有のインスタンスを stop & start したら IP が変ってしまうのが原因のようで、 他のサーバの ZooKeeper を1つずつ再起動していったら、ちゃんと動くようになりました。

ZooKeeper を指定して Solr を起動するだけで、設定らしい設定をすることなくシェード、 レプリカを増やせる SolrCloud はとてもいいですね。 MySQL のマスター、スレーブ構成とは違い、どの Solr サーバに対しても参照、更新リクエストをなげられるのはとても便利です。

また Multi-AZ 配置で冗長化したため Solr のアクセスがアベイラビリティゾーンをまたぐ必要がなくなり、 アプリのレスポンスタイムが 10ms くらい向上しました。

これでいこーよは完全に Multi-AZ 配置になりました。 アベイラビリティゾーンが1つつぶれてもサービスがとまらないということが週末の心のやすらぎに大きく貢献してくれる、 というのが今回の一番大きな発見でした。

最後に、 おでかけ先探しに悩むパパ・ママを助けてくれるエンジニアを募集していますので、 よろしくお願いします。

Passenger みたいに touch で Unicorn をリスタート

2014年07月22日
区分
デプロイ
報告者:
tahara

こんにちは、tahara です。

いこーよ では Passenger を使っていました。 最近チケット毎に Docker でステージングを作り、リリースする流れになった中、 デプロイ時の再起動でリクエストがつまってしまうのが問題になってきました。 そのため Unicorn に変えました。

実は以前 Uricorn を使っていました(さらにその前は Passenger です)。 そのころはさくらの専用サーバでした。

さくらから AWS にのりかえた際に Passenger にしました。 AWS のオートスケールでのデプロイ対応のためです。

いこーよは NFS サーバを動かしており、全 Rails サーバは NFS マウントして最新のコードを共有しています。 そうすれば、いつオートスケールしても問題なく最新のコードを参照でき、デプロイ毎に AMI を使り直す必要もありません。 オートスケールでの Rails サーバのたちあがりも、起動時にコードを最新にする必要がないので高速です。

オートスケールで Rails サーバが起動、停止するなか、 どの Rails サーバがいま動いているかを気にすることなく、デプロイは NFS サーバに対してだけおこないます。

Passenger なら全 Rails サーバの再起動は NFS サーバで touch tmp/restart.txt するだけです。

しかし Unicorn では USR2 シグナルを各 Rails サーバでなげる必要があります。

というわけでちょっとハックしました。

unicorn.conf.rb

before_fork do |server, worker|
  # the following is highly recomended for Rails + "preload_app true"
  # as there's no need for the master process to hold a connection
  defined?(ActiveRecord::Base) and
    ActiveRecord::Base.connection.disconnect!

  # oldsig QUIT
  old_pid = "#{server.config[:pid]}.oldbin"
  if File.exists?(old_pid) && server.pid != old_pid
    begin
      Process.kill("QUIT", File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
    end
  end

  # watch restart.txt
  if worker.nr == 0
    listener = Listen.to('/app/current/tmp/unicorn_restart',
                         force_polling: true ) do |modified, added, removed|
      pid = server.config[:pid]
      if pid == server.pid
        begin
          Process.kill("USR2", File.read(pid).to_i)
        rescue Errno::ENOENT, Errno::ESRCH
        end
      end
    end
    listener.start
  end
end

上記のコードでは /app を NFS マウントしています。 デプロイ時にNFS サーバで touch /app/current/tmp/unicorn_restart/restart.txt します。 そうすると Unicorn 自身がファイル変更を検知し、自分自身に USR2 シグナルをなげます。 まるで Passenger みたい。

NFS の場合は inotify が使えません。そのため incron の使用もあきらめ、上記の Listen でも force_polling: true としています。

また NFS のマウントオプションに actimeo=3 を付けキャッシュの有効期間を 3 秒にしました。 デフォルトでは 60 秒なので、Unicorn の再起動までかなり待つはめになります。 ちなみに actimeo=0 にしたら Unicorn が起動しなくなってしまいましたorz

Passenger と同じく touch /app/curret/tmp/restart.txt にしたかったのですが、 /app/curret/tmp 以下にあるファイル数が多いとファイル監視で CPU リソースをかなり使ってしまいます。 そのため専用のディレクトリ /app/current/tmp/unicorn_restart を用意しました。

問題点は NFS サーバが単一障害点である、ことですね...

ちにみに、いこーよでは Passenger と Unicorn でほとんどパフォーマンスに差はありませんでした。

Docker を使ってどんどんステージング環境を作る方法

2014年05月27日
区分
Docker
報告者:
tahara

こんにちは、tahara です。

Docker を使って git push をトリガーにステージング環境をどんどんたてて開発しています。 いま見たら24面のステージング環境が動いていました。

新しいブランチを push すると Jenkins が Docker のコンテナを作りそこにデプロイしてくれます。 Jenknis では Git Pluign を使っています。 あとは shell script でガシガシとドロくさくやっています。

次がその shell script です。

#!/bin/bash

export SSH_DOCKER="ssh user@docker.example.com"

source ./config/jenkins/functions.sh

# 20 は /etc/init.d/skype の XSERVERNUM=20
source `ls ~/.dbus/session-bus/*-20`
export DBUS_SESSION_BUS_ADDRESS

# bundler
rvm_path=/home/user/.rvm /home/user/.rvm/bin/rvm-shell 'ruby-2.1.0' -c 'bundle install --path vendor/bundler'

# https://wiki.jenkins-ci.org/display/JENKINS/Git+Plugin
echo "GIT_COMMIT=$GIT_COMMIT"
echo "GIT_BRANCH=$GIT_BRANCH"
echo "GIT_PREVIOUS_COMMIT=$GIT_PREVIOUS_COMMIT"
echo "GIT_URL=$GIT_URL"
echo "GIT_AUTHOR_EMAIL=$GIT_AUTHOR_EMAIL"
echo "GIT_COMMITTER_EMAIL=$GIT_COMMITTER_EMAIL"


# まずいらないコンテナを削除する
delete_unused_containers


BRANCH=`echo $GIT_BRANCH | sed -e 's/^origin\///'`
echo $BRANCH

CONTAINER_NAME="d`echo $BRANCH | sed -e 's/^d\///;s/[-_\/].*//'`"
echo "CONTAINER_NAME=$CONTAINER_NAME"

HOST_NAME="${CONTAINER_NAME}.o.example.com"
echo "HOST_NAME=${HOST_NAME}"


if ${SSH_DOCKER} docker.io ps | grep "$CONTAINER_NAME *$"
then
    echo "container is already exists."
    CONTAINER_ID=`${SSH_DOCKER} docker.io ps | grep "$CONTAINER_NAME *$" | awk '{print $1;}'`
    CONTAINER_IP=$(${SSH_DOCKER} "docker.io inspect --format='' ${CONTAINER_ID}")
else
    echo "create container..."
    CONTAINER_ID=$(${SSH_DOCKER} docker.io run -d -t --name "${CONTAINER_NAME}" localhost:5000/outing)
    CONTAINER_IP=$(${SSH_DOCKER} "docker.io inspect --format='' ${CONTAINER_ID}")

    ${SSH_DOCKER} ssh-keygen -f "/home/user/.ssh/known_hosts" -R ${CONTAINER_IP}

    echo "setup nginx..."
    sed -e "s/_server_name_/${CONTAINER_NAME}.o.example.com/;s/_container_ip_/${CONTAINER_IP}/" config/server/staging/docker/nginx-site.conf | ${SSH_DOCKER} tee /etc/nginx/conf.d/${CONTAINER_NAME}.conf
    ${SSH_DOCKER} sudo service nginx reload
fi

echo "CONTAINER_ID=$CONTAINER_ID"
echo "CONTAINER_IP=$CONTAINER_IP"

# cap
rvm_path=/home/user/.rvm /home/user/.rvm/bin/rvm-shell 'ruby-2.1.0' -c "bundle exec cap docker deploy:migrations -s host_ip=$CONTAINER_IP -s host_name=${HOST_NAME} -s branch=$BRANCH"

if [ $? -ne 0 ] ; then
    sbcl --script /var/lib/jenkins/skype/skype.lisp ";( ごめんなさい、エラーになっちゃいました。 https://ci.example.com/job/outing_docker_gitlab/ から確認してください。 (heidy)"
    exit 1
else
    sbcl --script /var/lib/jenkins/skype/skype.lisp "(ninja) Capistrano: いこーよを 確認(ステージング) 環境 http://${HOST_NAME}/ にデプロイしました (h)`git log --pretty='%n%s%n%b  %an' HEAD...HEAD~ | head -n 10`"
fi

Jenkins と Docker は別のマシンで動いているので ssh 経由でいろいろやっている感じです。

functions.sh

#!/bin/bash

error_exit() {
    ssh deployer@tosa.actindi.net sbcl --script /var/lib/jenkins/skype/skype.lisp ";( ごめんなさい、エラーになっちゃいました。 https://ci.actindi.net/job/outing_master/ から確認してください。 (heidy)"
    exit 1
}

# いらないコンテナを削除する
delete_unused_containers() {
    for container_id in `${SSH_DOCKER} docker.io ps -q`
    do
        name=`${SSH_DOCKER} "docker.io inspect --format '' ${container_id}"`
        if echo ${name} | grep -q -E "^/d[0-9]+"
        then
            branch=`echo ${name} | sed -e 's|/d||'`
            if git branch -a --no-merged origin/master | grep -q "d/${branch}"
            then
                echo "${name} is alive."
            else
                echo "kill ${name} ${container_id}"
                ${SSH_DOCKER} docker.io kill ${container_id}
                ${SSH_DOCKER} docker.io rm ${container_id}

                ${SSH_DOCKER} rm -f /etc/nginx/conf.d/d${branch}.conf
                ${SSH_DOCKER} sudo service nginx reload
            fi
        fi
    done
}

master にマージ済みのブランチのコンテナは自動で削除するようにしています。

あと Docker コンテナ内の nginx は外からは見えないので、 次のような nginx の設定ファイルをコンテナごとに自動で作っています。 そして DNS の設定で *.o.example.com を docker.example.com(Docker のホスト) の CNAME にしています。

# HTTP server
server {
    listen 80;
    server_name _server_name_;
    location / {
        auth_basic "Restricted";
        auth_basic_user_file /etc/nginx/conf.d/outing-password;

        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header Host $host;
        proxy_redirect   off;
        proxy_pass       http://_container_ip_;
    }
}

# HTTPS server
server {
    listen 443;
    server_name _server_name_;

    ssl on;
    ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
    ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;

    ssl_session_timeout 5m;

    ssl_protocols SSLv3 TLSv1;
    ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
    ssl_prefer_server_ciphers on;

    location / {

        auth_basic "Restricted";
        auth_basic_user_file /etc/nginx/conf.d/outing-password;

        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;
        proxy_redirect   off;
        proxy_pass       http://_container_ip_;
    }
}

Docker のイメージは日次で本番の最新データをマスクしたもので作り直しているので、ステージングのデータは常に本番に近いものになります。

push するだけでブランチごとのステージングが作られるというのは思っていたより、ずっと快適なことでした。 DB のマイグレーションのあるタスクでも他タスクを気にせず作業できます。 以前は月2回の本番リリースもタスクごとにリリースできるようになりました。 とてもおすすめです。

Active Record で null でない、かつ空文字でない

2014年04月21日
区分
Rails
報告者:
tahara

こんなふうに書けるんですね。

[1] pry(main)> Experience.where.not(content: [nil, '']).to_sql
=> "SELECT `experiences`.* FROM `experiences`  WHERE (NOT ((`experiences`.`content` = '' OR `experiences`.`content` IS NULL)))"

Docker のはまりどころ

2014年03月24日
区分
Docker
報告者:
tahara

こんにちは、tahara です。

ちかごろ Docker を使ってステージング環境を作っています。

いろいろはまりました。

Btrfs 使ってる

Btrfs を使っていると pwd が失敗したりします。 Btrfs 上では AUFS だと問題があるようです。

docker のデーモンを -s btrfs オプション付きで動かしましょう。

Debian なら /etc/init.d/docker.io で指定すればいいと思います。

DOCKER_OPTS="-s btrfs"

apt-get が遅い

日本にいるなら /etc/apt/sources.list で ftp.jp.debian.org などを指定しましょう。

Dockerfile であらかじめ用意していたファイルを ADD するといいと思います。

ADD sources.list /etc/apt/sources.list

タイムゾーンの設定

Dockerfile で

RUN echo "Asia/Tokyo" > /etc/timezone && dpkg-reconfigure tzdata

ロケールの設定

Dockerfile で

RUN echo "ja_JP.UTF-8 UTF-8" > /etc/locale.gen \
    && apt-get install locales \
    && update-locale LANG=ja_JP.UTF-8 \
    && . /etc/default/locale

RUN sudo service mysql start したのに...

Dockerfile で RUN sudo service mysql start したのに RUN mysql -uroot でつながらない。

RUN ごとに起動したサービスは終っちゃうようです。 1 つの RUN の中にまとめましょう。

RUN sudo service mysql start && mysql -uroot < init.sql && sudo service mysql stop

サービスを起動したいけど Supervisor めんどくさそう

こんなシェルスクリプトを用意しておきます。

#!/bin/bash

service ssh start
service mysql start
service memcached start
service td-agent start
service solr start
service nginx start

# /bin/bash -l
while true
do
    sleep 7
done

Dockerfile で

ADD start.sh /start.sh
CMD /start.sh

ssh でつなぐとプロンプトが表示されない

-t オプションをつけてコンテナを実行しましょう。

docker.io run -d -t -name foo localhost:5000/outing

コンテナ起動時に id と ip アドレスをとりたい

ID=$(docker.io run -d -name '1234' localhost:5000/outing)
IP=$(docker.io inspect -format="" $ID)
echo $ID $IP

いらないコンテナを削除したい

起動中のコンテナは削除しようとしてもできないので、次で削除しています。

docker.io rm $(docker.io ps -a -q)

いらないイメージを削除したい

イメージの作り直しをしていると、いらないイメージがたくさんできます。

リポジトリが <none> になっているのはきっといらないこなので、次で削除しています。

docker.io images | grep '<none>' | awk '{print $3}' | xargs docker.io rmi

イメージを別のサーバにコピーしたい

save と load でできます。

docker.io save localhost:5000/outing | ssh -C user@another.example.com docker.io load

IP アドレスが変わるので ssh が...

WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! としかられる。

-o 'StrictHostKeyChecking no' オプション付けましょう。

ssh -o 'StrictHostKeyChecking no' user@172.17.0.2'

Docker いいですね。 テスト用の MySQL や Solr のデータ準備に何十分もかかる問題も イメージを作る時にやっておけば解決です。 ディスクも節約できます。

GitLab 導入しました

2014年01月30日
区分
開発
報告者:
tahara

こんにちは、tahara です。

弊社もエンジニア一人体制がおわり、コードレビューが必要になってきました。

ついに GitHub Enterprise か! とも思いましが残念ながらコスト的に却下。

GitLab 導入しました。

インストールはこちらのページの手順を愚直に実行でうまくいきました。

ただし https で運用したいので /home/git/gitlab-shell/config.yml で self_signed_cert を true にし

gitlab_url: "https://foo.bar.baz/"

http_settings:
#  user: someone
#  password: somepass
#  ca_file: /etc/ssl/cert.pem
#  ca_path: /etc/pki/tls/certs
  self_signed_cert: true

/home/git/gitlab/config/config/gitlab.yml で次の設定にしました。

  gitlab:
    ## Web server settings
    host: foo.bar.baz
    port: 443
    https: true

あとは Chrome に MISAWA::MD を入れて、楽しいコードレビューライフです!

GitLab の導入から2か月ほどたちましたが、とてもいいです。 唯一の問題は私が みさわ 画像が使いこなせいことかも。 精進します。

Rails で is null と is not null

2013年12月03日
区分
Rails
報告者:
tahara

こんにちは、tahara です。

Rails で is null と is not null ってどうやって書くの?

is null

Foo.where(bar: nil)

is not null

Foo.where.not(bar: nil)

http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods/WhereChain.html#method-i-not

ちゃんときれいに書けるんですね。

assets:precompile が遅い

2013年11月01日
区分
Rails
報告者:
tahara

こんにちは、tahara です。

ようやく いこーよエンパ においつき Rails4 になりました! おいつかれたエンパ担当者は、「エンパはRails6くらいにしたい」などのたまっていましたが、 その時はエイリアンテクノロジーでつきはなしたいと思っています。

さて、Rails4 になって困ったことがありました。 Rails3 の時は10分ちょっとで終っていた本番デプロイが20分くらいかかるようになってしまったのです。 assets:precompile がとても遅いのです。

たくさんの画像ファイルと、大きな CSS ファイルがわるいのですが、 さすがに時間がかかりすぎます。

なんとかならないものかと、最初に試してみたのが Rails4なのにassets:precompileが遅い です。 これで速くなった! と喜んでいたのですが、 同じファイル名のまま画像を変えた時に *.js.erb が 再プリコンパイルされないという問題にぶつかり、あきらめました。

次に試したのが Rails deployments from 4 minutes to 40 seconds です。 ローカルで assets:precompile して tar でかため、それをアップロードする方法です。 これはうまくいきました! 特に問題もなく Rails3 の時と同じくらいの時間で本番デプロイできるようになりました。 よかった。

上記のエントリのコードにはこちらでは使わないものが入ってたりしたので、 次のようなコードを config/deploy.rb に書いています。

namespace :deploy do
  namespace :assets do
    desc <<-DESC
      Precompiles assets locally.
    DESC
    task :precompile, :roles => lambda { assets_role }, :except => { :no_release => true } do
      run_locally "rm -rf public/assets tmp/cache"
      run_locally "RAILS_ENV=#{rails_env.to_s.shellescape} #{rake} assets:precompile"
      run_locally "cd public && tar -jcf assets.tar.bz2 assets"
      top.upload "public/assets.tar.bz2", "#{shared_path}", via: :scp
      run "cd #{shared_path} && rm #{shared_path.shellescape}/#{shared_assets_prefix}/manifest-* && tar -jxf assets.tar.bz2 && rm assets.tar.bz2"
      run_locally "rm -rf public/assets.tar.bz2 public/assets tmp/cache"

      # Copy manifest to release root (for clean_expired task)
      run <<-CMD.compact
        cp -- #{shared_manifest_path.shellescape} #{current_release.to_s.shellescape}/assets_manifest#{File.extname(shared_manifest_path)}
      CMD
    end
  end
end

そんなわけで、そろそろ並列 assets:precompile が導入されないかな、と期待しています。

act-fluent-logger-rails を Rails4 対応にしました

2013年10月02日
区分
act-fluent-logger-rails
報告者:
tahara

こんにちは、tahara です。

Rails で Fluentd へログ出力する act-fluent-logger-rails を Rails4 対応にしました。

いままで config.log_tags = [ :subdomain, :uuid ] 等を指定していても無視していましたが、 ようやくタグを付けられるようにしました。遅くなってごめんなさい。

config/environments/production.rb で次のように書きます。

Foo::Application.configure do
  # Use a different logger for distributed setups
  config.logger = ActFluentLoggerRails::Logger.
    new(log_tags: {
          ip: :ip,
          ua: :user_agent,
          uid: ->(request) { request.session[:uid] }
        })
end

config.log_tags は使わないでください。使うと動かないです。

また次のように実行時に任意のタグを付けられます。

Rails::logger[:foo] = "foo value"

development 環境では普通のロガーを使いたい場合は config/initializers/logger.rb でも作るといいと思います。

# -*- coding: utf-8 -*-
# ActFluentLoggerRails::Logger を production の時だけ使うので
# development 環境の場合にエラーにならないようにする。
->(x) {
  unless x.respond_to?(:[]=)
    def x.[]=(k, v)
      debug("logger #{k} => #{v}")
    end
    def x.[](k)
    end
  end
}.call(Rails::logger)

さて、弊社ではエンジニアを募集をしています。 まずはランチを食べながらお話だけでも!

もうバックアップ壊れませんように

2013年09月09日
区分
バックアップ
報告者:
tahara

こんにちは、tahara です。

2つのハードディスクで RAID0 になっている外付けハードディスク2台で バックアップ用の Btrfs のファイルシステムを作っていました。

ある日2台ある外付けハードディスクのうち1台の AC アダプタが外れてしまいました。 そして Btrfs 復旧できませんでした。

Btrfs は全く悪くないのですが、RAID0 をやめて4台のハードディスクで ZFS の RAIDZ2 でファイルシステムを作り直しました。

お手がる zfs-fuse を使います。

sudo apt-get install zfs-fuse
sudo modprobe fuse

ディスクは /dev/disk/by-id の下の名前で指定するのがいいとのことです。

ls -l /dev/disk/by-id
lrwxrwxrwx 1 root root  9  8月 12 15:40 usb-BUFFALO_HD-WLU3_R1_1_000001070C58-0:0 -> ../../sdf
lrwxrwxrwx 1 root root  9  8月 12 15:40 usb-BUFFALO_HD-WLU3_R1_1_00000107800D-0:0 -> ../../sdh
lrwxrwxrwx 1 root root  9  8月 12 15:40 usb-BUFFALO_HD-WLU3_R1_2_000001070C58-0:1 -> ../../sdg
lrwxrwxrwx 1 root root  9  8月 12 15:40 usb-BUFFALO_HD-WLU3_R1_2_00000107800D-0:1 -> ../../sdi

RAIDZ2 を作ります。

sudo zpool create tank raidz2 \
 /dev/disk/by-id/usb-BUFFALO_HD-WLU3_R1_1_000001070C58-0:0 \
 /dev/disk/by-id/usb-BUFFALO_HD-WLU3_R1_1_00000107800D-0:0 \
 /dev/disk/by-id/usb-BUFFALO_HD-WLU3_R1_2_000001070C58-0:1 \
 /dev/disk/by-id/usb-BUFFALO_HD-WLU3_R1_2_00000107800D-0:1

gzip 圧縮指定でファイルシステムを作り、マウントします。 /etc/fstab とか書かなくていいんですね。

sudo zfs create -o compress=gzip tank/backup
sudo zfs set mountpoint=/backup tank/backup

これで日々 rsync してスナップショットです。

sudo crontab -e

0  8 * * * /sbin/zfs snapshot tank/backup@`date +%Y%m%d`

Zfs 簡単でいいです。

CloudFront + nginx + S3 でのサムネイル画像の動的生成

2013年06月26日
区分
nginx
報告者:
tahara

こんにちは、tahara です。

以前 nginx を使ったサムネイル画像の動的生成 でサムネイル画像の動的生成について書きました。 その後 Paperclip の画像保存先をファイルシステムから S3 に変更しましたので、 あらためて CloudFront + nginx + S3 でのサムネイル画像の動的生成について書きたいと思います。

今回も 簡単!リアルタイム画像変換をNginxだけで行う方法 | cloudrop をおおいに参照させていただきました(感謝です)。

Paperclip の設定です。 CloudFront を使うにあたり次のような感じにします。

  • s3_host_name に CloudFront のホスト名
  • url に ":s3_alias_url"
  • s3_protocol に "" (http でも https でもいけるように)
# Paperclip
config.paperclip_defaults = {
  storage: :s3,
  s3_credentials: {
    access_key_id: "XXXXXXXXXXXXXXXXXXXX",
    secret_access_key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  },
  s3_host_name: "s3-ap-northeast-1.amazonaws.com",
  bucket: "your.backet.name",
  path: "/system/:class/:attachment/:id/:style.:extension",
  s3_host_alias: "xxxxxx.cloudfront.net",   # CloudFront のホスト名
  url: ":s3_alias_url",
  s3_protocol: "",       # empty string to generate scheme-less URLs
  :default_style => :normal,
  :default_url => "/assets/missing_:class_:attachment_:style.png",
  :auto_orient => true
}

config/initializers/paperclip.rb で nginx の HttpImageFilterModule を使って crop, resize するためのメソッドを書きます。 前回と違い S3 に非公開で保存した画像を参照するための expiring_url でも crop, resize するメソッドを追加しています。

module Paperclip
  module ClassMethods
  # nginx の HttpImageFilterModule で crop, resize するメソッド
  class Attachment
    def crop(width, height, quality=75)
      ret = "#{url}&w=#{width}&h=#{height}"
      if quality != 75
        ret += "&q=#{quality}"
      end
      ret
    end

    def resize(width, height, quality=75)
      crop(width, height, quality) + "&t=r"
    end

    def expiring_crop(width, height, quality: 75, time: 1800)
      ret = "#{expiring_url(time)}&w=#{width}&h=#{height}"
      if quality != 75
        ret += "&q=#{quality}"
      end
      ret.sub(/^.*?\?/, url.sub(/\?.*$/, '?'))
    end

    def expiring_resize(width, height, quality: 75, time: 1800)
      expiring_crop(width, height, quality: quality, time: time) + "&t=r"
    end
  end
end

nginx の設定では次に注意です。

  • S3 への proxy_pass のために
    • resolver 172.16.0.23;
  • ブラウザからのリクエストに Basic 認証の Authorization ヘッダが付いていると S3 に蹴られるので
    • proxy_set_header Authorization "";
  • CloudFront 経由で expiring_url を使った時 S3 に蹴られるので
    • proxy_set_header X-Amz-Cf-Id "";
  • proxy_pass での image_filter のために
    • image_filter_buffer 5m;

nginx.conf はこんな感じです(関係のあるとこだけ抜粋)。

http {
    # S3 への proxy_pass のために必要。172.16.0.23 は AWS での DNS みたい
    resolver 172.16.0.23;
    server {
        location ~* ^/system/.*\.(jpg|jpeg|jpe|png|gif)$ {
            if ($query_string ~ [?&][wh]=.*) {
               rewrite ^/(.*)$ /image_filter/$1 last;
            }
            rewrite ^/(.*)$ /s3_original/$1 last;
        }
        location ~* ^/system/.*$ {
            rewrite ^/(.*)$ /s3_original/$1 last;
        }
        location ~* ^/s3_original/(.*)$ {
            internal;
            expires 1y;
            set $file $1;
            proxy_set_header Authorization "";
            proxy_set_header X-Amz-Cf-Id "";
            proxy_pass http://s3-ap-northeast-1.amazonaws.com/example.com/$file$is_args$args;
        }
        location ~* ^/image_filter/(.*)$ {
            internal;
            set $file $1;
            set $width 150;
            set $height 150;
            set $quality 75;
            if ($arg_w ~ (\d*)) {
                set $width $1;
            }
            if ($arg_h ~ (\d*)) {
                set $height $1;
            }
            if ($arg_q ~ (100|[1-9][0-9]|[1-9])) {
                set $quality $1;
            }
            if ($arg_t = "r") {
                rewrite ^ /resize last;
            }
            rewrite ^ /crop last;
        }
        location /resize {
            internal;
            expires 1y;
            proxy_set_header Authorization "";
            proxy_set_header X-Amz-Cf-Id "";
            proxy_pass http://s3-ap-northeast-1.amazonaws.com/example.com/$file$is_args$args;
            image_filter_buffer 5m;
            image_filter  resize  $width $height;
            image_filter_jpeg_quality $quality;
            error_page 415 = @empty;
        }
        location /crop {
            internal;
            expires 1y;
            proxy_set_header Authorization "";
            proxy_set_header X-Amz-Cf-Id "";
            proxy_pass http://s3-ap-northeast-1.amazonaws.com/example.com/$file$is_args$args;
            image_filter_buffer 5m;
            image_filter  crop  $width $height;
            image_filter_jpeg_quality $quality;
            error_page 415 = @empty;
        }
        location @empty {
            empty_gif;
        }
    }
}

なかなか proxy_set_header X-Amz-Cf-Id ""; にたどり着けず泣きそうになりましたが、 これで画像アップし放題、リサイズし放題です!

もう一つ注意すべきことが、 model.photo.exists? を使うとその都度 S3 に HEAD リクエストを投げます。 model.photo.present? を使いましょう。 さもないと、レスポンスタイムがひどいことになります。

最後に、ひき続きエンジニアを募集していますので、よろしくお願いします!

ModSecurity を nginx + passenger で

2013年05月07日
区分
ModSecurity
報告者:
tahara

こんにちは、tahara です。

Web Application Firewall の定番(?) ModSecurity を nginx + passenger でセットアップしてみました。

ModSecurity をコンパイル

cd /home/foo/src
wget http://www.modsecurity.org/tarball/2.7.3/modsecurity-apache_2.7.3.tar.gz
tar xvf modsecurity-apache_2.7.3.tar.gz
cd modsecurity-apache_2.7.3
./configure --enable-standalone-module
make

nginx のソースを準備

cd /home/foo/src
wget http://nginx.org/download/nginx-1.2.8.tar.gz
tar xvf nginx-1.2.8.tar.gz

passenger で nginx + ModSecurity をコンパイル

passenger-install-nginx-module
Enter your choice (1 or 2) or press Ctrl-C to abort: 2
Please specify the directory: /home/foo/src/nginx-1.2.8
Extra arguments to pass to configure script: --add-module=/home/foo/src/modsecurity-apache_2.7.3/nginx/modsecurity

—add-module=/home/foo/src/modsecurity-apache_2.7.3/nginx/modsecurity がポイントです。

設定ファイルの準備ですが、ModSecurity を nginx で使う場合は Apache の場合と違い、 設定ファイルを1ファイルに結合してやる必要があります。 ルールは https://github.com/SpiderLabs/owasp-modsecurity-crs から git clone して持ってきます。 あと *.data ファイルは modsecurity.conf と同じディレクトリに置いておきます。

cd /home/foo/src
git clone https://github.com/SpiderLabs/owasp-modsecurity-crs.git
mkdir /opt/nginx/conf/modsecurity
cat /home/foo/src/modsecurity-apache_2.7.3/modsecurity.conf-recommended \
    /home/foo/src/owasp-modsecurity-crs/modsecurity_crs_10_setup.conf.example \
    /home/foo/src/owasp-modsecurity-crs/base_rules/*.conf \
  > /opt/nginx/conf/modsecurity/modsecurity.conf
cp /home/foo/src/owasp-modsecurity-crs/base_rules/*.data /opt/nginx/conf/modsecurity

nginx の設定です。

server {
    listen 80;
    root /var/www/example/public;
    server_name foo.example.com;

    #### ModSecurity
    # ModSecurity を使う時は passenger_buffers を指定しないと、レスポンスが返らない。
    passenger_buffers 1024 1024k;
    location / {
       ModSecurityEnabled on;
       ModSecurityConfig modsecurity/modsecurity.conf;
       passenger_enabled on;
       rails_env production;
    }
}

十分な大きさの passenger_buffers を指定してやらないと、うまくレスポンスが返りません。 どのくらいの大きさがいいのかは調査中です。

passenger を使わない場合はプロキシーとして次のように設定します。 この場合も proxy_buffers の指定が必要なようです。

location / {
   ModSecurityEnabled on;
   ModSecurityConfig modsecurity/modsecurity.conf;
   proxy_pass http://localhost:40080;
   proxy_buffers 80 8k;
}

だいたいこんな感じでできあがりだと思います。

ModSecurity の nginx はまだ RC だし、passenger と同時使用してもいいものかも不明です。 とりあえず、動くようですが、ときどき nginx のプロセスが CPU 100% 持っていってしまいます。

/opt/nginx/conf/modsecurity/modsecurity.conf の中もちゃんと精査する必要もあります。

ひまを見付けてつめていきたいと思います。

最後に、ひき続きエンジニアを募集していますので、よろしくお願いします!

Stumpwm で Redmine のチケットのタイトルをかしこくコピペ

2013年04月08日
区分
Stumpwm
報告者:
tahara

こんにちは、tahara です。

前回は Stumpwm 使えば Redmine のチケットのタイトルを簡単にコピペできるよ、というのを紹介しました。

今回はさらにかしこくコピペしてみたいと思います。

自分のコミット時の行動を分析してみます。

  • Stumpwm を使っている。
  • Redmine のチケットは Firefox で見ている。
  • Firefox には Vimperator が入っている。
  • Git のコミットは Emacs から行なっている。

これらから次のようなヒューリスティックが得られます。

  1. 今開いているウインドウ(Emacs のはず)を覚える。
  2. 作業中のチケットを開いているはずの Firefox を表示する。
  3. "y" で Firefox からチケットの URL をクリックボードにコピーする。
  4. 1. で覚えていたウインドウを表示。
  5. チケットの URL から Redmine の API 経由でチケットのタイトルを取得する(前回のエントリ参照)。
  6. "C-y" でチケットのタイトルを(Emacs に)はりつける。

いかにも便利そうですよね。

その実装です。

(defcommand rr () ()
  "set the issue subject to a clibboard from Firefox with Vimperator."
  (labels ((send-key-seq (key-seq)
             (loop with window = (current-window)
                   for key in (stumpwm::parse-key-seq key-seq)
                   do (stumpwm::send-fake-key window key))))
    ;; 1. 今開いているウインドウ(Emacs のはず)を覚える。
    (let ((win (current-window)))
      ;; 2. 作業中のチケットを開いているはずの Firefox を表示する。
      (run-commands "firefox")
      (sleep 0.1)
      ;; 3. "y" で Firefox からチケットの URL をクリックボードにコピーする。
      (send-key-seq "y")
      (let* ((url (get-x-selection))
             (id (parse-integer url :start (1+ (position #\/ url :from-end t))
                                    :junk-allowed t)))
        ;; 4. 1. で覚えていたウインドウを表示。
        (stumpwm::pull-window win)
        ;; 5. チケットの URL から Redmine の API 経由でチケットのタイトルを取得する。
        (run-commands (format nil "r ~a" id))
        ;; 6. "C-y" でチケットのタイトルを(Emacs に)はりつける。
        (send-key-seq "C-y")))))

Stumpwm 最高!!

最後に、弊社ではエンジニアデザイナ募集をしています。 まずはランチを食べながらお話だけでも!

Stumpwm で Redmine のチケットのタイトルを簡単にコピペ

2013年04月03日
区分
Stumpwm
報告者:
tahara

こんにちは、tahara です。

今日は Stumpwm 使えば Redmine のチケットのタイトルを簡単にコピペできるよ、というお話です。

ataka さんに Redmine を 0.8.5 というすごく古いものか最新にアップデートしてもらいました。 いろいろ便利になったのですが、チケット詳細画面で以前はチケットの ID とタイトルが同じ行に 並んで表示さていたのに、別の行に表示されようになってしまいました。

これから

これになった

git のコミットメッセージに

refs #9999 いこーよ: 口コミタグ付大作戦!

というようなコメントを入れていたので、簡単にコピペできないのは大きな問題です。

幸い私は Stumpwm ユーザですので、次のようなコードを書いて解決できました。

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; Redmine
;;;;
;;;; sudo apt-get install xclip
;;;;
(ql:quickload :drakma)
(ql:quickload :cl-json)

(defun redmine-issue (id)
  (let* ((drakma:*text-content-types* '(("application" . "json"))))
    (cdr (assoc :issue
                (json:decode-json-from-string
                 (drakma:http-request
                  (format nil "https://example.com/issues/~a.json" id)
                  :additional-headers '(("X-Redmine-API-Key" . "xxxxxxxxxxxxxxxxxxxx"))))))))

(defun redmine-issue-subject (id)
  (let ((issue (redmine-issue id)))
    (format nil "#~a ~a: ~a" id
            (cdr (assoc :name (cdr (assoc :project issue))))
            (cdr (assoc :subject issue)))))

(defcommand r (id) ((:number "#: "))
  "set the issue subject to a clibboard."
  (let ((subject (redmine-issue-subject id)))
    (run-shell-command (format nil "echo \"~a\" | xclip -selection CLIPBOARD"
                               subject))
    (message "~a" subject)))

~/.stumpwmrc にこれを書いておけば

: r 10000

でクリップボードに「#9999 いこーよ: 口コミタグ付大作戦!」が入ります。

Stumpwm 最高!

最後に、弊社ではエンジニアデザイナ募集をしています。 まずはランチを食べながらお話だけでも!

nginx を使ったサムネイル画像の動的生成

2013年03月04日
区分
nginx
報告者:
tahara

こんにちは、tahara です。

画像ファイルのアップロードには Paperclip を使っています。 Paperclip は写真アップロード時にサムネイル画像を作ってくれてとても便利です。

でも、サイトのデザインが変更になって、サムネイル画像のサイズを変える必要が出た場合は、 当然のことながら全画像のリサイズ作業が必要になります。 そこで、サムネイル画像の動的生成をやってみることにしました。

要は、クックパッドさんの TOFU ですね。

nginx を使っていることもあり、簡単!リアルタイム画像変換をNginxだけで行う方法 | cloudrop をおおいに参照させていただきました(感謝です)。

次のオプションを付けて nginx をコンパイルします。

--with-http_image_filter_module

nginx の設定はこんな感じです。

location ~* /system/.*\.(jpg|jpeg|png|gif)$ {
    if ($query_string ~ .*=.*) {
      rewrite ^/system/(.*)$ /image_filter/$1 last;
    }
}

location ~* ^/image_filter/(.*)$ {
    internal;

    set $file $1;
    set $width 150;
    set $height 150;
    set $quality 75;

    if ($arg_w ~ (\d*)) {
        set $width $1;
    }
    if ($arg_h ~ (\d*)) {
        set $height $1;
    }
    if ($arg_q ~ (100|[1-9][0-9]|[1-9])) {
        set $quality $1;
    }

    if ($arg_t = "r") {
        rewrite ^ /resize last;
    }

    rewrite ^ /crop last;
}

location /resize {
    internal;
    rewrite ^ /system/$file break;
    image_filter  resize  $width $height;
    image_filter_jpeg_quality $quality;
    error_page 415 = @empty;
    expires max;
}

location /crop {
    internal;
    rewrite ^ /system/$file break;
    image_filter  crop  $width $height;
    image_filter_jpeg_quality $quality;
    error_page 415 = @empty;
    expires max;
}

location @empty {
    empty_gif;
}

Paperclip の Attachment クラスに crop と resize メソッドを追加します。

# -*- coding: utf-8 -*-
module Paperclip
  # nginx の HttpImageFilterModule で crop, resize するメソッド
  class Attachment
    def crop(width, height, quality=75)
      ret = "#{url}&w=#{width}&h=#{height}"
      if quality != 75
        ret += "&q=#{quality}"
      end
      ret
    end

    def resize(width, height, quality=75)
      crop(width, height, quality) + "&t=r"
    end
  end
end

いまだに rhtml 使っているビューで次のようにすれば OK.

<%= link_to(image_tag(facility.picture_1.crop(190, 190), alt: facility.name), facility_path(facility.id)) %>

負荷が気になりましたが、 Amazon CloudFront のおかげで全く問題ありませんでした。

これで柔軟にサムネイルサイズを変えながらデザイを行えるようになりました。

最後に、弊社では 100万組の親子を笑顔にするデザイナーを募集 しています。 まずはお気軽にお話だけでもさせてください。

jQuery Mobile で 一覧 → もっとみる → 詳細 → 戻る

2013年02月18日
区分
jQueryMobile
報告者:
tahara

こんにちは、tahara です。

jQuery Mobile のサイト。

  1. 一覧ページで次ページのかわりに「もっとみる」リンクがある。
  2. 「もっとみる」リンククリックで Ajax で次ページ分を一覧に追加。
  3. 一覧からどれか選んで詳細ページへ。
  4. ブラウザの「戻る」

とすると、「もっとみる」で続み込んだ分が一覧ページからなくなっている現象についてです。

詳細ページから「戻る」した時に jQuery Mobile の onPopState によって、 「もっとみる」クリック前の 1 ページ目がリクエストされてしまうんですね。

この対策として histroy.replaceState を使って詳細ページから戻った時の onPopState で2 ページ目がリクエストされるように history を書き換えれば いいのでは? と思いやってみました。

var url_back = "2ページ目の URL";
var state = {
    hash: "#" + url_back,
    title: document.title,
    initialHref: $.mobile.path.parseLocation().hrefNoHash
};
history.replaceState(state, document.title, url);

これで2ページ目に戻るようになりました。

しかし、スクロール位置がおかしい。 普通は戻った時に詳細に遷移する前の位置にスクロールするのに、 ページの一番上を表示したままスクロールしません。

jQuery Mobile のソースを読んでみると $.mobile.urlHistory の中に URL とスクロール位置などを持っているようでした。

var active = $.mobile.urlHistory.getActive();
active.url = url_back;
active.pageUrl = url_back;

というように $.mobile.urlHistory の中も書き換えてみたら、 戻った時にちゃんと前の位置にスクロールするようになりました。

jQurey Mobile で history.replaceState する時は $.mobile.urlHistory の方もあわせて書き換えないと、というお話でした。

最後に、弊社では100万組の親子を笑顔にするデザイナー募集しています。 ご興味のある方は一緒にランチでもとりながらお話しさせてください。

Rails ログを MongoDB と S3 につっこむ

2013年01月21日
区分
Rails
報告者:
tahara

こんにちは、tahara です。

そろそろデータマイニングなんか初めてみたいな、と思っている今日このごろです。 その前段階として、Fluentd を使って Rails のログを MongoDB と S3 に格納するようにしてみました。

で、そのために act-fluent-logger-rails を作りました。 Rails から fluent-logger-ruby を使う gem です。

Gemfile に gem 'act-fluent-logger-rails' を追加して bundle install を実行し、 config/environments/production.rb などで ActFluentLoggerRails::Logger を使うように指定します。

config.log_level = :info
config.logger = ActFluentLoggerRails::Logger.new

config/fluent-logger.yml を作ります。

development:
  fluent_host:   '127.0.0.1'
  fluent_port:   24224
  tag:           'foo'
  messages_type: 'string'

test:
  fluent_host:   '127.0.0.1'
  fluent_port:   24224
  tag:           'foo'
  messages_type: 'string'

production:
  fluent_host:   '127.0.0.1'
  fluent_port:   24224
  tag:           'foo'
  messages_type: 'string'

act-fluent-logger-rails は1リクエスト中の全ログ出力を、1つのログイベントとして Fluentd に出力します。 messages_type: 'string' の時はこんな感じで各ログメッセージを join("\n") して String で

2013-01-18T15:04:50+09:00 foo {"messages":"Started GET \"/\" for 127.0.0.1 at 2013-01-18 15:04:49 +0900\nProcessing by TopController#index as HTML\nCompleted 200 OK in 635ms (Views: 479.3ms | ActiveRecord: 39.6ms)"],"level":"INFO"}

messages_type: 'array' にした時はこんな感じで各ログメッセージを Array で出力します。

2013-01-18T15:04:50+09:00 foo {"messages":["Started GET \"/\" for 127.0.0.1 at 2013-01-18 15:04:49 +0900","Processing by TopController#index as HTML","Completed 200 OK in 635ms (Views: 479.3ms | ActiveRecord: 39.6ms)"],"level":"INFO"}

Rails の動くサーバに td-agent をインストールして次の設定を行ないログサーバにログをフォワードします。

/etc/td-agent/td-agent.conf

<match foo>
  type forward

  <server>
    name log
    host log.example.com
    port 24224
  </server>

  buffer_type file
  buffer_path /foo/bar/fluent
  buffer_chunk_limit 8m
  buffer_queue_limit 256
</match>

ログサーバ log.example.com にも td-agent をインストールして次の設定を行ない S3 と MongoDB にログをつっこみます。

<source>
  type forward
</source>

<match foo>
  type copy

  #### s3
  <store>
    type s3

    aws_key_id XXXXXXXXXXXXXXXX
    aws_sec_key xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    s3_bucket iko-yo.net
    s3_endpoint s3-ap-northeast-1.amazonaws.com
    path logs/app/

    time_slice_format %Y%m%d
    time_slice_wait 10m

    buffer_type file
    buffer_path /opt/fluent/buffer/s3
    buffer_chunk_limit 256m
    buffer_queue_limit 8
  </store>

  #### mongoDB
  <store>
    type mongo_outing

    database outing
    collection logs.app
    nodes xxx.ap-northeast-1.compute.amazonaws.com:27017,yyy.ap-northeast-1.compute.amazonaws.com:27018

    # for capped collection
    capped
    capped_size 4096m

    # flush
    flush_interval 10s

    buffer_type file
    buffer_path /opt/fluent/buffer/mongo
    buffer_chunk_limit 8m
    buffer_queue_limit 256
  </store>
</match>

さて、データマイニングがんばります!

2012年のふりかえり

2012年12月27日
区分
2012年
報告者:
tahara

こんにちは、tahara です。

今日は仕事納めなので、この一年をふりかえってみたいと思います。

1月

いこーよを Rails2 から Rails3 にアップデート。しんどかった。

2月

お墓・納骨堂・葬儀のポータルサイト エンディングパーク オープン。

3月

実相寺青山霊廟 サイトオープン

いこーよにパフォーマンス対策として memcached 導入。キャッシュはすごい。

4月

いこーよのサーバをさくらの専用サーバから AWS に移行。わーい EC2 だ!

いこーよマイページのリニューアル。

5月

いこーよ GW のピークにそなえ MySQL をマスタスレーブ構成にした(GW が終ったらスレーブ停止)。

いこーよ 5月3日の 917,241 PV/日(2,913rpm) をのりきる。AWS のオートスケール万歳!

いこーよ 5月4日の 3,092rpm をのりきる。

いこーよ SQL の like による全件検索から MySQL の全文検索に切り替える。

6月

梅雨

7月

いこーよ プレゼンターページリニューアル。

いこーよ Google Maps API から Yahoo! JavaScript マップ API に移行。有料化むりです・・・

ついのすみか プチリニューアル。

8月

いこーよ スマートホン用サイトオープン。

いこーよ 11,385,932 PV/月 をのりきる。

いこーよ 8月14日の 4,256rpm をのりきる。一人でよくがんばった。

9月

いこーよ ようやく Asset Pipeline にする。

いこーよ Amazon CloudFront 導入。

10月

いこーよ MySQL の全文検索から Apache Solr (Sunspot) の全文検索に切り替え。Solr 速い!

11月

あれ? 特にないな・・・

12月

待望の2人目のエンジニア aoki 入社。 ひき続き、エンジニアおよびデザイナー募集しています。@tahara までご気軽るにご相談ください。

おわりに

という感じの一年でした。

いこーよは Rails3 にしたり AWS にしたり memcached 入れたり Solr 入れたりと、アーキテクチャが大幅に変わりました。

来年はデータマイニングで親子のおでかけに貢献していきたいと思います。 エンジニア一人体制も過去のものとなり、何かできる気がします!

一年間ありがとうございました。

良いお年をお迎えください。

skipfish で Web アプリケーションのセキュリティテストを行う

2012年12月17日
区分
セキュリティ
報告者:
tahara

こんにちは、tahara です。

おかげさまで、アクトインディ技術部隊は aoki の入社により 100% 増量しました! ありがとうございます!!

さて、浮かれてばかりはいられませんので、今回は skipfish を使ってセキュリティテストを行ってみたいと思います。 脆弱性を洗い出します!

skipfish は URL さえ指定しすれば、リンクをたどって自動的にテストを行ってくれる、なかなかできるこです。 Basic 認証やフォーム認証にも対応しています。

Rails プロジェクトで skipfish を使う時の注意点をあげてみます。

  • ずごい勢いで大量にアクセスを行うので、本番環境で実行してはいけません。
  • ずごい勢いで大量にアクセスを行うので、development 環境ではなく、より速い production 環境で実行した方がいいと思います。
  • production 環境を使うのは色々危険なので、ネットワークは切った方がいいかも。
  • 特にメール送信とかしちゃうと大変なので、気を付けましょう。
  • メールを送信しないように config/environments/production.rb で config.action_mailer.delivery_method = :test して動かします。
  • Rew Relic の gem を Gemfile から削除します。
  • Exception Notfication の gem を Gemfile から削除します。
  • production 環境は public/assets を使うので、プリコンパイルしておきましょう
    • bundle exec rake RAILS_ENV=production RAILS_GROUPS=assets assets:precompile
    • テスト終ったら public/assets を忘れずに削除しましょう。さもないと理解できないバグで30分くらい浪費しちゃいます。
  • ログインが必要なページのテストする場合は、 -X オプションでログアウトページを除外し、 -I オプションでチェック対象 URL を限定しましょう。

ダウンロードして make すれば使えます。

./skipfish -L -W- -m 4 -o /tmp/skipfish-home -A "user:passowrd"  -I /foo -I /bar -X /foo/logout --auth-form https://localhost/foo/login --auth-user "user" --auth-pass "passowrd" --auth-verify-url http://localhost/foo --auth-user-field "login" --auth-pass-field "password" http://localhost/foo
  • テスト速度重視で辞書を使わない場合 -L -W- を指定します。
  • -m で同時アクセス数を指定できます。
  • -o で指定したディレクトリにテスト結果が出力されます。上記の場合は、ブラウザで /tmp/skipfish/index.html を開くとテスト結果を参照できます。
  • -A は Basic 認証です。
  • —auth で始まるのはフォーム認証です。
  • -I でテスト対象の URL を限定できます。 上記の場合は /foo と /bar 以下のページのみ対象とします。
  • -X でログアウトページを除外します。

ずごい勢いで大量にアクセスを行うので、Web アプリの規模によってはかなりの時間がかかります。 実行中はスペースキーで、進捗表示とアクセス中のページ表示を切り替えられます。

これでセキュリティについても安心です!!

・・・というわけには、いかないですよね。

やはり、ちゃんと勉強して、作る時、コードレビューの時、常にセキュリティを意識しないと。

Rails アプリの JavaScript エラーの通知

2012年12月03日
区分
Rails
報告者:
tahara

こんにちは、tahara です。

Rails のエラー通知に Exception Notifier を使われてたりすると思いますが、 JavaScript のエラー通知はみなさんどうされているのでしょうか?

ちょっと探してみたのですが、これだ! というもの見つからず、 PipelineDeals Dev Blog - PipelineDeals Dev blog - Javascript error reporting for fun and profit を参考にメールで JavaScript のエラー通知を行うようにしてみました。

  1. JavaScript の window.onerror でエラーをつかまえる。
  2. jQuery.post でエラー情報をサーバに送信。
  3. サーバで ActionMailer を使ってエラー情報をメール送信する。

app/assets/javascripts/onerror.js

// Determine if the error occurred before or after document ready
jQuery(function() { window.documentIsReady = true; });

window.onerror = function(errorMsg, file, lineNumber) {
    // report a maximum of n errors
    window.errorCount || (window.errorCount = 0);
    if (window.errorCount <= 3) {
        window.errorCount += 1;
        // post the error with all the information we need.
        jQuery.post('/javascript_error', {
            error: errorMsg,
            file: file,
            location: window.location.href,
            lineNumber: lineNumber,
            documentReady: window.documentIsReady,
            userAgent: navigator.userAgent
        });
    }
};
  • window.onerror で、エラーメッセージ等をサーバに POST します。
  • window.documentIsReady でエラーがおきたのが DOM 構築前か後か、わかるようにしてあります。
  • window.errorCount で1ページで3件をこえてエラーを通知しないようにしています。

app/controllers/javascript_error_controller.rb

# -*- coding: utf-8 -*-
class JavascriptErrorController < ApplicationController
  def index
    # 同じエラー1時間通知しない。
    Rails::cache.fetch("javascript_error #{params[:file]} #{params[:lineNumber]}",
                       :expires_in => 1.hours) do
      Notifier.javascript_error(params).deliver
      true
    end
    head :ok
  end
end
  • ActionMailer でメールを送るだけ。
  • memcached バックエントのキャッシュ使って、同じエラーを1時間の間は通知しないようにしています。

こんな感じで、JavaScript エラーがメールで通知されるようになりました。 もっとエレガントな方法があれば教えてください。

定期的に JavaScript のエラー殲滅デー をもうけて、品質を上げていきたいと思います。

デプロイすると Solr の管理画面が 404 Not Found

2012年11月26日
区分
Solr
報告者:
tahara

こんにちは、tahara です。

Sunspot を導入し Solrいこーよ本番環境で動くようになりました。

Solr 関連のファイル(jar や xml とか)は、Rails アプリのコードと一緒にリポジトリに入れてあります。 Solr は /var/www/outing/current/solr/start.jar で動かしていました。

特に問題なく動いていたのですが、ある日ブラウザで Solr の管理画面を開こうとしたら 404 Not Found になってしまいました。

デプロイには Capistrano を使っています。 デプロイの度に /var/www/outing/current が切り替てっいき、 やがて Solr が起動した時のディレクトリ (/var/www/outing/releases/20121111121212 とか) は削除されます。 そのため Solr の管理画面が 404 Not Found になってしまったようです。

次のように変えました。

  • Solr を /var/www/outing/current の下で動かすのをやめ、/opt/solr で動かす。
    • これがそもそもの間違いでした。
  • デプロイの時に Solr 関連のファイルを /opt/solr にコピーする。
    • after 'deploy:restart' でコピー
  • コピー後に Solr をリロードする。
    • curl で URL をたたくだけ。

config/deploy/production.rb

# sunspot (sunspot) tasks
set :solr_host, 'ec2-123-123-123-123.ap-northeast-1.compute.amazonaws.com'
namespace :sunspot do
  # solr copy & reload
  task :copy_and_reload, :hosts => solr_host do
    run "cp -a #{latest_release}/solr/* /opt/solr"
    run "curl -s 'http://localhost:8983/solr/admin/cores?wt=json&action=RELOAD&core=outing'"
  end

  # solr reindex
  task :reindex, :hosts => solr_host do
    run "cd #{latest_release} && #{rake} RAILS_ENV=#{rails_env} sunspot:solr:reindex "
  end
end
after 'deploy:restart', 'sunspot:copy_and_reload'

URL をたたくだけで、一瞬で設定ファイルをリロードしてくれる Solr に心から感謝しました。

#10000 なんか欲しくないんだからね!

2012年11月16日
区分
開発手法
報告者:
tahara

こんにちは、tahara です。

弊社では Redmine を使ったチケット駆動開発を行っています。

編集(ディレクション) suzuki とエンジニア tahara は東京、デザイナー uemura は大阪にいるため Redmine はなくてはならないツールです。

そんな Redmine ですが、ついにチケットが #10000 を越えました。

#10000

編集 suzuki に #10000 を持っていかれデザイナーの uemura と私(tahara)はとてもがっかりし、 仕事のモチベーションがすっかり落ちてしまいました。

#10000 なんか欲しくないんだからね!

Apdex 1.00

2012年10月29日
区分
全文検索
報告者:
tahara

こんにちは、tahara です。

いこーよの検索を MySQLの全文検索から Solr (Sunspot) にかえてからはじめての New Relic のパフォーマンスレポートが来ました。

Database の負荷が約 35% か約 9% に下がり、 Apdex が 1.00 になりました。

Solr すごいです。

20121029105838

Sunspot (Apache Solr) 導入

2012年10月18日
区分
全文検索
報告者:
tahara

こんにちは、tahara です。

いこーよApache Solr の全文検索を使う Sunspot を導入しました。

これとかこれで MySQL の全文検索を使っていましたが、Sunspot を試してみたら

  • 速い
  • ファセットがものすごく便利
  • Kuromoji という日本語形態素解析機が使える

だったので、

  • Sunspot は Apache Solr を使う。
  • Apache Solr を Java を使う。
  • Java か。。。

というあたりの精神的障壁を克服し、がんばって乗り換えることにしました。

ちょうど Solr 4.0.0 がリリースされたので

  • Solr 4.0.0
  • sunspot 2.0.0.pre.120925
  • sunspot_rails 2.0.0.pre.120925

を使うことにしました。

vi Gemfile

gem "sunspot_rails", "~> 2.0.0.pre.120925"
gem "sunspot", "~> 2.0.0.pre.120925"

bundle install

全文検索の対象となるモデルに searchable を書きます。 vi app/model/facility.rb

  searchable do
    text(:name, :boost => 1.5)
    text(:kana)
    string(:kana)
    text(:region_name, :boost => 3)
    text(:prefecture_name, :boost => 3)
    text(:address, :boost => 2)
    text(:body) do
      "#{pr} #{description} #{tag_list} #{search_keyword} #{features.map(&:name).join(' ')} #{ages.map(&:name).join(' ')}"
    end
    string(:tag, :multiple => true) do
      tag_list
    end
    latlon(:location) { Sunspot::Util::Coordinates.new(lat, lng) }
    integer(:age_ids, :multiple => true)
    integer(:feature_ids, :multiple => true)
    integer(:prefecture_id)
    integer(:region_id)
    integer(:favorites_count)
    boolean(:has_picture) do
      picture_1_file_size.to_i > 0
    end
    boolean(:publish)
    boolean(:coupon_enabled)
    float(:rating) do
      # 口コミがあるものは夜間バッチの評価更新で ratings.created_at <> ratings.updated_at になっている。
      if rating.created_at == rating.updated_at
        0
      else
        rating.overall_rating
      end
    end
    boost { coupon_enabled? ? 3.0 : 1.0 }
    time :created_at
  end

  def self.default_search_scope(solr, params)
    params = HashWithIndifferentAccess.new(params) unless HashWithIndifferentAccess === params
    solr.all_of do
      if params[:publish].blank?
        with(:publish, true)
      else
        with(:publish, params[:publish])
      end
      with(:age_ids, params[:age_ids]) if params[:age_ids].present?
      with(:feature_ids, params[:feature_ids]) if params[:feature_ids].present?
      with(:prefecture_id, params[:prefecture_ids]) if params[:prefecture_ids].present?
      with(:region_id, params[:region_ids]) if params[:region_ids].present?
      if params[:tags].present?
        params[:tags].each do |tag|
          with(:tag, tag) if tag.present?
        end
      end
    end
    solr.with(:location).in_radius(params[:lat], params[:lng], params[:distance] || 100, :bbox => true) if params[:lat].present? && params[:lng].present?
    solr.fulltext(params[:word]) if params[:word].present?
  end

よく検索するパターンがあるので、 self.default_search_scope にそれをまとめています。

検索を行うコントローラ vi app/controllers/facilities_controller.rb

    @facilities = Facility.search(:include => [:ages, :rating, :tags]) do
      Facility::default_search_scope(self, params)
      if params[:format] == 'rss'
        order_by :created_at, :desc
      elsif params[:lat].present?
        order_by_geodist(:location, params[:lat], params[:lng])
      else
        if params[:word].present?
          order_by :score, :desc
        end
        order_by :coupon_enabled, :desc
        order_by :rating, :desc
        order_by :has_picture, :desc
      end
      paginate(:page => params[:page], :per_page => params[:per_page])
      facet :region_id if params[:region_ids].blank? && params[:prefecture_ids].blank?
      facet :prefecture_id if params[:region_ids].present?
    end

facet によって検索結果に加え都道府県ごとのヒット件数をあわせて取得できます。 この機能はとても便利です。

Rails 側は以上で、次に Solr サイドです。

schema.xml は Sunspot のものにちょっと変更を加えます。

Solr 4.0.0 では _version_ フィールドタイプが必要みたいなので

    <field name="_version_" type="long" indexed="true" stored="true"/>
    <fieldType name="long" class="solr.TrieLongField" precisionStep="0" positionIncrementGap="0"/>

また日本語形態素解析機の Kuromoji を使うために text フィールドタイプのアナライザーを変更します。

    <fieldType name="text" class="solr.TextField" omitNorms="false" autoGeneratePhraseQueries="true" positionIncrementGap="100" >
      <analyzer type="index">
        <tokenizer class="solr.JapaneseTokenizerFactory" mode="search" userDictionary="lang/userdict_ja.txt"/>
        <!-- Reduces inflected verbs and adjectives to their base/dictionary forms (辞書形) -->
        <filter class="solr.JapaneseBaseFormFilterFactory"/>
        <!-- synonyms -->
        <filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true"/>
        <!-- Removes tokens with certain part-of-speech tags -->
        <filter class="solr.JapanesePartOfSpeechStopFilterFactory" tags="lang/stoptags_ja.txt" enablePositionIncrements="true"/>
        <!-- Normalizes full-width romaji to half-width and half-width kana to full-width (Unicode NFKC subset) -->
        <filter class="solr.CJKWidthFilterFactory"/>
        <!-- Removes common tokens typically not useful for search, but have a negative effect on ranking -->
        <filter class="solr.StopFilterFactory" ignoreCase="true" words="lang/stopwords_ja.txt" enablePositionIncrements="true" />
        <!-- Normalizes common katakana spelling variations by removing any last long sound character (U+30FC) -->
        <filter class="solr.JapaneseKatakanaStemFilterFactory" minimumLength="4"/>
        <!-- Lower-cases romaji characters -->
        <filter class="solr.LowerCaseFilterFactory"/>
        <!-- カタカナ → ひらがなに -->
        <filter class="org.apache.lucene.analysis.icu.ICUTransformFilterFactory" id="Katakana-Hiragana" />
      </analyzer>
      <analyzer type="query">
        <tokenizer class="solr.JapaneseTokenizerFactory" mode="search" userDictionary="lang/userdict_ja.txt"/>
        <!-- Reduces inflected verbs and adjectives to their base/dictionary forms (辞書形) -->
        <filter class="solr.JapaneseBaseFormFilterFactory"/>
        <!-- synonyms -->
        <filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true"/>
        <!-- Removes tokens with certain part-of-speech tags -->
        <filter class="solr.JapanesePartOfSpeechStopFilterFactory" tags="lang/stoptags_ja.txt" enablePositionIncrements="true"/>
        <!-- Normalizes full-width romaji to half-width and half-width kana to full-width (Unicode NFKC subset) -->
        <filter class="solr.CJKWidthFilterFactory"/>
        <!-- Removes common tokens typically not useful for search, but have a negative effect on ranking -->
        <filter class="solr.StopFilterFactory" ignoreCase="true" words="lang/stopwords_ja.txt" enablePositionIncrements="true" />
        <!-- Normalizes common katakana spelling variations by removing any last long sound character (U+30FC) -->
        <filter class="solr.JapaneseKatakanaStemFilterFactory" minimumLength="4"/>
        <!-- Lower-cases romaji characters -->
        <filter class="solr.LowerCaseFilterFactory"/>
        <!-- カタカナ → ひらがなに -->
        <filter class="org.apache.lucene.analysis.icu.ICUTransformFilterFactory" id="Katakana-Hiragana" />
      </analyzer>
    </fieldType>

Solr を起動する init スクリプトも必要ですね。

#! /bin/sh
### BEGIN INIT INFO
# Provides:          solr
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Apache Solr
# Description:       Apache Solr
#                    sudo ln -s /var/www/outing/current/solr/etc/init.d/init.sh solr
#                    sudo update-rc.d solr defaults
### END INIT INFO

# Author: antindi <dev@actindi.net>
#
# Please remove the "Author" lines above and replace them
# with your own name if you copy and modify this script.

# Do NOT "set -e"

# PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="Solr"
NAME=solr
PROCESS_NAME=java
SOLR_HOME=/var/www/outing/current/solr
DAEMON=/usr/bin/java
DAEMON_ARGS="-Xmx1024m -Djava.util.logging.config.file=etc/logging.properties -jar start.jar"
PIDFILE=/var/run/$NAME/$NAME.pid
LOG_DIR=/var/log/$NAME
BASE_DIR=/var/lib/$NAME
DATA_DIR=$BASE_DIR/data
SCRIPTNAME=/etc/init.d/$NAME
SOLR_USER=deployer

# Exit if the package is not installed
[ -x "$DAEMON" ] || exit 0

# Read configuration variable file if it is present
[ -r /etc/default/$NAME ] && . /etc/default/$NAME

# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh

# Define LSB log_* functions.
# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
# and status_of_proc is working.
. /lib/lsb/init-functions

#
# Function that starts the daemon/service
#
do_start()
{
        mkdir `dirname $PIDFILE` > /dev/null 2>&1 || true
        chown $SOLR_USER `dirname $PIDFILE`
        mkdir $LOG_DIR > /dev/null 2>&1 || true
        chown $SOLR_USER $LOG_DIR
        mkdir -p $DATA_DIR > /dev/null 2>&1 || true
        chown $SOLR_USER $DATA_DIR
        # Return
        #   0 if daemon has been started
        #   1 if daemon was already running
        #   2 if daemon could not be started
        start-stop-daemon -b -m -c $SOLR_USER -d $SOLR_HOME --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
                || return 1
        start-stop-daemon -b -m -c $SOLR_USER -d $SOLR_HOME --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \
                $DAEMON_ARGS \
                || return 2
        # Add code here, if necessary, that waits for the process to be ready
        # to handle requests from services started subsequently which depend
        # on this one.  As a last resort, sleep for some time.
}

#
# Function that stops the daemon/service
#
do_stop()
{
        # Return
        #   0 if daemon has been stopped
        #   1 if daemon was already stopped
        #   2 if daemon could not be stopped
        #   other if a failure occurred
        start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $PROCESS_NAME
        RETVAL="$?"
        [ "$RETVAL" = 2 ] && return 2
        # Wait for children to finish too if this is a daemon that forks
        # and if the daemon is only ever run from this initscript.
        # If the above conditions are not satisfied then add some other code
        # that waits for the process to drop all resources that could be
        # needed by services started subsequently.  A last resort is to
        # sleep for some time.
        #start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
        #[ "$?" = 2 ] && return 2
        # Many daemons don't delete their pidfiles when they exit.
        rm -f $PIDFILE
        return "$RETVAL"
}

#
# Function that sends a SIGHUP to the daemon/service
#
do_reload() {
        #
        # If the daemon can reload its configuration without
        # restarting (for example, when it is sent a SIGHUP),
        # then implement that here.
        #
        start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $PROCESS_NAME
        return 0
}

case "$1" in
  start)
        [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
        do_start
        case "$?" in
                0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
                2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
        esac
        ;;
  stop)
        [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
        do_stop
        case "$?" in
                0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
                2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
        esac
        ;;
  status)
        status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
        ;;
  #reload|force-reload)
        #
        # If do_reload() is not implemented then leave this commented out
        # and leave 'force-reload' as an alias for 'restart'.
        #
        #log_daemon_msg "Reloading $DESC" "$NAME"
        #do_reload
        #log_end_msg $?
        #;;
  restart|force-reload)
        #
        # If the "reload" option is implemented then remove the
        # 'force-reload' alias
        #
        log_daemon_msg "Restarting $DESC" "$NAME"
        do_stop
        case "$?" in
          0|1)
                do_start
                case "$?" in
                        0) log_end_msg 0 ;;
                        1) log_end_msg 1 ;; # Old process is still running
                        *) log_end_msg 1 ;; # Failed to start
                esac
                ;;
          *)
                # Failed to stop
                log_end_msg 1
                ;;
        esac
        ;;
  *)
        #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
        echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
        exit 3
        ;;
esac

:
sudo update-rc.d solr defaults
sudo service solr

今朝本番投入しましが、ちゃんと動いてくれているようです。よかった。

弊社ではエンジニア募集しています。お気軽にお問い合わせください。

ネットワーク帯域を無駄遣いして、すみませんでした。

2012年10月01日
区分
CloudFront
報告者:
tahara

こんにちは、tahara です。

Google さんから来た AdSense ニュースレターに PageSpeed Insights を使ってサイトのパフォーマンスを向上させなさい、と書いてあったので、PageSpeed Insights 使ってみました。

すると、css と js が圧縮されなていないことが判明! えっ! Asset Pipeline にして Amazon CloudFront を導入したはずなのに、何か起っている!?

いえ、Nginx の設定が足りていないだけでした。

次のように gzip_proxied と gzip_vary の設定を追加しました。

location ~ ^/assets/ {
    # to serve pre-gzipped version
    gzip_static on;
    # for CloudFront
    gzip_proxied any;
    gzip_vary on;
    expires 1y;
    add_header Cache-Control public;
    add_header ETag "";
    break;
}

gzip_http_version 1.0; が必要との情報もいくつか見かけましたが、 いまの CloudFront は HTTP/1.1 でリクエストしてくるので設定しなくても大丈夫のようです。

以上、Amazon CloudFront で Nginx の gzip_static が効いてない件でした。

あいかわらず、弊社ではエンジニア募集しています。お気軽にお問い合わせください。

Jenkins から Common Lisp で Skype へ通知

2012年09月24日
区分
CommonLisp
報告者:
tahara

こんにちは、tahara です。

いいかげん Jenkins くら導入しようよ、ということで導入しました(ステージングへの cap が Asset Pipeline の precompile で時間がかかるようになって、手で cap するのがめんどうになったのが本当の理由です)。

Jenkins のセットアップは色々なとこに情報があったのですんなりいきました。 でも、Jenkins から Skype への通知でてこずりました。 Skype Plugin があるので、これを使えば問題ないよね、と思っていたのですが、一日格闘しても動かせず。

Skype Plugin をあきらめ Common Lisp 経由で Skype を使うことにしたら、すんなりできました。 Jenkins → ビルドシェル → Common Lisp → D-Bus → Skype という流れになります。

まず Linux上で動くSkype用のbotを作る方法 - muddy brown thang を参照して Skype を Xvfb で動くようにします。

sudo vi /etc/init.d/skype

#!/bin/bash
#
# Init file for daemonized Skype service
#
### BEGIN INIT INFO
# Provides: skype
# Required-Start:    $local_fs $remote_fs $network $syslog
# Required-Stop:     $local_fs $remote_fs $network $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# X-Interactive:     false
# Short-Description: starts the skype on Xvfb
# Description:       starts skype using start-stop-daemon
### END INIT INFO

# sudo update-rc.d skype defaults

DAEMON=/usr/bin/skype
DAEMON_USER=jenkins
PIDFILE=/var/run/skype.pid
XSERVERNUM=20
XAUTHFILE=/var/run/skype/Xauthority
LOGFILE=/var/log/skype/error.log
DBPATH=/var/lib/jenkins/.Skype
XAUTHPROTO=.
USERNAME=your-skype-account
PASSWORD=your-skype-password
NAME=skype

test -x $DAEMON || exit 0

set -e

. /lib/lsb/init-functions

RETVAL=0

dircheck() {
    if [ ! -d `dirname "$1"` ]; then
        echo "`dirname \"$1\"` does not exist"
        return 1
    else
        return 0
    fi
}

start() {
    echo -n "Starting $DESC: "
    if ! dircheck "$LOGFILE" || ! dircheck "$XAUTHFILE"; then
        echo -n "ng. $LOGFILE or $XAUTHFILE"
        echo
        RETVAL=1
        return
    fi
    MCOOKIE=`mcookie` && \
        sudo -u "$DAEMON_USER" env XAUTHORITY=$XAUTHFILE sh -c "xauth add \":$XSERVERNUM\" \"$XAUTHPROTO\" \"$MCOOKIE\" >> \"$LOGFILE\" 2>&1" && \
        sudo -u "$DAEMON_USER" env XAUTHORITY=$XAUTHFILE sh -c "Xvfb :$XSERVERNUM -screen 0 800x600x8 -nolisten tcp >> \"$LOGFILE\" 2>&1 & echo \$!" >"$PIDFILE" &&
    sleep 3 && \
        (sudo -u "$DAEMON_USER" env DISPLAY=:$XSERVERNUM XAUTHORITY=$XAUTHFILE SKYPE="$DAEMON" sh -c "echo \"$USERNAME $PASSWORD\" | nohup \"$DAEMON\" --dbpath=\"$DBPATH\" --pipelogin &") >> "$LOGFILE" 2>&1 && \
        (echo -n "ok" && [ -d /var/lock/subsys ] && touch /var/lock/subsys/skype || true) \
        || (RETVAL=$?; kill -TERM `cat $PIDFILE`; echo -n "ng")
}

stop() {
    echo -n "Stopping" "skype"
    if [ -e "$PIDFILE" ]; then
        kill -TERM `cat $PIDFILE` && \
            rm -f $PIDFILE && \
            if [ -d /var/lock/subsys ]; then rm -f /var/lock/subsys/skype; fi && \
            echo -n "ok" || echo -n "ng"
    else
        echo "ng. maybe not running."
        RETVAL=1
    fi
}

usage() {
    echo "Usage: $NAME {start|stop|restart}"
}

case $1 in
    start)
        start
        ;;
    stop)
        stop
        ;;
    restart)
        stop
        start
        ;;
    *)
        usage
        RETVAL=255
        ;;
esac

exit $RETVAL
$ sudo chmod +x /etc/init.d/skype
$ sudo mkdir /var/log/skype
$ sudo chown jenkins:jenkins /var/log/skype
$ sudo mkdir /var/run/skype
$ sudo chown jenkins:jenkins /var/run/skype
$ sudo update-rc.d skype defaults
$ sudo service skype start

Common Lisp と Quicklisp のセットアップ。

$ sudo apt-get install sbcl
$ curl -O http://beta.quicklisp.org/quicklisp.lisp
$ sbcl --load quicklisp.lisp
* (quicklisp-quickstart:install)
* (ql:add-to-init-file)

Common Lisp から Skype を使うコードです。

vi /var/lib/jenkins/skype/skype.lisp

(load "~/quicklisp/setup.lisp")

(let* ((*standard-output* (make-broadcast-stream))
       (*error-output* *standard-output*))
  (ql:quickload :dbus))

(defpackage :skype
  (:use :cl :dbus))

(in-package :skype)

(defparameter *chat-id* "#xxxxxx/$xxxxxx;3xxxx9999xxxxx" "Skype チャットの ID")

(defun message (chat-id message)
  (with-open-bus (bus (session-server-addresses))
    (with-introspected-object (skype
                               bus
                               "/com/Skype"
                               "com.Skype.API")
      (flet ((skype (command)
               (print (skype "com.Skype.API" "Invoke" command))))
        (skype "NAME FromCommonLisp")
        (skype "PROTOCOL 8")
        (skype (format nil "CHATMESSAGE ~a ~a" chat-id message))))))

(let ((message (second sb-ext:*posix-argv*)))
  (message *chat-id* message))

Common Lisp からは D-Bus 経由のため、 環境変数 DBUS_SESSION_BUS_ADDRESS の設定が必要になります(cron と gconftool-2 について - AOTRの日記)。

/etc/init.d/skype の XSERVERNUM に 20 を指定しているので、次のようにすればよさそうです。

$ source `ls ~/.dbus/session-bus/*-20`
$ export DBUS_SESSION_BUS_ADDRESS

これで sbcl すれば skype でメッセージが送れるはずです。

$ sbcl --script /var/lib/jenkins/skype/skype.lisp 'テストメッセージ'

あ、Skype を Xvfb で動かす前に一度普通に動かして Common Lisp からパブリップ API を使えるよう許可しておく必要があります。 次のような感じです。

$ ssh -X jenkins@example.com
$ skype &
$ echo $DISPLAY   # DISPLAY 番号を確認 localhost:10.0
$ source `ls ~/.dbus/session-bus/*-10`
$ export DBUS_SESSION_BUS_ADDRESS
$ sbcl --noinform --disable-debugger --load /var/lib/jenkins/skype/skype.lisp 'テストメッセージ'

最後に Jenkins のビルドスクリプトです。 cap をして Skype で通知します。 git のコミットメッセージなども付けるようにします。

ビルドスクリプトはリポジトリに入れておくのがベストプラクテスとのことですので vi $YOURE_RAILS_WORKSPACE/config/jenkins/build.sh

#!/bin/bash

# cap
rvm_path=/home/deployer/.rvm /home/deployer/.rvm/bin/rvm-shell 'ruby-1.9.3@iko-yo-rails3' -c 'cap staging deploy:migrations'

# 20 は /etc/init.d/skype の XSERVERNUM=20
source `ls ~/.dbus/session-bus/*-20`
export DBUS_SESSION_BUS_ADDRESS
sbcl --script /var/lib/jenkins/skype/skype.lisp "(ninja) Capistrano: いこーよを 確認(ステージング) 環境 http://outing.actindi.net/ にデプロイしました (h)`git log --pretty='%n%s%n%b%n%an' HEAD...HEAD~`"

Jenkins の "ビルド" > "シェルの実行" > "シェルスクリプト" に config/jenkins/build.sh を指定で、できあがり。

最後に、弊社ではエンジニア募集しています。お気軽にお問い合わせください。

Asset Pipeline と Amazon CloudFront を導入

2012年09月18日
区分
Rails
報告者:
tahara

こんにちは、tahara です。

いまだに Asset Pipeline を使っていなかったいこーよですが、Amazon CloudFront とともに、ようやく導入しました。

温泉街の旅館のごとくページを増やしていたため、複数のレイアウトファイルが乱立しており、かなり大変な目にあいましたw

ポイントだけまとめておきます。

javascript_include_tag, stylesheet_link_tag している js, css は 全て config/environments/production.rb に追加しておきます。 さもないと ActionView::Template::Error で容赦なく本番が落ちます。 ただし application.js と application.css はデフォなので追加しなくても大丈夫。

config.assets.precompile += %w( admin.js admin.css provdire.js provider.css ... )

static gzip を使うために nginx をコンパイルしなおします。 ついでに最新にしちゃいます。 最近の nginx はデフォルトで static gzip モジュールが有効になるようですね。

# ソースを用意しておく
cd /tmp
wget http://nginx.org/download/nginx-1.2.3.tar.gz
tar xvf nginx-1.2.3.tar.gz

# パッセンジャーごと新しくする。
gem install passenger
rvmsudo passenger-install-nginx-module
# 2. No: I want to customize my Nginx installation. (for advanced users) を選択。
# Where is your Nginx source code located? で /tmp/nginx-1.2.3
# Extra arguments to pass to configure script: で
# --with-http_stub_status_module --with-http_flv_module を追加

# vi nginx.conf で passenger_ruby を新しいパスにする。
passenger_ruby 新しいパス;

location ~ ^/assets/ {
    gzip_static on;
    # for CloudFront
    gzip_proxied any;
    gzip_vary on;
    expires 1y;
    add_header Cache-Control public;
    add_header ETag "";
    break;
}

* gzip_proxied と gzip_vary を追加 2012-10-01

あとは画像ファイルの参照を erb 内では asset_path('foo.jpg') に、 scss 内では image-url("foo.jpg") にひたすら書き換えれば、 Asset Pipeline の導入完了です。

次に Amazon CloudFront です。

CloudFront の設定は AWS のコンソールから簡単にできます。

Rails 側は config/environments/production.rb で config.action_controller.asset_host を設定します。 production.rb はステージングと本番で同じものを使っているので、 config/deploy/production.rb で sed しちゃいます。

namespace :deploy do
  desc 'setup production env'
  task :setup_production_env do
    run "sed -i -e 's/.*config.action_controller.asset_host.*/config.action_controller.asset_host = \"\\/\\/d2goguvysdoarq.cloudfront.net\"/' #{release_path}/config/environments/production.rb"
  end
  before 'deploy:finalize_update', 'deploy:setup_production_env'
end

これだけで、CloudFront の導入ができました。 これは簡単だった。

次のように CloudFront がさばいてくれる分、リクエスト数が減りました。 20120918172018

最後に、エンジニア募集しています。

Rails のログを寡黙に

2012年09月04日
区分
Rails
報告者:
tahara

こんにちは、tahara です。

Rails のプロダクションログですが

Started GET "/" for 127.0.0.1 at 2011-12-11 13:32:11 +0900
  Processing by TopController#index as HTML
Rendered shared/_login_box.html.erb (1.2ms)
Rendered top/_header.html.erb (2.3ms)
Rendered shared/_oneline_finder.html.erb (129.7ms)
Rendered shared/_teiban_spot_working.html.erb (0.3ms)
Rendered inline template (0.4ms)
Rendered top/_top_pr.html.erb (0.2ms)
Rendered top/_experiences.html.erb (310.6ms)
Rendered top/_ranking.html.erb (38.5ms)
Rendered inline template (0.3ms)
Rendered inline template (0.3ms)
Rendered shared/ad/pc/_osusume_jouhou.html.erb (9.3ms)

Rendered で始まる行が多過ぎです。 これをちょっと黙らせたいと思います。

http://stackoverflow.com/questions/6377190/modify-log-format-and-content-from-default-actioncontroller-logsubscriber-in-rai を参考に

config/initializers/log_unsubscribe.rb

if Rails.env.production?
  # unsubscribe ActionView::LogSubscriber
  ['render_template', 'render_partial', 'render_collection'].each do |event|
    ActiveSupport::Notifications.notifier.listeners_for("#{event}.action_view").each do |x|
      ActiveSupport::Notifications.unsubscribe(x)
    end
  end

  # unsubscribe xxx_fragment
  %w(write_fragment read_fragment exist_fragment? expire_fragment expire_page write_page).each do |event|
    ActiveSupport::Notifications.notifier.listeners_for("#{event}.action_controller").each do |x|
      ActiveSupport::Notifications.unsubscribe(x)
    end
  end
end

これでプロダクション環境では、レンダーとフラグメントのログが出力されなくなりました。

最後に、エンジニア募集中です。

btrfs のスナップショットを使った差分バックアップ

2012年08月13日
区分
バックアップ
報告者:
tahara

こんにちは、tahara です。

おかげさまで、いこーよ 月間一千万ページビューこえました。ありがとうございます。 AWS のオートスケールの出番ができて嬉しいです。

今回は btrfs のスナップショットを使ったバックアップについて書いてみたいと思います。

以前は rsync の —link-dest オプションを使って、変更のないファイルは前日のバックアップからハードリンクをはって、差分バックアップを行っていました。 この方法だと前日のバックアップが失敗していた場合、フルバックアップになってしまい、あっとういまにディスクを使いきってしまいます。

その対策として btrfs のスナップショットを使った差分バックアップに切り替えました。

今回は既にバップアップディスクが ext4 になっていたので、それを btrfs に変換しました。

こんな感じのバックアップディスクになっていました。

/dev/sde1 on /backup type ext4 (rw,commit=0)

btrfs に変換します。

sudo apt-get install btrfs-tools
sudo umount /backup/
sudo btrfs-convert /dev/sde1

これで btrfs に変換できました。

次にスナップショットをとるためにサブボリュームを作ります。

sudo mkdir /btrfs
sudo mount -t btrfs -o compress /dev/sde1 /btrfs
sudo btrfs subvolume create /btrfs/backup

確認してみます。

sudo btrfs subvolume list /btrfs
ID 256 top level 5 path ext2_saved
ID 257 top level 5 path backup

ext2_saved の方は ext4 から btrfs に変換した時に作られたバップアップファイルです。 きちんと変換できていればいらないので消してしまいます。

sudo btrfs subvolume delete /btrfs/ext2_saved/

sudo blkid で UUID を確認し sudo vi /etc/fstab を書き換えます。

UUID=b20c3f2a-37e0-47ac-96df-3051d30f917a /btrfs btrfs compress 0 0
UUID=b20c3f2a-37e0-47ac-96df-3051d30f917a /backup btrfs subvol=backup,compress 0 0

パックアップスクリプトはこんな感じのシンプルなものになります。 rsync でファイルをバックアップして、btrfs のスナップショットをとります。 /backup の下は常に最新のバックアップで、/btrfs/backup-snapshot の下に日毎にバックアップのスナップショットが作られます。

#!/bin/sh

rsync -azv --delete foo@example.com:/var/www/app /backup
rsync -azv --delete bar@hoge.jp:/var/www/hoge-ap /backup
/sbin/btrfs subvolume snapshot /backup /btrfs/backup-snapshot/`date +%Y%m%d`

シンプルでディスク容量を節約できるバックアップになりました。

最後に、弊社ではエンジニアを募集しています。

WordPress の引越し(ドメイン変更)

2012年07月17日
区分
WordPress
報告者:
tahara

こんにちは tahara です。

WordPress ってどうして http:// からのフルパスで URL を DB に格納しちゃうんでしょうか? 開発環境で構築した WordPress サイトを本番環境にリリースするとき泣きたくなります。

プラグインによっては s:81:\"http://...." のような形式の文字列長付きで保持している場合もあるため、sed もできません。

泣きながら検索してみれば、ちゃんと Moving WordPress « WordPress Codex に書いてありました。

Search and Replace for WordPress Databases Script」を使えと。

s:81:\"http://...." のような文字列長付きのものも、日本語まじりのものも綺麗にリプレースしてくれます。 なんてすばらしい。

使い方は簡単。

  1. WordPress Serialized PHP Search Replace Tool | Interconnect IT - WordPress Consultants, Web Development and Web Design から searchreplacedb21.zip をダウンロードする。
  2. searchreplacedb21.zip を解凍し searchreplacedb2.php を WordPerss サイトのドキュメントルートに置く。
  3. http://www.example.com/searchreplacedb2.php にアクセスする(www.example.com は環境に合わせてください)。
  4. あとは画面にしたがって、DB 上の任意の文字列をリプレースすることができます。 ドメインが変わる場合等は「Leave GUID column unchanged? 」にはチェックを付けない方がいいです。 付けるとリプレースされないものがたくさん残ってしまいます。
  5. リプレース作業が終ったら searchreplacedb2.php を消す。 消さないとだれでもあなたの DB を書き換えることができてしまいます。

最後に、弊社ではシステムエンジニア、プログラマ、インフラエンジニアなどを募集しています。 おきがるにお問い合わせください。

Ruby で Google Analytics API その2

2012年06月18日
区分
Google
報告者:
tahara

こんにちは、tahara です。

しばらくぼんやりしていたら Google API がすごい勢いで変っていて全然ついていけていませんでした。 Google APIs Console なんてものができていたんですね。

というわけで、今回は最近のやり方で Analytics の API をたたいてみたいと思います。

Ruby で Google Analytics API」では Garb を使いましたが、今回は google-api-ruby-client を使います。

まずは Google APIs Console で新しいプロジェクを作り、Analytics API を ON にします。 引き続き Google APIs Console の API Access で Client ID for installed applications を作ります。 これで Client ID と Client secret が手に入ります。

Gemfile に gem 'google-api-client' を追加して bundle install を実行します。 OAuth のアクセスキーはコマンドラインを使うと簡単に取得できます。

bundle exec google-api oauth-2-login --scope='https://www.googleapis.com/auth/analytics.readonly' --client-id='Google APIs Console で取得した Client ID' --client-secret='Google APIs Console で取得した Client secret'

これを実行するとブラウザが起動していアクセス許可を求められます。 アクセスを許可すると次のような ~/.google-api.yaml というファイルにアクセスキー等が書き出されます。

---
mechanism: oauth_2
scope: https://www.googleapis.com/auth/analytics.readonly
client_id: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
client_secret: xxxxxxxxxxxxxxxxxxxxxxxx
access_token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
refresh_token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

これで必要なものがそろったので API をたたきます。

# -*- coding: utf-8 -*-
require 'google/api_client'

module Actindi
  module Google
    class Analytics

      PROFILE_DEFAULT        = 'ga:99999999' # プロファイル ID

      attr_accessor :profile

      def initialize
        @profile = PROFILE_DEFAULT
        @max_results = 10000

        @client = ::Google::APIClient.new(:authorization => :oauth_2)
        @client.authorization.scope = 'https://www.googleapis.com/auth/analytics.readonly'
        # Google APIs Console で取得した Client ID
        @client.authorization.client_id = 'xxxxxxxxxxxxxxxxxxxxxxx'
        # Google APIs Console で取得した Client secret
        @client.authorization.client_secret = 'xxxxxxxxxxxxxxxxxxx'
        # ~/.google-api.yaml の access_token
        @client.authorization.access_token = 'xxxxxxxxxxxxxxxxxxxx'
        # ~/.google-api.yaml の refresh_token
        @client.authorization.refresh_token = 'xxxxxxxxxxxxxxxxxxx'
        @client.authorization.fetch_access_token!

        @analytics = @client.discovered_api('analytics', 'v3')
      end

      # x = Actindi::Google::Analytics.new.page_view('^/facilities/[0-9]+$', 1.month.ago, Date.today)
      def page_view(path, from, to, start_index = 1)
        result = @client.execute(:api_method => @analytics.data.ga.get,
                                 :parameters => {
                                   'ids' => @profile,
                                   'start-date' => from.to_date.strftime('%Y-%m-%d'),
                                   'end-date' => to.to_date.strftime('%Y-%m-%d'),
                                   'metrics' => 'ga:pageviews',
                                   'dimensions' => 'ga:pagePath',
                                   'filters' => "ga:pagePath=~#{path}",
                                   'sort' => '-ga:pageviews',
                                   'start-index' => start_index,
                                   'max-results' => @max_results
                                 })
        if result.status != 200
          raise result.response.body.to_s
        end
        hash = result.data.rows.inject(Hash.new(0)) do |acc, x|
          x[0] =~ /([0-9]+)/
          acc[$1.to_i] = x[1].to_i
          acc
        end
        if result.data.rows.size >= @max_results
          hash.merge!(page_view(path, from, to, start_index + @max_results))
        end
        hash
      end
    end
  end
end

いこーよ の過去1か月間のスポット詳細のアクセス数を取得します。

x = Actindi::Google::Analytics.new.page_view('^/facilities/[0-9]+$', 1.month.ago, Date.today)

@client.execute に渡す parameters にいては Google Analytics Query Explorer 2 を使って確認するといいと思います。

最後に、弊社ではシステムエンジニア、プログラマ、インフラエンジニアなどを募集しています。 おきがるにお問い合わせください。

ブール全文検索を併用してスコア調整

2012年06月04日
区分
全文検索
報告者:
tahara

こんにちは、tahara です。 いこーよに導入した全文検索ですが、問題があり改善を行いました。

「品川 水族館」で検索を行った時、「品川」という語だけ多数含んでいるスポッ トが「品川」と「水族館」の両方を含んでいるスポットより上位に表示されて いました。 これじゃ水族館におでかけできない!

MySQL の全文検索のスコア付けを調整できるか調べてみたのですが、それらし いパラメータは見付つかりませんでした。

さてどうしたものかと悩んだ結果、order by でブール全文検索を使ってがんば ることにしました。

ブール全文検索を使えば、「品川」と「水族館」両方含んでいるものだけ見付 けることができます。

次のように自然言語検索とブール全文検索を併用してスコアを調整しました。

select ... from ...
where MATCH (content) AGAINST ('品川 水族館')
order by MATCH (content) AGAINST ('品川 水族館') *
         (case when MATCH (content) AGAINST ('+品川 +水族館' IN BOOLEAN MODE) then 2.0 else 1.0 end)

「品川」と「水族館」の両方を含んでいるスポットは、片方しか含んでいない スポットの 2 倍のスコアになります。 これでちゃんと水族館におでかけできます。

問題点はブール全文検索を行う分、少しだけ遅くなることです。 もっといい方法はないでしょうか?

最後に、弊社ではシステムエンジニア、プログラマ、インフラエンジニアなどを募集しています。 おきがるにお問い合わせください。

全件検索から全文検索へ (Rails で MySQL の全文検索)

2012年05月15日
区分
Rails
報告者:
tahara

こんにちは、tahara です。 ようやくいこーよを like '%foo%' の(SQL 力づく)全件検索から、MySQL の全文検索に変更しました。

最初は mroonga を使おうかと思ったのですが、結局は MeCab を使って MySQL の全文検索をそのまま使うことにしました。

MySQL の全文検索は MyISAM じゃないと動かないので ENGINE = MyISAM で検索用のテーブルを作成します。

class CreateFacilityIndices < ActiveRecord::Migration
  def up
    execute <<SQL
create table facility_indices (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `facility_id` int(11) NOT NULL,
  `content` text NOT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  FULLTEXT INDEX (content)
) ENGINE = MyISAM DEFAULT CHARSET utf8;
SQL
    Facility.find_each {|f|f.update_facility_index()}
  end

  def down
    execute "drop table facility_indices"
  end
end

検索したいテーブル(モデル)の after_save で上記のテーブ(facilities_indices)の登録、更新を行うようにします。 また、全文検索用のスコープも作成します。 order by で floor しているのは、同じようなスコアの場合は他の条件でソートしたいめです。

class Facility < ActiveRecord::Base
  after_save :update_facility_index
  has_one(:facility_index, :dependent => :destroy)

  def update_facility_index
    index = self.facility_index || self.build_facility_index
    index.update_from_facility
  end

  scope(:scoped_by_word,
        lambda { |text|
          joins(:facility_index).
          where("MATCH (facility_indices.content) AGAINST (?)", FacilityIndex.normalize(text)).
          order(sanitize_sql(["floor(MATCH (facility_indices.content) AGAINST (?)) desc", FacilityIndex.normalize(text)]))
        })
end

FacilityIndex では次のように、全文検索のインデックスを作ります。

  1. 検索対象の検索対象フィールドを一つも文字列にコンカチします。
  2. 地域と都道府県は全文検索結果スコアへの影響を大きくするために、単純に数回繰り返します。
  3. nkf で文字種の正規化を行います。
  4. MeCab で分解します。
  5. 助詞、助動詞は除きます。
  6. 原形があれば、原形を使うようにします。
  7. 「ある」などは検索上無意味なのでストップワードしとて除外します。
  8. MeCab 前にやっちゃうと、うまくいかないカタカナひらがな変換を行います。
  9. 以上のように正規化したものを FULLTEXT INDEX の付いたカラムに登録します。

検索時にはユーザの入力した検索ワードを同様に FacilityIndex::normalize で正規化して MATCH (...) AGAINST で検索します。 上記の Facility::scoped_by_word です。

# -*- coding: utf-8 -*-
require 'nkf'
require 'MeCab'

# 一文字からも検索できるように
# sudo vi /etc/mysql/my.cfg
# [mysqld]
# ft_min_word_len = 1
class FacilityIndex < ActiveRecord::Base
  belongs_to :facility

  # Facility.find_each {|f|f.update_facility_index()}
  def update_from_facility
    facility = self.facility
    # 都府県は除く
    prefecture_name = facility.prefecture_name.try(:gsub, /(都|府|県)$/, '')
    # region と prefecture は加重する。
    self.content = FacilityIndex.normalize("#{facility.name} #{facility.kana} #{facility.pr} #{facility.description} #{([facility.prefecture.region.name]*7).join(' ')} #{([prefecture_name]*5).join(' ')}#{facility.address} #{facility.search_keyword} #{facility.features.map(&:name).join(' ')} #{facility.tag_list.join(' ')}")
    self.save!
    self
  end

  class << self

    def normalize(text)
      # UTF8
      # 半角カタカナ => 全角カタカナ
      # MIMEはデコードしない
      # 全角アルファベット、全角スペースを半角に
      #
      # ひらがな、カタカナは変換すると MeCab トークナイザが
      # 正しく動かないのでそのままにする。
      text = NKF.nkf('-WwXm0Z1', text).gsub(/[\r、。・()「」【】!?]/, '')
      mecab = MeCab::Tagger.new
      node = mecab.parseToNode(text)
      s = ''
      while node
        features = node.feature.force_encoding('UTF-8').split(/,/)
        unless %w[助詞 助動詞].include?(features[0])
          # 原形を使う。
          word = features[6]
          # 原形がなければ、表層形を使う。
          word = node.surface.force_encoding('UTF-8') if word == '*'
          unless stop_word?(word)
            s += word + ' '
          end
        end
        node = node.next
      end
      # Mecab の後にカタカナをひらがなに変換する
      NKF.nkf('-Wwh1', s)
    end

    def stop_word?(word)
      # 下の select_stop_word でひっかかったワード
      return true if ["", "ある", "市", "OK", "いる", "施設", "する", "-", "お", "れる", "できる"].include?(word)
      false
    end

    # 全件の 50% にでてくるものは MySQL の全文検索でひっかからないので、
    # そのようなワードをストップワードにする。
    def select_stop_word()
      hash = Hash.new(0)
      FacilityIndex.find_each do |x|
        x.content.split(/ /).uniq.each do |word|
          hash[word] += 1
        end
      end
      half = FacilityIndex.count / 2
      stop_words = hash.select do |k, v|
        v > half
      end
      puts stop_words
      stop_words.map {|k, v| k}
    end
  end
end

これで、全文検索ができました。 ある程度あいまいな検索ができるようになった上に、検索も速くなりました。

最後に、弊社ではシステムエンジニア、プログラマ、インフラエンジニアなどを募集しています。 おきがるにお問い合わせください。

Rails での MySQL マスタースレーブ構成

2012年05月07日
区分
Rails
報告者:
tahara

いこーよ の GW の負荷対策として MySQL のレプリケーションを使いマスタースレーブ構成にしてみました。

一番悩んだのがマスタースレーブ構成のためにどのライブリを使うか。 次のような理由から seamless_database_poolフォークして使うことにしました。

  • アプリ起動時にスレーブが落ちていても動く。 ただし、この場合は途中からスレーブが動きだしてもスレーブにはつながらない。
  • アプリ起動中にスレーブが落ちても動く。
  • 途中でスレーブが復帰すればまたスレーブにつながるようになる。
  • マスターをスレーブに含めることも、含めないこともできる。
  • マスター、および各スレーブの接続ウエイト指定ができる。

フォークする理由は次のとおりです。

  • geokit-rails が UnsupportedAdapter 例外を投げることの対策。
  • 毎回セッション使わないようにする。 Rails 3 Slave Databases: Compare Octopus to SDP を参照。
  • デバッグ時にマスターにつないでいるのか、スレーブにつないでいるのかわからないので、接続先をログ出力する。

以下、セットアップ手順を書きていきます。

マスターとスレーブ両方にレプリケーション用のアカウントを作成します。

mysql -uroot

GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO repl@'%' IDENTIFIED BY 'password';

AWS のセキュリティグループがあるので repl@'%' でよし。

マスターの my.cnf を編集します。

sudo vi /etc/mysql/my.cnf

[mysqld]
server-id              = 10
log_bin                = /var/log/mysql/mysql-bin.log

で MySQL を再起動。 sudo service mysql restart

スレーブの my.cnf を編集します。

sudo vi /etc/mysql/my.cnf

server-id               = 11
log_bin                 = /var/log/mysql/mysql-bin.log
relay_log               = /var/log/mysql/mysql-relay-bin.log
log_slave_updates       = 1
read_only               = 1
slave_load_tmpdir       = /var/tmp

slave_load_tmpdir はマシンを再起動してもファイルが消えないディレクトリにしておかないと問題があるようです。

で MySQL を再起動。 sudo service mysql restart

スレーブで次のようにしてマスターを指定します。

mysql -uroot

CHANGE MASTER TO MASTER_HOST='ec2-123-123-123-123.ap-northeast-1.compute.amazonaws.com', MASTER_USER='repl', MASTER_PASSWORD='password';
show slave status\G

マスターからデータをスレーブに投入します。

mysqldump -uroot --single-transaction --all-databases --master-data=1 | ssh -C deployer@ec2-54-248-109-31.ap-northeast-1.compute.amazonaws.com mysql -uroot

スレーブでレプリケーションを開始します。

mysql -uroot

start slave;
show slave status\G

Rails で seamless_database_pool を使うようにします。

application_controller.rb でデフォルトでマスターを使うようにします。

class ApplicationController < ActionController::Base
  include SeamlessDatabasePool::ControllerFilter
  use_database_pool :all => :master # 個別にスレーブを使うように指定する。

スレーブを参照したいコントローラで次のように書きます。 アクション毎に指定できます。

class FacilitiesController < ApplicationController
  use_database_pool [:index, :show, :map_xhr] => :persistent

application_controller.rb で全てスレーブ参照にしても更新系の SQL はちゃんとマスターにいくのですが、 レプリケーションの遅延があった場合よくあるアップデートして show にリダイレクで古い情報を表示してしまう問題と、 ごく一部のアクションが全クエリーのほとんどをしめるという理由からデフォルトマスターで、個々にスレーブを使うように指定する方針にしました。

おかげさまで、今回の GW の瞬間的なピークは New Relic で 3,092rpm を記録し、 Google Analytics リアルタイムのアクディブユーザで 1,900 を越えました。 AWS のオートスケールでアプリサーバも1台から5台まで自動的にスケールしました。

いこーよ のご利用ありがとうございました。

最後に、弊社ではシステムエンジニア、プログラマ、インフラエンジニアなどを募集しています。 おきがるにお問い合わせください。

Amazon Web Service にいこーよ

2012年04月16日
区分
AWS
報告者:
tahara

いこーよ を Amazon Web Service (AWS) に移行しました。 御蔭様でいこーよのアクセス数は伸びてきており、これで3回目の引越しです。

移行に際して一番悩んだのがオートスケールを前提としたデプロイ、監視でした。 オートスケールの設定をしている場合、インスタンスは負荷に応じて起動したり削除されたりします。 当然 IP アドレスも都度変っていきます。

  • cap production deploy:migrations する時に生きているサーバはどれ?
  • オートスケールでインスタンスが起動した時、ちゃんと最新のソースで起動するにはどうする?
  • munin でリソース監視したいけど、監視対象サーバはどれ?

といったところが、全くわかりませんでした。 世のオートスケール利用者の方々はどうされているんでしょうか。

いろいろ試行錯誤し、結局のところ次のようにしました。

  • unicorn を passenger に変更
    • unicorn に問題があったわけではありません。
  • cap は DB サーバにのみ行う。
  • passenger が動くアプリサーバは DB サーバのデプロイディレクトリを nfs マウント。

これでデプロイは DB サーバに対してだけ行えばよく、アプリの再起動も DB サーバで touch tmp/restart.txt を行えば、それを nfs マウントしているアプ リサーバの passenger がみんな再起動する、という仕組みです。

munin の方はうまい方法を思い付かなかったのでシェルスクリプトを書きました。 DB サーバは Elastic IP を使っているのでホスト名が固定です。 ec2-describe-instances の出力から DB サーバを除いて起動しているホスト名を取得し、 /etc/munin/munin.conf を都度生成します。

#!/bin/sh

export JAVA_HOME=/usr/lib/jvm/java-7-openjdk-i386
export EC2_HOME=/opt/ec2-api-tools
export PATH=$EC2_HOME/bin:$PATH
export EC2_PRIVATE_KEY=/foo/pk-KKKKKKKKKKKK.pem
export EC2_CERT=/foo/cert-XXXXXXXXXXXXXXX.pem
export EC2_URL=https://ec2.ap-northeast-1.amazonaws.com

ec2-describe-instances --show-empty-fields | \
  sed -n '/^INSTANCE.*running/p' | \
  sed -n '/54-248-122-232/!p' | \
  awk '{ print $4, $5; }' | \
  sed -e '=' | \
  sed -e 'N;s/\(.*\)\n\(.*\) \(.*\)/[iko-yo.net;ap\1]\n    address \2\n    use_node_name yes\n/' > /tmp/aws-munin.conf.tmp

sed -e '/KOKO/r /tmp/aws-munin.conf.tmp' -e '/KOKO/d' /etc/munin/munin.conf.template > /etc/munin/munin.conf

/etc/munin/munin.conf.template はこんな感じ。

...前略...

# DB サーバ
[iko-yo.net;db.iko-yo.net]
    address db.iko-yo.net

# アプリサーバ
KOKO

...後略...

さて DB サーバ m1.large x 1, アプリサーバ c1.medium x 1 で本番投入してみたのですが、DB サーバが重い。 その夜急遽 DB サーバを c1.xlarge に変更。 c1.xlarge にしたらリソースが余ったで、アプリも DB サーバで動かして c1.medium のアプリサーバ停止。 という構成になりました。

きっと GW か夏休みのピークにはオートスケールの出番が来るかと思います。

ちなみに最近のピーク時のアクセスは nginx のリクエスト数でいうと 90 request/second で New Relic の Throughput でいうと 770 rpm くらいです。 はやく DB の全件 like 検索しているのをなんとかしたいところです。

最後に、弊社ではシステムエンジニア、プログラマ、インフラエンジニアなどを募集しています。 お気軽るにお問い合わせください。

Rails 3.2.2 へのアップクレード (ssl_allowed が allowed されない件)

2012年03月21日
区分
Rails
報告者:
tahara

こんにちは tahara です。

先日 いこーよ を Rails 3.1.0 から Rails 3.2.2 にアップグレードしました。 今日はその模様を報告したいと思います。

まず Gemfile で Rails のバージョン指定を 3.2.2 にし bundle update しました。

gem 'rails', '3.2.2'

Asset Pipeline は使っていなので、 jquery-rails-2.0.1/vendor/assets/javascripts/jquery_ujs.js を public/javascripts にコピー。

config/environments/development.rb に次を追加。 開発環境で遅いクエリーは自動的に explain してくれるのはいいですね。 log/development.log に出力されます。

  # Raise exception on mass assignment protection for Active Record models
  config.active_record.mass_assignment_sanitizer = :strict

  # Log the query plan for queries taking more than this (works
  # with SQLite, MySQL, and PostgreSQL)
  config.active_record.auto_explain_threshold_in_seconds = 0.5

で、動かしてみたのですが ssl_requirement でエラーになりました。 どうやら bartt-ssl_requirement を使うのがよさそう。 Gemfile を次のように書き替えて bundle install したらうまく動きました。

gem 'bartt-ssl_requirement', '~>1.4.0', :require => 'ssl_requirement'

と思ったのですが、ssl_allowed しているアクションが http から https にリ ダイレクトされてしまう現象に遭遇しました。 もともとの ssl_requirement では ssl_allowed が ssl_required より優先されていたのに、 bartt-ssl_requirement では ssl_required の方が優先されるようになっていました。 ここは github の慣例にならって fork し、ssl_allowed が優先されるようにしました。 修正は一行です。

という感じで いこーよ をRails 3.2.2 にアップグレードできました。

弊社ではエンジニアを募集しています。 詳細はこちらを御覧ください。

CoffeeScript を使って Titanium でアプリを作る (Common Lisp バージョン)

2012年02月27日
区分
Titanium
報告者:
tahara

こんにちは、tahara です。

CoffeeScript を使って Titanium でアプリを作るにはいくつか方法があるようです。

弊社ではこのような場合 Common Lisp を使います(個人的に)。

コードは下記のとおり。 もし動かしてみたいという方がいらっしゃるようでしたら、 defparameter しているものを環境に合わせて変更してください。 あと Quicklisp でインストールできるものの他に https://github.com/quek/info.read-eval-print.series-ext も必要になります。

エラーがあれば repl に表示されます。 一度 Titanium Studio から Emulator でアプリを起動していれば、 CoffeeScript の保存で自動的に Eumlator での実行まで行います。 快適です。

;;;; CoffeeScript を使って Titanium で Android アプリを作る
;;;;
;;;; 参考にしたサイト
;;;; http://a-h.parfe.jp/einfach/archives/2011/0106235955.html

(eval-when (:compile-toplevel :load-toplevel :execute)
  (require :alexandria)
  (require :bordeaux-threads)
  (require :cl-ppcre)
  (require :info.read-eval-print.series-ext))

(info.read-eval-print.series-ext:sdefpackage
 :compile-coffee
 (:use :cl))

(in-package :compile-coffee)

(defparameter *builder.py*
  "~/.titanium/mobilesdk/linux/1.8.1/android/builder.py"
  "Titanium のビルドコマンド
iPhone で動かす場合は s/android/iphone/ でいいかもしれない")

(defparameter *project-dir*
  "~/Titanium\\ Studio\\ Workspace/outing-app"
  "Titanium のプロジェクトディレクトリ")

(defparameter *android-sdk*
  "~/local/opt/android-sdk-linux"
  "Android SDK のディレクトリ")

(defparameter *interval* 0.3
  "ファイルの変更監視間隔")

(defvar *compile-titanium-process* nil)

(defun escape-sh-arg (arg)
  (ppcre:regex-replace-all " " arg "\\ "))

(defun sh-async (control-string &rest format-arguments)
  (let* ((command (apply #'format nil control-string format-arguments))
         (process (progn (format *terminal-io* "~&~a" command)
                         (sb-ext:run-program "/bin/sh"
                                             (list "-c" command)
                                             :wait nil
                                             :output :stream
                                             :error :stream)))
         (streams (list (sb-ext:process-output process)
                        (sb-ext:process-error process)))
         (threads (labels ((cat (stream)
                             (bordeaux-threads:make-thread
                              (lambda ()
                                (collect-stream
                                 *terminal-io*
                                 (delete #\Return
                                         (scan-stream stream #'read-line))
                                 #'write-line)))))
                    (collect (cat (scan 'list streams))))))
    (bordeaux-threads:make-thread
     (lambda ()
       (let ((exit-code (sb-ext:process-exit-code (sb-ext:process-wait process))))
         (collect-ignore (progn
                           (bordeaux-threads:join-thread (scan 'list threads))
                           (close (scan 'list streams))))
         (if (zerop exit-code)
             (prog1 t (format *terminal-io* "~&ok"))
             nil))))
    process))

(defun sh (control-string &rest format-arguments)
  (let* ((command (apply #'format nil control-string format-arguments))
         (process (progn (format *terminal-io* "~&~a" command)
                         (sb-ext:run-program "/bin/sh"
                                             (list "-c" command)
                                             :wait nil
                                             :output :stream
                                             :error :stream)))
         (streams (list (sb-ext:process-output process)
                        (sb-ext:process-error process)))
         (threads (labels ((cat (stream)
                             (bordeaux-threads:make-thread
                              (lambda ()
                                (collect-stream
                                 *terminal-io*
                                 (delete #\Return
                                         (scan-stream stream #'read-line))
                                 #'write-line)))))
                    (collect (cat (scan 'list streams)))))
         (exit-code (sb-ext:process-exit-code (sb-ext:process-wait process))))
    (collect-ignore (progn
                      (bordeaux-threads:join-thread (scan 'list threads))
                      (close (scan 'list streams))))
    (if (zerop exit-code)
        (prog1 t (format *terminal-io* "~&ok"))
        nil)))

(defun compile-titanium ()
  (when *compile-titanium-process*
    (sb-ext:process-kill *compile-titanium-process*
                         sb-posix:sigterm)
    (setf *compile-titanium-process* nil))
  (setf *compile-titanium-process*
        (sh-async "~a run ~a ~a" *builder.py* *project-dir* *android-sdk*)))

(defun compile-coffee (file)
  (when (sh "coffee -o ~a -c ~a"
            (escape-sh-arg (directory-namestring file))
            (escape-sh-arg (namestring file)))
    (compile-titanium)))

(defun run ()
  (sb-cltl2:compiler-let ((series::*suppress-series-warnings* t))
    (collect-ignore
     (compile-coffee
      (choose-if (complement
                  (lambda (x)
                    (alexandria:starts-with-subseq ".#" (file-namestring x))))
                 (scan-file-change
                  (format nil "~a/**/*.coffee" *project-dir*)
                  :interval *interval*))))))

;; 実行
;; (bordeaux-threads:make-thread #'run :name "compile-coffee")

WordPress で同じ URL のまま User-Agent によって別のページを表示する

2012年02月21日
区分
WordPress
報告者:
tahara

こんにちは tahara です。

今回は WordPress です。

http://example.com/foo がリクエストされた時、クライアントがスマホだったら、 リダイレクトすることなく、同じ URL のまま、スマホ用のページを表示するには、 どうしたらいいか? 「、」が多い。

これがリダクレクトして別 URL になっても OK だよ、なら話は簡単だったのですが、 WordPress のパーマリンクの関連でかなり悩みました。

mod_rewrite で /foo が来たら /smartphon/foo に書き替えればいいや、と思っ ていたのですが、WordPress のパーマリンクは /smartphon/foo に書き替えて も最初のブラウザの要求の /foo をもとにページを返してきます。

で、結局は自分自身に proxy して解決しました。 しかし、自分自身への proxy は http://httpd.apache.org/docs/current/mod/mod_rewrite.html によると

doesn't make sense, not supported

らしいです。 とりあえず思ったとおりに動いてはいますが、他にいい方法はないものでしょうか?

.htaccess

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /

# for smartphone
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} !^/smartphone.*$
RewriteCond %{REQUEST_URI} !^/index.php
RewriteCond %{HTTP_USER_AGENT} (iPhone|iPod|Android|BlackBerry) [NC]
RewriteCond %{HTTP_USER_AGENT} !iPad [NC]
RewriteCond %{HTTP_COOKIE} !wptouch_switch_toggle=normal
RewriteRule .* - [E=SMART_PHONE_P:T]

RewriteCond %{ENV:SMART_PHONE_P} =T
RewriteRule ^$ http://example.com/smartphone [P,L]

RewriteCond %{ENV:SMART_PHONE_P} =T
RewriteRule ^foo$ http://example.com/smartphone/foo [P,L]

RewriteCond %{ENV:SMART_PHONE_P} =T
RewriteRule ^foo/bar$ http://example.com/smartphone/bar [P,L]

# for WordPress permlink
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]

</IfModule>

アクトインディ技術部隊報告書を Elephant から Rucksack に移行

2012年02月06日
区分
CommonLisp
報告者:
tahara

こんにちは、tahara です。

前々回のRails3 への移行に続いて移行ネタです。

本ブログ「アクトインディ技術部隊報告書」は Common Lisp (SBCL) + Hunchentoot + Elephant で動いていました。 しかし、1年2ヶ月ぶりのエントリを書こうとした時、新規エントリの登録ができなくなっていました。 Elephant をアップグレードしたのが原因だったのですが、 新しいバージョンにマイグレーションするのがめんどうだったので、 Rucksack に移行してしまいました。

Rucksack は同時に一トランザクションしか時行できなかったのですが、 そのへんは適当に改造して同時に複数トランザクション実行できるようにしました。 そのソースはこちら https://github.com/quek/rucksack です。

Hunchentoot と Rucksack の連携は次のように行います。 Hunchentoot のリダイレクによる throw と Rucksack のトランザクション制御を 両立するためにすこしめんどうなことになってまいます。

(defmethod hunchentoot::acceptor-dispatch-request ((self my-acceptor) request)
  (let (response
        (handler-done t))
    (rucksack:with-transaction ()
      ;; hunchentoot のリダイレクトのハンドリング
      (catch 'hunchentoot::handler-done
        (setf response (call-next-method))
        (setf handler-done nil)))
    (if handler-done
        (throw 'hunchentoot::handler-done nil)
        response)))

というわけで「アクトインディ技術部隊報告書」復活しましたので、 今後ともよろしくお願いいたします。

デプロイ時の Unicorn リスタートが失敗する件

2012年01月30日
区分
Rails
報告者:
tahara

こんにちは tahara です。

デプロイ時の Unicorn リスタートがときどき失敗して悩んでいました。 幸い本番環境では発生せず、ステージング環境と開発環境で発生していました。

リスタートは unicorn-4.1.1/examples/init.sh の upgrade を使っています。 upgrade は sig USR2 && sleep 2 && sig 0 && oldsig QUIT とい一連の流れになっています。 調べてみると sig USR2 で新しい PID ファイルが作成されるのですが、 sig 0 の時点でまだそれができていなくて失敗していました。 本番環境はサーバの性能が高いので sleep 2 で間に合っていましたが、 ステージング環境等では間に合わなかったんですね。

そこで、次のように sig 0 が成功するまで一定期間リトライするようにしました。

upgrade)
        echo -n "sig USR2"
        if sig USR2
        then
            sleep 1
            n=$UPGRADE_TIMEOUT
            while ! sig 0 && test $n -ge 0
            do
                printf '.' && sleep 1 && n=$(( $n - 1 ))
            done
            echo
            if test $n -lt 0 && ! sig 0
            then
                echo >&2 "sig SUR2 failed!"
                exit 1
            fi

            echo -n "oldsig QUIT"
            if oldsig QUIT
            then
                n=$TIMEOUT
                while test -s $old_pid && test $n -ge 0
                do
                    printf '.' && sleep 1 && n=$(( $n - 1 ))
                done
                echo
                if test $n -lt 0 && test -s $old_pid
                then
                    echo >&2 "$old_pid still exists after $TIMEOUT seconds"
                    exit 1
                fi
                echo "ok"
                exit 0
            fi
        fi
        echo
        echo >&2 "Couldn't upgrade, starting '$CMD' instead"
        $CMD
        ;;

これでうまくリスタートできるようになりました。

ついでに、何かの拍子に古い方のプロセスに QUIT を送れなかった時の対策として、 Unicorn の設定ファイルで QUIT を送るようにしました。

before_fork do |server, worker|
  # the following is highly recomended for Rails + "preload_app true"
  # as there's no need for the master process to hold a connection
  defined?(ActiveRecord::Base) and
    ActiveRecord::Base.connection.disconnect!

  # oldsig QUIT
  old_pid = "#{server.config[:pid]}.oldbin"
  if File.exists?(old_pid) && server.pid != old_pid
    begin
      Process.kill("QUIT", File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
    end
  end
end

これなら init.sh の方では sig USR2 だけで、後は Unicorn に古いプロセスの QUIT を まかせればいいかと思いましたが、 そうすると新しいプロセスのワーカが動き出すまで古いワーカが処理を行ってしまい、 DB のマイグレーションを行った時なんかは悲しいことになりそうです。

上記のように init.sh で古いプロセスを QUIT する場合は、 新しいプロセスのワーカが動き出すまでリクエストは待たされるので、 まだこちらの方がいいんじゃないかしらん、というところです。

Rails3 への移行

2012年01月24日
区分
Rails
報告者:
tahara

こんにちは、tahara です。

突然ですが、弊社では現在エンジニアを募集しています。 仕事内容は情シス業務と自社サービの開発です。 開発は主に Ralis で PHP もときどきあります。 たまぁに Common Lisp もあます(増やしていきたいです)。 詳細はこちらをご覧ください。

それでは本題です。

いまさらではありますが、弊社で運営している http://iko-yo.net を Rails3 に移行しました。 今回はその移行作業について書いていきたいと思います。

rvm で ruby 1.9.3 をインストールし gemsent を作成する。

bash < <(curl -s https://raw.github.com/wayneeseguin/rvm/master/binscripts/rvm-installer)
rvm install ruby-1.9.3
rvm use ruby-1.9.3
gem install bundler
rvm gemset create iko-yo-rails3
rvm --rvmrc --create 1.9.3@iko-yo-rails3
cd ..
cd -

きれいする

rm -r vendor/rails
rm -r vendor/gems
rm -r vendor/plugins/*

gem を入れる

vi Gemfile

assets は使わない。。。

source 'http://rubygems.org'

gem 'rails', '3.1.0'

# Bundle edge Rails instead:
# gem 'rails',     :git => 'git://github.com/rails/rails.git'

gem 'mysql2'
gem 'jquery-rails'
gem 'exception_notification'
gem 'geokit-rails3'
gem 'jpmobile'
gem 'nokogiri'
gem 'paperclip'
gem 'restful-authentication'
gem 'ssl_requirement'
gem 'acts_as_taggable_on_steroids'
gem 'acts_as_commentable'
gem 'will_paginate'
gem 'dynamic_form'
gem 'mecab-ruby', :require => 'MeCab'
gem 'twitter'
gem 'oauth'
gem 'garb'
gem 'gdata_19', :require => 'gdata'
gem 'holiday_jp'
gem 'dalli'
gem 'newrelic_rpm'

#;; config.assets.enabled = false
#;; # Gems used only for assets and not required
#;; # in production environments by default.
#;; group :assets do
#;;   gem 'sass-rails', "  ~> 3.1.0"
#;;   gem 'coffee-rails', "~> 3.1.0"
#;;   gem 'uglifier'
#;; end

# Use unicorn as the web server
gem 'unicorn'

# Deploy with Capistrano
gem 'capistrano'
gem 'capistrano-ext'

# To use debugger
# gem 'ruby-debug19', :require => 'ruby-debug'

group :test, :development do
  # Pretty printed test output
  gem 'turn', :require => false
  gem 'spork'
  gem 'rspec-rails', "~> 2.6"
  gem 'capybara'
  gem 'ZenTest'
  gem 'autotest-stumpwm'
  gem 'remarkable_activerecord', '>=4.0.0.alpha4'
  gem 'spork'
end

gem 入れて rails3 にする。

bundle install
bundle exec rails new

ソースの編集

config/rootes.rb はがんばる。 メールまわりも完全に書きなおし。

ソースをちまちま書きかえる(以下はイメージです。実際に動作するものではありません)。

config/boot.rb に次を追加
# /home/ancient/.rvm/rubies/ruby-1.9.2-p290/lib/ruby/1.9.1/psych.rb:148:in `parse': couldn't parse YAML at line 18 column 13 (Psych::SyntaxError)
require 'yaml'
YAML::ENGINE.yamler= 'syck'

各ファイルの1行目に次を追加
# -*- coding: utf-8 -*-


helper 系メソッドに .html_safe を付加

あは以下のようなイメージでどんどん書きかえていく。

s/adapter: mysql/adapter: mysql2/ database.yml.release

s/named_scope/scope/

s/RAILS_ROOT/Rails.root/

s/returning/tap/

s/request_uri/fullpath/

s/<% form/<%= form/
s/<%= f.fields_for/<%= f.fields_for/

layout が使われなかったのは ApplicationController#initialize で super を呼んでなかったからだった。

s/(.*).merge_conditions (.*)/where(\1).where(\2)/

s/mobile_filter :hankaku => true/hankaku_filter :input => true/

s/include ActionController::UrlWriter/include Rails.application.routes.url_helpers/

s/.class_name/.name/

s/link_to_remote .*/link_to \1, :remote => true/
:complete, :before 等は js で bind('ajax:complete', ...), bind('ajax:before', ...) にする。
http://www.alfajango.com/blog/rails-3-remote-links-and-forms/

error_messages がなくなったので gem 'dynamic_form'

s/choice/sample/

s/observe_field/ふつうの手書き jQuery/

s/action mailer/スーパークラスは Jpmobile::Mailer::Base で書きなおす/

/self.include_root_in_json = false/d config/initializers/wrap_parameters.rb

s/model.save(false)/model.save(:validate => false)/

s/errors.or\((.*)\)/errors[\1]/

他にもいっぱいあったような気もしますが、だいたいこんな感じです。

次に実行環境まわり。

実行環境

Apache と Passenger だったのを nginx と unicorn にしました。 unicorn についているサンプルをもとに設定しました。

まずは ngix

nginx.conf

user  deployer;
worker_processes  2;

pid /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    client_max_body_size 50m;
    client_header_buffer_size 4k;

    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  15;

    gzip  on;
    gzip_disable "msie6";
    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

    #open_file_cache max=2000 inactive=300s;
    #open_file_cache_valid 360s;
    #open_file_cache_min_uses 2;
    #open_file_cache_errors off;

    include /var/www/outing/current/config/unicorn/production/nginx-site.conf;
}

nginx-site.conf

upstream outing {
    # for UNIX domain socket setups:
    #server unix:/tmp/.outing.sock fail_timeout=0;
    # for TCP setups, point these to your backend servers
    server 127.0.0.1:8080 fail_timeout=0;
}

server {
    listen 80;
    root /var/www/outing/current/public;
    server_name iko-yo.net;

    location / {

        #auth_basic "Restricted";
        #auth_basic_user_file /etc/nginx/outing-password;

        if ($request_uri ~* "\.(jpg|jpeg|gif|css|png|js|ico)\?[0-9]+$") {
            expires max;
            access_log off;
            break;
        }
        if (-f $request_filename) {
            expires 24h;
            access_log off;
            break;
        }

        try_files $uri @app;
    }

    location @app {
      # an HTTP header important enough to have its own Wikipedia entry:
      #   http://en.wikipedia.org/wiki/X-Forwarded-For
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

      # enable this if and only if you use HTTPS, this helps Rack
      # set the proper protocol for doing redirects:
      # proxy_set_header X-Forwarded-Proto https;

      # pass the Host: header from the client right along so redirects
      # can be set properly within the Rack application
      proxy_set_header Host $http_host;

      # we don't want nginx trying to do something clever with
      # redirects, we set the Host: header above already.
      proxy_redirect off;

      # set "proxy_buffering off" *only* for Rainbows! when doing
      # Comet/long-poll/streaming.  It's also safe to set if you're using
      # only serving fast clients with Unicorn + nginx, but not slow
      # clients.  You normally want nginx to buffer responses to slow
      # clients, even with Rails 3.1 streaming because otherwise a slow
      # client can become a bottleneck of Unicorn.
      #
      # The Rack application may also set "X-Accel-Buffering (yes|no)"
      # in the response headers do disable/enable buffering on a
      # per-response basis.
      # proxy_buffering off;

      proxy_pass http://outing;
    }
}


# HTTPS server

server {
    listen 443;
    root /var/www/outing/current/public;
    server_name iko-yo.net;

    ssl on;
    ssl_certificate /etc/ssl/iko-yo.net/iko-yo.net.crt.cer;
    ssl_certificate_key /etc/ssl/iko-yo.net/iko-yo.net.key;

    ssl_session_timeout 5m;

    ssl_protocols SSLv3 TLSv1;
    ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
    ssl_prefer_server_ciphers on;

    try_files $uri @app;

    location @app {
      # an HTTP header important enough to have its own Wikipedia entry:
      #   http://en.wikipedia.org/wiki/X-Forwarded-For
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

      # enable this if and only if you use HTTPS, this helps Rack
      # set the proper protocol for doing redirects:
      proxy_set_header X-Forwarded-Proto https;

      # pass the Host: header from the client right along so redirects
      # can be set properly within the Rack application
      proxy_set_header Host $http_host;

      # we don't want nginx trying to do something clever with
      # redirects, we set the Host: header above already.
      proxy_redirect off;

      # set "proxy_buffering off" *only* for Rainbows! when doing
      # Comet/long-poll/streaming.  It's also safe to set if you're using
      # only serving fast clients with Unicorn + nginx, but not slow
      # clients.  You normally want nginx to buffer responses to slow
      # clients, even with Rails 3.1 streaming because otherwise a slow
      # client can become a bottleneck of Unicorn.
      #
      # The Rack application may also set "X-Accel-Buffering (yes|no)"
      # in the response headers do disable/enable buffering on a
      # per-response basis.
      # proxy_buffering off;

      proxy_pass http://outing;
    }
}

# for munin
server {
    listen 127.0.0.1;
    server_name localhost;
    location /nginx_status {
        stub_status on;
        access_log   off;
        allow 127.0.0.1;
        deny all;
    }
}

# redirect sub domain
server {
    listen 80;
    server_name *.iko-yo.net;
    rewrite ^(.*) http://iko-yo.net$1 permanent;
}

次に unicorn

次の unicorn-init.sh を /etc/init.d/outing へ ln -s します。

#!/bin/sh
### BEGIN INIT INFO
# Provides:          outing
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Example initscript
# Description:       This file should be used to construct scripts to be
#                    placed in /etc/init.d.
### END INIT INFO

# sudo ln -s /var/www/outing/current/config/unicorn/production/unicorn-init.sh /etc/init.d/outing
# sudo update-rc.d outing default
# sudo update-rc.d outing enable

set -e
# Example init script, this can be used with nginx, too,
# since nginx and unicorn accept the same signals

# Feel free to change any of the following variables for your app:

. "/usr/local/rvm/environments/ruby-1.9.3-p0@iko-yo-rails3"

TIMEOUT=${TIMEOUT-60}
APP_ROOT=/var/www/outing/current
PID=$APP_ROOT/tmp/pids/unicorn.pid
CMD="unicorn_rails -E production -D -c $APP_ROOT/config/unicorn/production/unicorn.conf.rb"
INIT_CONF=$APP_ROOT/config/init.conf
action="$1"
set -u

test -f "$INIT_CONF" && . $INIT_CONF

old_pid="$PID.oldbin"

cd $APP_ROOT || exit 1

sig () {
        test -s "$PID" && kill -$1 `cat $PID`
}

oldsig () {
        test -s $old_pid && kill -$1 `cat $old_pid`
}

case $action in
start)
        sig 0 && echo >&2 "Already running" && exit 0
        $CMD
        ;;
stop)
        sig QUIT && exit 0
        echo >&2 "Not running"
        ;;
force-stop)
        sig TERM && exit 0
        echo >&2 "Not running"
        ;;
restart|reload)
        sig HUP && echo reloaded OK && exit 0
        echo >&2 "Couldn't reload, starting '$CMD' instead"
        $CMD
        ;;
upgrade)
        if sig USR2 && sleep 2 && sig 0 && oldsig QUIT
        then
                n=$TIMEOUT
                while test -s $old_pid && test $n -ge 0
                do
                        printf '.' && sleep 1 && n=$(( $n - 1 ))
                done
                echo

                if test $n -lt 0 && test -s $old_pid
                then
                        echo >&2 "$old_pid still exists after $TIMEOUT seconds"
                        exit 1
                fi
                exit 0
        fi
        echo >&2 "Couldn't upgrade, starting '$CMD' instead"
        $CMD
        ;;
reopen-logs)
        sig USR1
        ;;
*)
        echo >&2 "Usage: $0 <start|stop|restart|upgrade|force-stop|reopen-logs>"
        exit 1
        ;;
esac

unicorn.conf.rb

# Sample verbose configuration file for Unicorn (not Rack)
#
# This configuration file documents many features of Unicorn
# that may not be needed for some applications. See
# http://unicorn.bogomips.org/examples/unicorn.conf.minimal.rb
# for a much simpler configuration file.
#
# See http://unicorn.bogomips.org/Unicorn/Configurator.html for complete
# documentation.

# Use at least one worker per core if you're on a dedicated server,
# more will usually help for _short_ waits on databases/caches.
worker_processes 4

# Since Unicorn is never exposed to outside clients, it does not need to
# run on the standard HTTP port (80), there is no reason to start Unicorn
# as root unless it's from system init scripts.
# If running the master process as root and the workers as an unprivileged
# user, do this to switch euid/egid in the workers (also chowns logs):
user "deployer", "deployer"

# Help ensure your application will always spawn in the symlinked
# "current" directory that Capistrano sets up.
working_directory "/var/www/outing/current" # available in 0.94.0+

# listen on both a Unix domain socket and a TCP port,
# we use a shorter backlog for quicker failover when busy
#listen "/tmp/.outing.sock", :backlog => 64
listen 8080, :tcp_nopush => true

# nuke workers after 30 seconds instead of 60 seconds (the default)
timeout 60

# feel free to point this anywhere accessible on the filesystem
pid "/var/www/outing/current/tmp/pids/unicorn.pid"

# By default, the Unicorn logger will write to stderr.
# Additionally, ome applications/frameworks log to stderr or stdout,
# so prevent them from going to /dev/null when daemonized here:
stderr_path "/var/www/outing/current/log/unicorn.stderr.log"
stdout_path "/var/www/outing/current/log/unicorn.stdout.log"

# combine REE with "preload_app true" for memory savings
# http://rubyenterpriseedition.com/faq.html#adapt_apps_for_cow
preload_app true
GC.respond_to?(:copy_on_write_friendly=) and
  GC.copy_on_write_friendly = true

before_fork do |server, worker|
  # the following is highly recomended for Rails + "preload_app true"
  # as there's no need for the master process to hold a connection
  defined?(ActiveRecord::Base) and
    ActiveRecord::Base.connection.disconnect!

  # The following is only recommended for memory/DB-constrained
  # installations.  It is not needed if your system can house
  # twice as many worker_processes as you have configured.
  #
  # # This allows a new master process to incrementally
  # # phase out the old master process with SIGTTOU to avoid a
  # # thundering herd (especially in the "preload_app false" case)
  # # when doing a transparent upgrade.  The last worker spawned
  # # will then kill off the old master process with a SIGQUIT.
  # old_pid = "#{server.config[:pid]}.oldbin"
  # if old_pid != server.pid
  #   begin
  #     sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
  #     Process.kill(sig, File.read(old_pid).to_i)
  #   rescue Errno::ENOENT, Errno::ESRCH
  #   end
  # end
  #
  # Throttle the master from forking too quickly by sleeping.  Due
  # to the implementation of standard Unix signal handlers, this
  # helps (but does not completely) prevent identical, repeated signals
  # from being lost when the receiving process is busy.
  # sleep 1
end

after_fork do |server, worker|
  # per-process listener ports for debugging/admin/migrations
  # addr = "127.0.0.1:#{9293 + worker.nr}"
  # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true)

  # the following is *required* for Rails + "preload_app true",
  defined?(ActiveRecord::Base) and
    ActiveRecord::Base.establish_connection

  # if preload_app is true, then you may also want to check and
  # restart any other shared sockets/descriptors such as Memcached,
  # and Redis.  TokyoCabinet file handles are safe to reuse
  # between any number of forked children (assuming your kernel
  # correctly implements pread()/pwrite() system calls)
end

問題は unicorn の upgrade (sudo service outing upgrade) で失敗する場合があること。 なぜでしょう。。。

あと unicorn は production 環境ではメモリ使用量が少ないのですが、 development 環境ではどんどんメモリをくっていきます。 これもなぜでしょう。。。

Ruby で Picasa

2010年10月01日
区分
Picasa
報告者:
tahara

こんにちは!! tahara です。 Ruby で Picasa の API をたたいてみました。

OAuth でアクセスできる素敵なライブラリをうまく見つけることができなかったので、 Google Data Ruby Utility Library を使って地味に作りました。

Developer's Guide: Protocol - Picasa Web Albums Data API - Google Code を参照しながらの試行錯誤だったので、いまいち自信ありません。

# -*- coding: utf-8 -*-
require 'oauth/client/net_http'

module Actindi
  class Picasa

    SITE = "http://photos.googleapis.com"
    CONSUMER_KEY = "xxxxxxxxx"
    CONSUMER_SECRET = "xxxxxxxxxxx"

    PUBLIC_ALBUM  = "公開アルバム"
    PRIVATE_ALBUM = "プライベートアルバム"
    ALBUM_ACCESS_PUBLIC = "public"
    ALBUM_ACCESS_PROTECTED = "protected"

    def initialize(token, secret)
      consumer = OAuth::Consumer.new CONSUMER_KEY, CONSUMER_SECRET, {
        :site             => SITE,
        :signature_method => 'HMAC-SHA1',
        :token            => OAuth::Token.new(token, secret)
      }
      @picasa = GData::Client::Photos.new(:http_service => GData::HTTP::OAuthService.new(consumer))
    end

    def ensure_albums
      return if @public_album && @private_album

      feed = @picasa.get("#{SITE}/data/feed/api/user/default").to_xml
      feed.elements.each("entry") do |entry|
        title = entry.elements["title"].text
        puts title
        if title == PUBLIC_ALBUM
          @public_album = entry
        elsif title == PRIVATE_ALBUM
          @private_album = entry
        end
      end
      return if @public_album && @private_album
      unless @public_album
        @public_album = create_album(PUBLIC_ALBUM, "public")
      end
      unless @private_album
        @private_album = create_album(PRIVATE_ALBUM, "protected")
      end
      ensure_albums
    end

    def user_data
      feed = @picasa.get("#{SITE}/data/entry/api/user/default").to_xml
      puts feed
      feed
    end

    def create_album(title, access)
      entry = <<ENTRY
<entry xmlns='http://www.w3.org/2005/Atom'
       xmlns:media='http://search.yahoo.com/mrss/'
       xmlns:gphoto='http://schemas.google.com/photos/2007'>
  <title type='text'>#{title}</title>
  <summary type='text'>あるばむぅ</summary>
  <gphoto:access>#{access}</gphoto:access>
  <category scheme='http://schemas.google.com/g/2005#kind'
    term='http://schemas.google.com/photos/2007#album'></category>
</entry>
ENTRY
      @picasa.headers = {}
      feed = @picasa.post("#{SITE}/data/feed/api/user/default", entry).to_xml
      feed
    end

    def post_photo(title, summary, photo_file_path, mime_type, access)
      album_id = album_id_from_access(access)
      entry = <<ENTRY
<entry xmlns='http://www.w3.org/2005/Atom'>
  <title>#{title}</title>
  <summary>#{summary}</summary>
  <category scheme="http://schemas.google.com/g/2005#kind"
    term="http://schemas.google.com/photos/2007#photo"/>
</entry>
ENTRY
      url = "#{SITE}/data/feed/api/user/default/albumid/#{album_id}"
      puts url
      puts entry
      @picasa.headers = {}
      feed = @picasa.post_file(url,
                               photo_file_path,
                               mime_type,
                               entry).to_xml
      puts feed
      feed.elements["link[@rel='edit']"].attributes['href']
    end

    def delete_photo(feed)
      @picasa.headers = {}
      @picasa.delete(feed)
    end

    def change_album(feed_url, access)
      album_id = album_id_from_access(access)
      @picasa.headers = {}
      entry = @picasa.get(feed_url).to_xml
      entry.elements["gphoto:albumid"].text = album_id
      edit_url = entry.elements["link[@rel='edit']"].attributes['href']
      @picasa.headers = {}
      feed = @picasa.put(edit_url, entry.to_s).to_xml
      feed.elements["link[@rel='edit']"].attributes['href']
    end

    def album_id_from_access(access)
      ensure_albums
      url = if access == ALBUM_ACCESS_PUBLIC
              @public_album.elements["id"].text
            else
              @private_album.elements["id"].text
            end
      url =~ /([^\/]+$)/
      $1
    end

    class << self
      def example
        token = "access token"
        secret = "access token secret"
        picasa = Actindi::Picasa.new(token, secret)
        feed = picasa.post_photo("題名", "サマリー", "/tmp/aaa.jpg", "image/jpeg", Actindi::Picasa::ALBUM_ACCESS_PUBLIC)
      end
    end
  end

end

module GData
  module HTTP
    class OAuthService

      def initialize(consumer)
        @consumer = consumer
      end

      def new
        self
      end

      # Take a GData::HTTP::Request, execute the request, and return a
      # GData::HTTP::Response object.
      def make_request(request)
        url = URI.parse(request.url)
        http = Net::HTTP.new(url.host, url.port)
        http.use_ssl = (url.scheme == 'https')
        http.verify_mode = OpenSSL::SSL::VERIFY_NONE

        case request.method
        when :get
          req = Net::HTTP::Get.new(url.request_uri)
        when :put
          req = Net::HTTP::Put.new(url.request_uri)
        when :post
          req = Net::HTTP::Post.new(url.request_uri)
        when :delete
          req = Net::HTTP::Delete.new(url.request_uri)
        else
          raise ArgumentError, "Unsupported HTTP method specified."
        end

        case request.body
        when String
          req.body = request.body
        when Hash
          req.set_form_data(request.body)
        when File
          req.body_stream = request.body
          request.chunked = true
        when GData::HTTP::MimeBody
           req.body_stream = request.body
          request.chunked = true
        else
          req.body = request.body.to_s
        end

        request.headers.each do |key, value|
          req[key] = value
        end

        request.calculate_length!

        @consumer.sign!(req)
        res = http.request(req)

        response = Response.new
        response.body = res.body
        response.headers = Hash.new
        res.each do |key, value|
          response.headers[key] = value
        end
        response.status_code = res.code.to_i
        return response
      end
    end
  end
end

Ruby で Google Analytics API

2010年07月09日
区分
Google
報告者:
tahara

こんにちは!! tahara です。 Ruby で Google Analytics API をたたいてみました。

といっても Garb を使えば簡単です。 ユーザID(email)とパスワードでも認証ができるのですが、今回は OAuth を使います。

インストール

gem install garb oauth

まずは Google Analytics, OAuth and Ruby. Oh, my. | everburning を参考に OAuth します。 あらかじめ https://www.google.com/accounts/ManageDomains から CONSUMER_KEY と CONSUMER_SECRET を取得しておく必要があります。

# -*- coding: utf-8 -*-

require 'oauth'

CONSUMER_KEY = "xxxxxx"
CONSUMER_SECRET ="xxxxxxxxx"

consumer = OAuth::Consumer.new CONSUMER_KEY, CONSUMER_SECRET, {
      :signature_method   => 'HMAC-SHA1',
      :site               => 'https://www.google.com',
      :request_token_path => '/accounts/OAuthGetRequestToken',
      :authorize_path     => '/accounts/OAuthAuthorizeToken',
      :access_token_path  => '/accounts/OAuthGetAccessToken',
    }

request_token = consumer.
  get_request_token({}, :scope => "https://www.google.com/analytics/feeds/")

# 次の URL をブラウザでアクセスし、確認コードを取得する。
p request_token.authorize_url

# 取得した確認コード
ACCESS_CODE = "xxxxxxxxx"

# 確認コードからアクセストークンを取得
access_token = request_token.get_access_token(:oauth_verifier => ACCESS_CODE)

# access_token.token と access_token.secret を取得する。
p access_token.token
p access_token.secret
ACCESS_TOKEN = access_token.token
ACCESS_SECRET = access_token.secret

# 次回からは次のようにしてアクセストークンを生成する。
access_token = OAuth::AccessToken.new(consumer, ACCESS_TOKEN, ACCESS_SECRET)

CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_SECRET がそろったので準備完了です。 Garb を使ってみます。 次は正規表現 ^/facilities/[0-9]+$ にマッチするページのページビューを取得するコードです。

# -*- coding: utf-8 -*-
=begin
http://github.com/vigetlabs/garb
=end

require "garb"
require "oauth"

CONSUMER_KEY = "xxxxx"
CONSUMER_SECRET ="xxxxxx"
ACCESS_TOKEN = "xxxxx"
ACCESS_SECRET = "xxxxx"

consumer = OAuth::Consumer.new CONSUMER_KEY, CONSUMER_SECRET, {
      :signature_method   => 'HMAC-SHA1',
      :site               => 'https://www.google.com',
      :request_token_path => '/accounts/OAuthGetRequestToken',
      :authorize_path     => '/accounts/OAuthAuthorizeToken',
      :access_token_path  => '/accounts/OAuthGetAccessToken',
    }
access_token = OAuth::AccessToken.new(consumer, ACCESS_TOKEN, ACCESS_SECRET)

Garb::Session.access_token = access_token

# プロファイル を指定
profile = Garb::Profile.first('UA-xxxxxxx-x')

class PageView
  extend Garb::Resource

  # 横に並ぶ項目。複数指定可能
  metrics :pageviews
  # 縦に並ぶ項目。複数指定可能
  dimensions :page_path
  # 並び順。複数指定可能。降順は後に .desc をつける。
  sort :pageviews.desc

  # フィルタ
  filters do
    # 正規表現で指定可能
    # http://code.google.com/intl/ja/apis/analytics/docs/gdata/gdataReferenceDataFeed.html#filters
    # http://www.google.com/support/analytics/bin/answer.py?answer=55582
    contains(:page_path, '^/facilities/[0-9]+$')
  end
end

# OpenStruct の配列で結果を取得。最大 10000 件取得できる。:offset で取得開始位置も指定可能。
res = PageView.results(profile, :start_date => '2010-07-01'.to_date, :end_date => '2010-07-07'.to_date, :limit => 10000)
# => [#<OpenStruct page_path="/facilities/159", pageviews="1237">, #<OpenStruct page_path="/facilities/164", pageviews="1061">, ...]

Data Feed Query Explorer - Google Analytics - Google Code ではブラウザから Analytics Data Export API をたたけるようになっていますので、 このページを参考にしながら、 metrics や dimensions の設定をいろいろかえると面白いことができるかもしれません。

Lisp on Rails 第9回 〜 ビュー

2010年06月05日
区分
LisponRails
報告者:
tahara

こんにちは!! tahara です。 Objective-C づけになり、すっかりこぶさたしておりましたが Lisp on Rails 第9回です!

今回はビューです。 Common Lisp で ERB 相当を実装します。 Common Lisp で実装するからにはリードテーブルを使い、 HTML ファイルを関数にコンパイルしたいと思います。

HTML ファイルを関数にコンパイルソースは http://github.com/quek/lisp-on-rails/blob/master/action-pack/ecl.lisp です。

ところどころ説明させていただきます。 html-defun-readtable ではビューファイルの最初の1文字をマクロキャラクタにして、 先頭に (in-package :xxxx) を追加し、全体を (defun xxxx () ...) でくるむようにしています。 動的にリーダをカスタマイズしているのです。 これでビューファイルを1つの関数として読み込むことができるようになります。

(defun html-defun-readtable (fname pathspec)
  (let ((*readtable* (basic-readtable)))
    (set-macro-character
     (first-char pathspec)
     (let ((in-package t))
       (lambda (stream char)
         (unread-char char stream)
         (print
          (if in-package
              (progn
                (setf in-package nil)
                `(in-package ,(package-name action-controller:*app-package*)))
              `(defun ,fname ()
                 ,(body-code stream char)))))))
    *readtable*))

Rails ではコントローラからビューへの値の受け渡しは @foo のようなインスタンス変数が使われます。 それに対応するためビューファイルの中に @ で始まるシンボルがあれば、 コントローラのスロット値へのアクセスに変換するシンボルマクロを定義します。

(defun body-code (stream char)
  (walk-body-code (read-body-code stream char)))

(defun read-body-code (stream char)
  (let ((*readtable* (make-html-readtable char)))
    (loop for x = (read stream nil stream t)
          until (eq x stream)
          collect x)))

(defun walk-body-code (code)
  `(symbol-macrolet
       ,(series:collect
            (series:mapping
             ((x (series:choose-if (q:^ q:symbol-head-p _ "@")
                                   (series:scan-lists-of-lists-fringe code))))
             `(,x (slot-value action-controller:*controller*
                              ',(intern (subseq (symbol-name x) 1)
                                        action-controller:*app-package*)))))
     ,@code))

そんなこんなで、なんとかモデル、コントローラ、ビューが繋がりました。

モデル

(in-package :blog)

(def-record post
  (:has-many comments))

(def-record comment
  (:belongs-to post))

コントローラ。生の defclass です。

(in-package :blog)

(defclass top-controller (application-controller)
  ((message)
   (posts)))

(defmethod index ((self top-controller))
  (with-slots (message posts) self
    (setf message "まみむめも♪"
          posts (all post))))

ビュー。HTML タグと loop が混在するのもまた一興ですね。

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>ブログ</title>
  </head>
  <body>
    <h1><%= @message %></h1>
    <h2>投稿を loop で表示する</h2>
    <ul><% (loop for post in @posts for comments = (comments-of post) do %>
      <li><%= (content-of post) %> -- <%= (name-of post) %></li>
      <% if comments do %>
      <ul><% (loop for comment in comments do %>
        <li><%= (body-of comment) %> -- <%= (commenter-of comment) %></li><% ) %>
      </ul><% ) %>
    </ul>
  </body>
</html>

コントローラは次のように書けるようにすると、それっぽい気もしますが、いまはまだ書けません。

(def-controller top (application)
  (def-action index
      (setf @message "まみむめも♪"
            @posts (all post)))
  (def-action foo
      (setf @essage "foo")))

以上、まとめますと 「リーダをいじれる言語は素敵ですね!」 でした。

ソースはこちらから http://github.com/quek/lisp-on-rails

第10回につづきます。

Apache で特定の User-Agent だけ BASIC 認証をバイパスする方法

2010年05月14日
区分
Apache
報告者:
tahara

こんにちは!! tahara です。 iPhone アプリからのアクセス以外はベーシック認証でブロックしたい、というときのお話です。 この設定で User-Agent に CFNetwork が含まれていない場合だけベーシック認証が必要になります。

<Location />
  Satisfy Any
  BrowserMatchNoCase CFNetwork is_iPhone=1
  Order Deny,Allow
  Deny from all
  Allow from env=is_iPhone

  AuthUserFile /var/www/htpasswd
  AuthGroupFile /dev/null
  AuthName "Please enter username and password"
  AuthType Basic
  require valid-user
</Location>

これでステージング環境が Google に補足されることもなくなるはずです。

関連する単語

2010年04月24日
区分
集合知
報告者:
tahara

こんにちは!! tahara です。

少々事情があってある単語に関連する単語を自動的に取得したくなりました。 『集合知イン・アクション』 を参考に Common Lisp で書いてみました。

Yahoo の Web API を利用させていただきます。

  • ウェブ検索とブログ検索で単語に関連するテキストを収集
  • 日本語形態素解析で単語に分解
  • 単語からタームベクトルを作成
(eval-when (:compile-toplevel :load-toplevel :execute)
  (require :drakma)
  (require :cxml)
  (require :cl-ppcre))

(defparameter *words*
  '("アナウンサー" "お医者さん" "イラストレーター" "宇宙飛行士"
    "タクシー運転手" "電車運転士" "バス運転士" "映画監督" "絵本作家"
    "演奏家" "歌手" "カメラマン" "看護師" "外交官" "画家" "高校の先生"
    "小学校の先生" "中学校の先生" "気象予報士" "キャビンアテンダント"
    "救急救命士" "銀行員" "警察官" "裁判官" "作詞家" "サッカー監督"
    "サッカー選手" "作曲家" "シェフ" "指揮者" "社長" "小説家" "消防士"
    "新聞記者" "動物のお医者さん" "政治家" "声優" "船長" "大工" "図書館司書"
    "俳優" "花火師" "花屋" "パイロット" "パン屋さん" "美容師"
    "ピアノニスト" "プロ野球選手" "弁護士" "幼稚園の先生")
  "これらの単語に関連する単語が欲しいのです。")

(defparameter *yahoo-appid*
  "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  "Yahoo Web API の アプリケーションID")
(defparameter *yahoo-ma-url* "http://jlp.yahooapis.jp/MAService/V1/parse"
  "日本語形態素解析")
(defparameter *yahoo-web-search-url*
  "http://search.yahooapis.jp/WebSearchService/V1/webSearch"
  "ウェブ検索")
(defparameter *yahoo-blog-search-url*
  "http://search.yahooapis.jp/BlogSearchService/V1/blogSearch"
  "ブログ検索")

(defparameter *occurrence-threshold* 5
  "これより少ない出現頻度の単語は無視します。")
(defparameter *stop-words*
  '("あれ" "いい" "こんな" "こちら" "こと" "これ" "それ" "ため" "とき" "ない"
    "もの" "よく"
    "以上" "一覧" "最新"
    "amp" "at" "by" "com" "gt" "http" "https" "jp" "lt")
  "これらの単語は無視します。")
(defparameter *stop-words-regexps*
  (mapcar #'ppcre:create-scanner
          '("^[0-90-9]+$" "^.$"))
  "これらの正規表現に一致する単語ま無視します。")

;; Drakma の設定
(setf drakma:*drakma-default-external-format* :utf-8)
(pushnew '("application" . "xml") drakma:*text-content-types* :test #'equal)

(defun stop-word-p (word)
  (or (find word *stop-words* :test #'string=)
      (some (lambda (x) (ppcre:scan x word)) *stop-words-regexps*)))

(defun yahoo-ma-request (text)
  (drakma:http-request
   *yahoo-ma-url*
   :method :post
   :parameters `(("appid" . ,*yahoo-appid*)
                 ("filter" . "1|9") ; 形容詞 名詞
                 ("sentence" . ,text))))

(defun text-to-words (text)
  (destructuring-bind (result-set
                       schema-location
                       (ma-result
                        _
                        total-count
                        filtered-count
                        word-list))
      (cxml:parse (yahoo-ma-request text) (cxml-xmls:make-xmls-builder))
    (declare (ignorable result-set schema-location ma-result _
                        total-count filtered-count))
    (loop for (_a _b (_c _d word)) in (cddr word-list)
         collect word)))

(defun yahoo-web-search-request (query)
  (drakma:http-request
   *yahoo-web-search-url*
   :method :get
   :parameters `(("appid" . ,*yahoo-appid*)
                 ("query" . ,query)
                 ("results" . "50")
                 ("format" . "html"))))

(defun web-search (query)
  (destructuring-bind (result-set
                       pgr . results)
      (cxml:parse (remove #\lf (yahoo-web-search-request query))
                  (cxml-xmls:make-xmls-builder))
    (declare (ignorable result-set pgr))
    (loop for (result _a (_title _b title) (_summary _c summary)) in results
         collect (list title summary))))

(defun yahoo-blog-search-request (query)
  (drakma:http-request
   *yahoo-blog-search-url*
   :method :get
   :parameters `(("appid" . ,*yahoo-appid*)
                 ("query" . ,query)
                 ("results" . "50"))))

(defun blog-search (query)
  (destructuring-bind (result-set first-result-position . results)
      (cxml:parse (remove #\lf (yahoo-blog-search-request query))
                  (cxml-xmls:make-xmls-builder))
    (declare (ignorable result-set first-result-position))
    (loop for (result _a id rss-url (_title _b title)
                      (_description _c description)) in results
         collect (list title description))))

(defun word-to-word-list (word)
  (remove-if #'stop-word-p
             (loop for i in '(web-search blog-search)
                append (text-to-words
                        (format nil "~{~{~a ~a ~}~}" (funcall i word))))))

(defun word-count-alist (word)
  (let (alist)
    (loop for i in (word-to-word-list word)
       if (assoc i alist :test #'string=)
       do (incf (cdr (assoc i alist :test #'string=)))
       else
       do (setf alist (acons i 1 alist)))
    (setf alist (sort alist #'(lambda (x y)
                                (>= (cdr x) (cdr y)))))
    (remove-if (lambda (x) (< (cdr x) *occurrence-threshold*)) alist)))

(defun normalize (alist)
  "重みづけを、その平方和が 1 とらるように正規化する。"
  (loop with factor = (sqrt (loop for i in alist sum (expt (cdr i) 2)))
       for (word . magnitude) in alist
       collect (cons word (/ magnitude factor))))

(defun all-word-alist ()
  (loop for word in *words*
     collect (print (cons word (normalize (word-count-alist word))))))

出力は次のとおりです。

("宇宙飛行士" ("宇宙" . 0.7740145) ("飛行士" . 0.5890963) ("山崎" . 0.09774244)
 ("若田" . 0.06604219) ("野口" . 0.05547544) ("地球" . 0.05547544)
 ("訓練" . 0.050192066) ("ステーション" . 0.047550377) ("毎日新聞" . 0.044908687)
 ("日本人" . 0.042267002) ("国際" . 0.042267002) ("日本" . 0.039625313)
 ("直子" . 0.039625313) ("スペースシャトル" . 0.03434194) ("ニュース" . 0.03434194)
 ("シャトル" . 0.03170025) ("帰還" . 0.03170025) ("家族" . 0.03170025)
 ("写真" . 0.029058563) ("ISS" . 0.029058563) ("情報" . 0.026416875)
 ("光一" . 0.026416875) ("聡一" . 0.026416875) ("JAXA" . 0.023775188)
 ("活動" . 0.023775188) ("飛行" . 0.023775188) ("紹介" . 0.023775188)
 ("産経新聞" . 0.023775188) ("映像" . 0.021133501) ("ミッション" . 0.021133501)
 ("NASA" . 0.021133501) ("交信" . 0.021133501) ("職業" . 0.018491814)
 ("毛利" . 0.018491814) ("滞在" . 0.018491814) ("撮影" . 0.018491814)
 ("研究" . 0.018491814) ("女性" . 0.018491814) ("サイト" . 0.018491814)
 ("搭乗" . 0.015850125) ("ページ" . 0.015850125) ("選抜" . 0.015850125)
 ("イベント" . 0.015850125) ("実現" . 0.015850125) ("アポロ" . 0.015850125)
 ("きぼう" . 0.013208438) ("航空" . 0.013208438) ("開発" . 0.013208438)
 ("機構" . 0.013208438) ("参加" . 0.013208438) ("さいたま市" . 0.013208438)
 ("試験" . 0.013208438) ("仕事" . 0.013208438) ("最後" . 0.013208438)
 ("月面" . 0.013208438) ("着陸" . 0.013208438) ("特集" . 0.013208438)
 ("時事通信" . 0.013208438) ("契約" . 0.013208438) ("サム" . 0.013208438))

さて、この出力を利用することができるかどうかがまた問題です。

Lisp on Rails 第8回 〜 before_*

2010年04月10日
区分
LisponRails
報告者:
tahara

こんにちは!! tahara です。 Lisp on Rails 第8回です!

今回は ActiveRecord::Base の save, create, update, destroy 等々のメソッドには beforo_* や after_* というフックメソッドを定義することができます。 ActiveRecord::Callbacks でそのあたりの実装がされています。

これを Common Lisp でやろうとした場合、

(defmethod save :before ((self post)) ...)

で OK と思ったらそうはいきません。 before_* メソッドが false を返した場合はメソッドを呼び出しを中断する必要があります。 Common Lisp の before メソッドは返り値は無視してしまうので、そのまま使うことはできないのです。

仕方ないので自分で新しいメソッドコンビネーションを実装します。

(define-method-combination active-record ()
  ((around (:around))
   (before (:before))
   (primary () :required t)
   (after (:after)))
  "before メソッドが nil を返した場合メソッドの実行を中断する。"
  (flet ((call-methods (methods)
           (mapcar #'(lambda (method)
                       `(call-method ,method))
                   methods))
         (call-methods-and (methods)
           `(and ,@(mapcar #'(lambda (method)
                               `(call-method ,method))
                           methods))))
    (let ((form (if (or before after (rest primary))
                    `(when ,(call-methods-and before)
                       (multiple-value-prog1
                           (call-method ,(first primary)
                                        ,(rest primary))
                         ,@(call-methods (reverse after))))
                    `(call-method ,(first primary)))))
      (if around
          `(call-method ,(first around)
                        (,@(rest around)
                           (make-method ,form)))
          form))))

あとは defgeneric するときにこのメソッドコンビネーションを指定すれば OK です。

(defgeneric save (record)
  (:method-combination active-record)
  ...)

簡単にメソッドの呼び出し方法を定義できてしまうなんて Common Lisp はいい言語ですね。

ソースはこちらから http://github.com/quek/lisp-on-rails

第9回につづきます。

Lisp on Rails 第7回 〜 ActiveRecord::Base の find メソッド

2010年03月27日
区分
LisponRails
報告者:
tahara

遅くなりました!! tahara です。 Lisp on Rails 第7回です!

今回は ActiveRecord::Base の find メソッドの機能を多少実装してみたいと思います。

ActiveRecord::Base の find メソッドは次の4つの使い方があります。

  1. id で検索。 引数は (id, *args), (id1, id2, ..., *args), ([id1, id2, ..., *args]) の3パターン。 該当するレコードがない場合は RecordNotFound が発生する。
  2. 最初の1件を検索。 引数は (:first, *args) で、該当がない場合は nil を返す。 Model.first(*args) というショートカットがある。
  3. 最後の1件を検索。 引数は (:last, *args) で、該当がない場合は nil を返す。 Model.last(*args) というショートカットがある。
  4. 該当する全件を検索。 引数は (:all, &args) で、該当がない場合は nil を返す。 Model.all(*args) というショートカットがある。

上記の4つの使い方全てで次のハッシュオプションが使えます。

  • :conditions - いわゆる検索条件。文字列またはリストで指定。
  • :order - SQL の ORDEY BY
  • :group - SQL の GROUP BY
  • :having - SQL の HAVING
  • :limit - 最大取得件数。
  • :offset - 取得開始位置。
  • :joins - SQL の JOIN だけど、普通次の :include を使う。
  • :include - 検索時に一緒にとってくるテーブルをアソシエーション名(has_many :xxxs)で指定する。
  • :select - 取得カラム。デフォルトは "*"。
  • :from - SQL の FROM。ビューから検索するとき等に使える。
  • :readonly - 取得結果をリードオンリー指定する。
  • :lock - SQL でのロック。"FOR UPDATE" とか。

このような場合 Common Lisp ではマルチディスパッチとキーワード引数を使えばうまくいくはずです。

;; find は CL にあるので select にする
(defgeneric select (class id-or-keyword
                          &key
                          conditions
                          order
                          group
                          having
                          limit
                          offset
                          joins
                          include
                          select
                          from
                          readonly
                          lock
                          &allow-other-keys))

(defmethod select ((class symbol) id-or-keyword &rest args)
  (apply #'select (find-class class) id-or-keyword args))

(defmethod select ((class active-record-class) (id integer)
                   &rest args
                   &key conditions)
  (setf conditions (append (list :id id) conditions))
  ...)

(defmethod select ((class active-record-class) (ids list)
                   &rest args
                   &key conditions)
  (setf conditions (append (list :id ids) conditions))
  ...)

(defmethod select ((class active-record-class) (keyword (eql :all))
                   &rest args)
  ...)

(defmethod select ((class active-record-class) (keyword (eql :first))
                   &rest args
                   &key (order "id"))
  ...)

(defmethod select ((class active-record-class) (keyword (eql :last))
                   &rest args
                   &key (order "id"))
  ...)

(defun all (&rest args)
  (apply #'select (car args) :all (cdr args)))
;; first と last は CL パッケージとかぶる。

といった感じで実装してみました。 joins, include, readonly 等はまだ未実装です。

しかし Common Lisp パッケージとシンボル名(find, first, last)がかぶるのが悩ましいところです。shadowing しようかしらん。

ソースはこちらから http://github.com/quek/lisp-on-rails

第8回につづきます。

Lisp on Rails 第6回 〜 ここらでリファクタリング

2010年03月19日
区分
LisponRails
報告者:
tahara

こんにちは!! tahara です。 Lisp on Rails 第6回です!

has-one を has-many のコピペで書いてしまったので、 ここらでリファクタリングしたいと思います。

has-one のスロット定義

(defclass ar-has-one-slot-mixin ()
  ((has-one :initarg :has-one
            :initform nil
            :accessor has-one)
   (class-symbol :initarg :class-symbol
                 :initform nil
                 :accessor class-symbol)))

(defmethod initialize-instance :after ((self ar-has-one-slot-mixin) &rest args)
  (declare (ignore args))
  (unless (class-symbol self)
    (setf (class-symbol self) (has-one self))))

(defclass ar-has-one-direct-slot-definition (ar-direct-slot-definition
                                             ar-has-one-slot-mixin)
  ())

(defclass ar-has-one-effective-slot-definition (ar-effective-slot-definition
                                                ar-has-one-slot-mixin)
  ())

has-many のスロット定義

(defclass ar-has-many-slot-mixin ()
  ((has-many :initarg :has-many
             :initform nil
             :accessor has-many)
   (class-symbol :initarg :class-symbol
                 :initform nil
                 :accessor class-symbol)))

(defmethod initialize-instance :after ((self ar-has-many-slot-mixin) &rest args)
  (declare (ignore args))
  (unless (class-symbol self)
    (setf (class-symbol self)
          (sym (singularize (has-many self))))))

(defclass ar-has-many-direct-slot-definition (ar-direct-slot-definition
                                              ar-has-many-slot-mixin)
  ())

(defclass ar-has-many-effective-slot-definition (ar-effective-slot-definition
                                                 ar-has-many-slot-mixin)
  ())

いやー、ひどいですね。 ほとんど one と many の違いだけです。

さて、これをリファクタリングするのに Common Lisp にはマクロという手抜きプログラマには必須の機能があります。

普通リファクタリングするとなると、関数、メソッド、スーパークラス等々の 切り出しが必要になりますよね? でも、マクロなら何ら設計を変更することなくリファクタリングが可能になります。

では、実際にマクロを使ってリファクタリングしてみましょう。

(defmacro def-has-xxx-slot-definition (xxx
                                       default-class-symbol-form)
  `(progn
     (defclass ,(sym "ar-has-" xxx "-slot-mixin") ()
       ((,(sym "has-" xxx) :initarg ,(key-sym "has-" xxx)
                 :initform nil
                 :accessor ,(sym "has-" xxx))
        (class-symbol :initarg :class-symbol
                      :initform nil
                      :accessor class-symbol)))

     (defmethod initialize-instance :after ((self ,(sym "ar-has-" xxx "-slot-mixin")) &rest args)
        (declare (ignore args))
        (unless (class-symbol self)
          (setf (class-symbol self) ,default-class-symbol-form)))

     (defclass ,(sym "ar-has-" xxx "-direct-slot-definition") (ar-direct-slot-definition
                                                  ,(sym "ar-has-" xxx "-slot-mixin"))
       ())

     (defclass ,(sym "ar-has-" xxx "-effective-slot-definition") (ar-effective-slot-definition
                                                     ,(sym "ar-has-" xxx "-slot-mixin"))
       ())
     ))

(def-has-xxx-slot-definition one (has-one self))
(def-has-xxx-slot-definition many (sym (singularize (has-many self))))

すばらい。 最初のひどい設計を何ら変えることなくリファクタリングできました。

手抜き設計のベタ書きコードそのままで、リファクタリングを可能とするマクロは、 未熟なプログラマにとって、なくてはならない存在です。

ソースはこちらから http://github.com/quek/lisp-on-rails

第7回につづきます。

Lisp on Rails 第5回 〜 has-one

2010年03月12日
区分
LisponRails
報告者:
tahara

こんにちは!! tahara です。 Lisp on Rails 第5回です!

今回は ActiveRecord の has_one アソシエーションもどきを実装したいと思います。 前回の has_many に瓜二つです。 ar-has-one-direct-slot-definition と ar-has-one-effective-slot-definition を定義します。

(defclass ar-has-one-slot-mixin ()
  ((has-one :initarg :has-one
            :initform nil
            :accessor has-one)
   (class-symbol :initarg :class-symbol
                 :initform nil
                 :accessor class-symbol)))

(defmethod initialize-instance :after ((self ar-has-one-slot-mixin) &rest args)
  (declare (ignore args))
  (unless (class-symbol self)
    (setf (class-symbol self) (has-one self))))

(defclass ar-has-one-direct-slot-definition (ar-direct-slot-definition
                                             ar-has-one-slot-mixin)
  ())

(defclass ar-has-one-effective-slot-definition (ar-effective-slot-definition
                                                ar-has-one-slot-mixin)
  ())

direct-slot-definition-class と effective-slot-definition-class と compute-effective-slot-definition でゴニョゴニョすると上記の slot-definition が使えるようになります。

slot-value-using-class と (setf slot-value-using-class) で関連テーブルの 取得と設定を行います。

(defmethod c2mop:slot-value-using-class
  ((class active-record-class)
   instance
   (slot-def ar-has-one-effective-slot-definition))
  (aif (call-next-method)
       it
       (setf (slot-value instance (has-one slot-def))
             (car (all (find-class (class-symbol slot-def))
                       :conditons (list (key-sym (class-name class) '-id)
                                        (%value-of instance :id)))))))

(defmethod (setf c2mop:slot-value-using-class) :after
           (new-value
            (class active-record-class)
            instance
            (slot-def ar-has-one-effective-slot-definition))
   (when new-value
     (setf (%value-of new-value (str (class-name class) "-id"))
           (%value-of instance :id))))

で、だいたいこんな感じで使えるようになります。

(def-record post
  (:has-many comments)
  (:has-one post-info))
(def-record post-info
  (:belongs-to post))
(let* ((post (car (all post)))
       (post-info (post-info-of post)))
  (describe post-info))

以上、なんとなく has-one できました。 ソースはこちらから http://github.com/quek/lisp-on-rails

第6回につづきます。

すみません。 次回はコピペじゃないように頑張ります。

多段 ssh で vnc

2010年03月05日
区分
ssh
報告者:
tahara

こんにちは!! tahara です。

例えば host1 を経由して host2 に vnc したい場合どうすればいいか調べてみました。 結果 ↓ のページに書かれてあるとおりすれば可能でした。ありがとうございます。

ssh を多段に使ってずっと遠くにあるマシンに port forward する|裏表(Phinloda のもう裏だか表だか分からないページ)

蛇足になりますが、次の手順で host2 に vnc できました。

iPhone アプリケーションの開発

2010年02月26日
区分
iPhone
報告者:
tahara

こんにちは!! tahara です。

突然ではありますが、とりあえず読むべきは iPhone Dev Center にある以下のドキュメントでしょうか。

まずは iPhone ヒューマンイン ターフェイス ガイドライン を読んでみました。

iPhone が他と違うところ

  • 小さい画面サイズ。 480x320
  • メモリ少ない。OS が警告をくれたらすぐにメモリ開放すべすし。didReceiveMemoryWarning
  • 1画面。ある意味 Stumpwm!
  • さらに、同時に1アプリケーション。
  • 標準に準拠してヘルプコンテンツを最小限にすべし。

3つのアプリケーションの形態

  • iPhone アプリケーション
  • Web 専用contents
  • ハイブリッドアプリケーション(上の2つの組み合わせ)

3つのアプリケーションスタイル

  • 生産性型アプリケーション(例 Mail)
  • ユーティリティ型アプリケーション(例 Weather)
  • 没入型アプリケーション(例 ゲームアプリ)

デスクトップアプリを移植する場合はよく使われる 20% の機能のみに絞る。

ヒューマンインターフェイスの原則

  • 自然なメタファで
  • 具体的なものを直接いじる感覚
  • テキスト入力を最小限に、選択肢を提示
  • ユーザの操作に対するフィードバック、効果的なやりとり
  • ユーザが開始でき、それを停止できる動きであること
  • インターフェイスの一貫性

製品定義ステートメントを作りましょう。

  • 対象ユーザを明確化
  • 実装機能の限定

簡潔さと使いやすさ

  • 使い方を明白に
  • 頻繁に使用される情報を画面上部に集中
  • テキスト入力を最小限
  • 重要な情報を簡潔に
  • タップ可能な要素の領域は指先サイズに

製品ステートメントにふさわしい主たる機能に焦点を当て、 簡潔かつ入力を最小限にする。

ジェスチャを適切にサポートする。

  • 単純かつ簡単な方法は必須
  • 新しいジェスチャを定義しない

ブランドはささやかで控え目に。 ただしアプリケーションアイコンにはブランドももりこむ。

一般的なタスク

  • 開始
    • ステータスバーに適切なスタイルを設定する
    • 最初の画面に似た起動画面を用意して感覚的起動時間を短縮する
    • 余計なスプラッシュ画面は不要
    • 基本縦で起動
    • 最後に実行したときの状態を復元する
  • 停止
    • いつでも停止できるように
    • 停止時は次の起動のために可能なかぎり詳細の情報を保存しておく
  • 設定(Settings)と設定オプション
    • 設定は一度設定したら変更しないもの
    • 設定オプションは頻繁に変更する可能性があるもの
    • 理想はユーザに設定を要求しないこと
  • その他いろいろ ... ちょっと省略してしまいました。

ユーザインターフェイスの設計

  • ステータスバーはあまりいじらない
  • ToolBar は 44x44 で5つ以下が妥当
  • Tab Bar はモードの切り替え。 5つまでは横に並んで、それをこえたものは More に。 バッジを表示できる。
  • モーダルビューは煩わしいので乱用しない。
  • Action Sheet はユーザの選択肢でり複数のボタンを備える。 害のあるものは赤で一番上に。
  • モーダルビューは主たる機能が関連する自己簡潔型のタスクで(例 Mail のメッセージ作成)
  • アクティビティインジケータ
    • ステータスバーのは1〜2秒ネットワークにアクセスする場合
    • もっと時間がかかる場合はツールバーに表示
  • 進捗がわかるものは Progress View
  • Text Field は左端に使用目的、右端に追加機能
  • システムが用意しているボタン、アイコンを意図されたとおり使う
    • ユーザにやさしい
    • 工数削減

アイコン、画像の作り方の説明でおしまい。

このようなドキュメントがしっかり用意されているのは素晴しいですね。

Lisp on Rails 第4回 〜 has-many

2010年02月19日
区分
LisponRails
報告者:
tahara

こんにちは!! tahara です。 Lisp on Rails 第4回です!

今回は ActiveRecord の has_many アソシエーションもどきを実装したいと思います。 前回の belongs_to とほとんど同じです。 ar-has-many-direct-slot-definition と ar-has-many-effective-slot-definition を定義します。

(defclass ar-has-many-slot-mixin ()
  ((has-many :initarg :has-many
             :initform nil
             :accessor has-many)
   (class-symbol :initarg :class-symbol
                 :initform nil
                 :accessor class-symbol)))

(defmethod initialize-instance :after ((self ar-has-many-slot-mixin) &rest args)
  (declare (ignore args))
  (unless (class-symbol self)
    (setf (class-symbol self)
          (sym (singularize (has-many self))))))

(defclass ar-has-many-direct-slot-definition (ar-direct-slot-definition
                                              ar-has-many-slot-mixin)
  ())

(defclass ar-has-many-effective-slot-definition (ar-effective-slot-definition
                                                 ar-has-many-slot-mixin)
  ())

direct-slot-definition-class と effective-slot-definition-class と compute-effective-slot-definition でゴニョゴニョすると上記の slot-definition が使えるようになります。

slot-value-using-class と (setf slot-value-using-class) で関連テーブルの 取得と設定を行います。

(defmethod c2mop:slot-value-using-class
  ((class active-record-class)
   instance
   (slot-def ar-has-many-effective-slot-definition))
  (aif (call-next-method)
       it
       (setf (slot-value instance (has-many slot-def))
             (all (find-class (class-symbol slot-def))
                  :conditons (list (key-sym (class-name class) '-id)
                                   (%value-of instance :id))))))

(defmethod (setf c2mop:slot-value-using-class) :after
           (new-value
            (class active-record-class)
            instance
            (slot-def ar-has-many-effective-slot-definition))
  (loop with id = (%value-of instance :id)
        with column = (str (class-name class) "-id")
        for x in new-value
        do (setf (%value-of x column) id)))

で、だいたいこんな感じで使えるようになります。

(def-record comment
    (:belongs-to post))
(def-record post
    (:has-many comments))
(let* ((post (car (all post)))
       (comments (comments-of post)))
  (mapc #'describe comments))

以上、なんとなく has-many できました。 ソースはこちらから http://github.com/quek/lisp-on-rails

第5回につづきます

Lisp on Rails 第3回 〜 belongs-to

2010年02月12日
区分
LisponRails
報告者:
tahara

こんにちは!! tahara です。 Lisp on Rails 第3回です!

今回は ActiveRecord の belongs_to アソシエーションもどきを実装したいと思います。 実行時のイメージとしては次のようになります。

(def-record post)
(def-record comment
    (:belongs-to post)) ; comments テーブルは post_id 列を持っている。

(mapcar #'post-of (all comment))
;; => ポストのリストを取得

これを実装するのに Common Lisp の MOP を使ってみます。 インスタンス変数とそのインスタンス変数へのアクセスを拡張することになります。

まずインスタンス変数の拡張です。 ar-belongs-to-slot-mixin に belongs_to のアソシエーションテーブルと 外部キーを持たせ、それを継承した ar-belongs-to-effective-slot-definition を belongs_to のアソシエーションのインスタンス変数とします。

(defclass ar-belongs-to-slot-mixin ()
  ((belongs-to :initarg :belongs-to
               :initform nil
               :accessor belongs-to)
   (foreign-slotname :initarg :foreign-slotname
                     :initform nil
                     :accessor foreign-slotname)))

(defclass ar-belongs-to-effective-slot-definition (ar-effective-slot-definition
                                                   ar-belongs-to-slot-mixin)
  ())

slot-value-using-class を使い ar-belongs-to-effective-slot-definition にアクセスする際に、 foreign-slotname を外部キーとして belongs-to のテーブルから レコードを取得するようにします。

(defmethod c2mop:slot-value-using-class
  ((class active-record-class)
   instance
   (slot-def ar-belongs-to-effective-slot-definition))
  (aif (call-next-method)
       it
       (setf (slot-value instance (belongs-to slot-def))
             (select (find-class (belongs-to slot-def))
                     (slot-value instance (foreign-slotname slot-def))))))

以上、なんとなく belongs-to できました。 ソースはこちらから http://github.com/quek/lisp-on-rails

第4回につづきます

CMYK

2010年02月05日
区分
RubyonRails
報告者:
tahara

こんにちは!! tahara です。 今回は Lisp on Rails はお休みで、Ruby on Rails の Paperclip の話題です。

IE で RGB の画像は表示できますが、CMYK の画像は表示できません。 CMYK から RGB に変換する手もありますが、変換時に結構色が変ってしまいます。 ということでアップロードする画像が CMYK の場合は、 バリデーションではじくようにします。

まず CMYK の判定ですが、ImageMagic の identify コマンドで可能です。

~% identify -format '%[colorspace]' ~/archive/normal.jpg
CMYK
~% identify -format '%[colorspace]' ~/archive/normal-rgb.jpg
RGB

ただし古い ImageMagic では -format '%[colorspace]' オプションが認識されないので -verbose オプションを使います。

~% identify -verbose ~/archive/normal.jpg
Image: /home/ancient/archive/normal.jpg
  Format: JPEG (Joint Photographic Experts Group JFIF format)
  Class: DirectClass
  Geometry: 290x200+0+0
  Resolution: 72x72
  Print size: 4.02778x2.77778
  Units: PixelsPerInch
  Type: ColorSeparation
  Endianess: Undefined
  Colorspace: CMYK      <=== これ!!
  Depth: 8-bit
  ...

この identify を呼び出して CMYK をチェックするバリデーションを config/initializers/paperclip.rb に組み込みます。

module Paperclip
  module ClassMethods
    ...

    # CMYK は IE で表示できないため、検証でエラーとする。
    def validates_attachment_not_cmyk name, options = {}
      message = options[:message] || "CMYK フォーマットの画像はブラウザによって表示できないため使用できません。お手数ですが RGB フォーマットに変換してください。"
      attachment_definitions[name][:validations][:not_cmyk] = lambda do |attachment, instance|
        original_file = attachment.instance_variable_get(:@queued_for_write)[:original]
        colorspace =
          begin
            attachment.file? && original_file &&
              Paperclip.run("identify", "-verbose #{original_file.path}")
          rescue
            nil
          end
        if colorspace && colorspace =~ /^\s*Colorspace: CMYK$/
          message
        end
      end
    end
    ....

これでモデルから validates_attachment_not_cmyk というひどい名前のバリデーションが使えるようになります。

  validates_attachment_not_cmyk :picture_1

まとめるとこうなります。

(incf ImageMagic)
(decf IE most-positive-fixnum)

Lisp on Rails 第2回 〜 メタクラス

2010年01月29日
区分
LisponRails
報告者:
tahara

こんにちは!! tahara です。 Lisp on Rails 第2回です!

ActiveRecord::Base ではクラスメソッドとして find や all の検索メソッドが定義されています。

Common Lisp でそれらを実装するにあたり、メタクラスのメソッドとして実装してみます。 Common Lisp では自分でメタクラスを定義することができます。 次のようにメタクラスを定義します。 なんとなく、テーブル名とカラム情報を持たせています。

(defclass active-record-class (standard-class)
  ((%table-name :initarg :%table-name :accessor %table-name-of)
   (%columns :initarg :%columns :accessor %columns-of)))

各テーブル毎のクラスはこの active-record-class のインスタンスになり、 インスタンス変数としてテーブル名とカラム情報を持つイメージです。

メタクラスに全レコードを取得する all メソッドを実装します。

(defmethod all ((class active-record-class))
  (multiple-value-bind (rows columns)
      (clsql-sys:query (format nil "select * from ~a" (%table-name-of class)))
    (loop for i in rows
          collect (make-instance-from-row class i columns))))

(def-record post) は次のように展開されて :METACLASS ACTIVE-RECORD::ACTIVE-RECORD-CLASS を指定します。

(PROGN
  (DEFPARAMETER POST
    (DEFCLASS POST (BASE)
      ((ID :INITARG :ID :INITFORM NIL :ACCESSOR ID-OF)
       (NAME :INITARG :NAME :INITFORM NIL :ACCESSOR NAME-OF)
       (TITLE :INITARG :TITLE :INITFORM NIL :ACCESSOR TITLE-OF)
       (CONTENT :INITARG :CONTENT :INITFORM NIL :ACCESSOR CONTENT-OF)
       (CREATED-AT :INITARG :CREATED-AT :INITFORM NIL :ACCESSOR
                   CREATED-AT-OF)
       (UPDATED-AT :INITARG :UPDATED-AT :INITFORM NIL :ACCESSOR
                   UPDATED-AT-OF))
      (:METACLASS ACTIVE-RECORD::ACTIVE-RECORD-CLASS)))
      ...

Ruby のクラス定義と違って Common Lisp のクラス定義ではクラス名に何も束縛しないため、 わざわざ defparameter しています。

そんな感じでメタクラスによる

(all post)

ができました。

ソースはこちらから http://github.com/quek/lisp-on-rails

第3回につづきます

Lisp on Rails 第1回

2010年01月22日
区分
LisponRails
報告者:
tahara

こんばんは!! tahara です。

いま仕事で Ruby on Rails を使っています。 正直 Active Record いいっすねー。 DB いじるには楽です。

くやしいので Common Lisp でそれっぽいのを実装してみます。 Lisp on Rails ですね(何だか以前から聞く言葉のような気もします)。

第1回目の今日は何となく Active Record っぽい雰囲気のような感じだけです。

(eval-when (:compile-toplevel :load-toplevel :execute)
  (setf *connection-spec* '("localhost" "blog_test" "root" ""))
  (setq clsql-sys:*default-database-type* :mysql)
  (establish-connection))

;; レコードの定義
(def-record post)
;; インサート
(save (make-instance 'post :name "名前" :title "タイトル" :content "内容"))
;; 検索
(all post)

ソースはこちらから http://github.com/quek/lisp-on-rails

第2回につづきます

autotest-stumpwm

2010年01月15日
区分
Stumpwm
報告者:
tahara

こんにちは!! tahara です。

script/autospec の結果を表示してくれる Growl がうらやましいのですが、私 の使っているのは Linux です。しかし、Linux には Stumpwm があります。 Stumpwm の stumpish を使えば echo でメッセージを表示することができます。

そこで script/autospec の結果を Stumpwm で表示する gem を作ってみます。

参考サイト

gem を作るには Jeweler を使うのがよさそうです。 Jeweler は GitHub の API を使うようなので、まずその設定を行います。 user_name と api_token には自分の Username と API Token を指定します。

git config --global github.user user_name
git config --global github.token api_token

Jeweler をインストールしてプロジェクトを作ってみます。

gem install jeweler
jeweler --rspec --create-repo autotest-stumpwm

これで綺麗にプロジェクトが作成されます。 おまけに GitHub にリポジトリまでできています。

次に Rakefile を編集します。TODO になっている次の二箇所を適当に編集すればよさそうです。 あと依存する gem も指定しておきます。

    gem.summary = %Q{TODO: one-line summary of your gem}
    gem.description = %Q{TODO: longer description of your gem}
    gem.add_dependency "autotest-growl", ">= 0.1.7"

次に lib の下のファイルに Stumpwm を使って script/autospec の結果を表示するたコードを書きます。 コードが出きたら次のようにリリースします。

rake version:write
rake gemspec
rake release

これだけで Gemcutter にも自動的に公開されます。 Jeweler すばらしい!!

修正したら git commit して、次のいずれかでバージョンをあげ rake release するだけ。

rake version:bump:major              # Bump the gemspec by a major version.
rake version:bump:minor              # Bump the gemspec by a minor version.
rake version:bump:patch              # Bump the gemspec by a patch

できあがった autotest-stumpwm を使うには gem install autotest-stumpwm して、 ~/.autotest に次の一行を書きます。これで autospec と Stumpwm の幸せな生活がおくれます。

require 'autotest/stumpwm'

さて、一番大切な Stumpwm で結果を表示するコードですが、

# -*- coding: undecided -*-
require 'rubygems'
require 'autotest/growl'

module Autotest::Growl
  def self.growl(title, message, icon, priority=0, stick="")
    priority = if priority > 0  # error or feailed
                 1              # red
               elsif priority == -2 # passed
                 2                  # green
               else
                 3              # yellow
               end
    system "stumpish echo ^#{priority}* #{title} #{message}"
  end
end

ごめんなさい autotest-growl にモンキーパッチをあてただけです。

Hunchentoot になりました

2009年12月24日
区分
blog制作
報告者:
tahara

こんにちは。12月からメンバにくわわった tahara と申します。
chiba さんの念願でした、このブログの AllegroServe から Hunchentoot への移行を行いました。
既に chiba さんが Hunchentoot への移植はやってくれていたので Elephant の導入がメイン作業でした。 Elephant は Common Lisp のオブジェクトデータベースです。バックエンドは色々選べるのですが、今回は SQLite3 を使いました。

現在このブログは次のような構成で動いています。

それでは、今後ともよろしくお願いいたします。

技師部隊からの
お知らせ

【求人】エンジニア募集しています。

本頁の来客数
八十七万千百七十六名以上(計測停止中)

メンバー一覧

アクトインディ技師部隊員名簿

アクトインディ技師部元隊員

アクトインディへ

カテゴリー

アクトインディ

aaaa