デプロイ時の Unicorn リスタートが失敗する件

平成24年1月30日(月) 16時14分22秒
区分
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 する場合は、 新しいプロセスのワーカが動き出すまでリクエストは待たされるので、 まだこちらの方がいいんじゃないかしらん、というところです。

>View Comments          このページの上へ戻る

Rails3 への移行

平成24年1月24日(火) 23時01分53秒
区分
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 環境ではどんどんメモリをくっていきます。 これもなぜでしょう。。。

>View Comments          このページの上へ戻る

Ruby で Picasa

平成22年10月1日(金) 10時11分03秒
区分
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

>View Comments          このページの上へ戻る

Ruby で Google Analytics API

平成22年7月9日(金) 11時41分18秒
区分
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 の設定をいろいろかえると面白いことができるかもしれません。

>View Comments          このページの上へ戻る

Lisp on Rails 第9回 〜 ビュー

平成22年6月5日(土) 17時15分45秒
区分
Lisp on Rails
報告者:
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回につづきます。

>View Comments          このページの上へ戻る

Apache で特定の User-Agent だけ BASIC 認証をバイパスする方法

平成22年5月14日(金) 10時50分16秒
区分
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 に補足されることもなくなるはずです。

>View Comments          このページの上へ戻る

関連する単語

平成22年4月24日(土) 18時38分25秒
区分
集合知
報告者:
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))

さて、この出力を利用することができるかどうかがまた問題です。

>View Comments          このページの上へ戻る

Lisp on Rails 第8回 〜 before_*

平成22年4月10日(土) 18時42分58秒
区分
Lisp on Rails
報告者:
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回につづきます。

>View Comments          このページの上へ戻る

Lisp on Rails 第7回 〜 ActiveRecord::Base の find メソッド

平成22年3月27日(土) 07時20分34秒
区分
Lisp on Rails
報告者:
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回につづきます。

>View Comments          このページの上へ戻る

Lisp on Rails 第6回 〜 ここらでリファクタリング

平成22年3月19日(金) 12時10分44秒
区分
Lisp on Rails
報告者:
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回につづきます。

>View Comments          このページの上へ戻る

技師部隊からの
お知らせ

エンジニア募集 しています。

本頁の来客数
九万八千五百二十一名

メンバー一覧

アクトインディ技師部隊員名簿

アクトインディ技師部元隊員

アクトインディへ

投稿する

カテゴリー

アクトインディ

aaaa