ImageMagick(Ruby 1.9.3/Passenger 4.0.4/Rails 3.2/rmagick 2.3.11/ImageMagick 6.7.9-10)を使って画像処理をしているプロセス(Passenger)が固まってレスポンスを返さない現象が発生していました。

エラーを返すならわかるのですが、プロセスが固まるとなると、フロントにあるHTTPサーバ(Apache/prefork)も応答できずに固まり、リクエストは次々とくるのでプロセスはどんどん増えていくというとても恐ろしいことになります。

■ やったこと

  • ImageMagickをソースからインストール
  • ghostscriptがインストールされていない
    • yumなどのパッケージ管理ツールからImageMagickをインストールすると依存関係でghostscriptがインストールされる(と思う)
  • rmagickでフォントを使ってテキストを描画

■ エラーメッセージ

1回目は、以下のようなエラーが出ます。2回目は無限ループが発生します。

Magick::ImageMagickError (Postscript delegate failed `/tmp/magick-Zb2G2URF': No such file or directory @ error/ps.c/ReadPSImage/833: `(null)')


■ 解決策とまとめ

ググると、ghostscript関連のエラーのようですので、ghostscriptをインストールしたところ、問題は発生しなくなりました。

  • 当たり前のことですが、ソースからインストールする場合は依存関係に注意(他にもfreetypeを入れ忘れたり・・・)
  • 開発サーバでは、本現象は発生せず(開発サーバにはいろいろ入れるので、ghostscriptもすでに入っていた)

ソースからインストールする前に、yum deplistとかで依存関係をチェックすればこういうミスを減らせるかなぁ・・・。

Posted by あかさた
仕事では、Rails 3.2を主に使っているのですが、よく忘れるのでメモっておきます。
# いまだに、fullpathと書くべきところをrequest_uriとか書いてしまうのですが・・・。

# IP/Path関連
puts request.ip
puts request.remote_ip

# http://192.168.1.1/my/project/1
# にアクセスした場合
puts request.url
# => http://192.168.1.1/my/project/1
puts request.fullpath
# => /my/project/1

# HTTP METHOD関連
puts request.request_method
puts request.get?
puts request.post?
puts request.put?
puts request.delete?


# headersやflashも使いますね。。。

■ 参考

ActionDispatch::Request
http://api.rubyonrails.org/classes/ActionDispatch/Request.html

Posted by あかさた
今作っているサービスでRuby 2.0.0を使っているので、開発サーバのRVMをアップデート(1.16.6 → 1.20.5)して、Ruby 2.0.0を入れてみました。

# 安定板を入れるならrvm get stable
rvm get latest

# 呼ばなくても大丈夫そうですが
rvm reload

# Ruby 2.0.0のインストール
rvm install 2.0.0

# 2.0.0に切り替えて、バージョン確認
rvm 2.0.0
ruby -v
 => ruby 2.0.0p0 (2013-02-24 revision 39474) [x86_64-linux]


いい感じですね!

Posted by あかさた
今更感が途方もなく漂いますが、気付いていなかったので。。。

Ruby 1.8では、レシーバのオブジェクトを取り出すにはブロック引数を使っていましたが、Ruby 1.9ではselfを使って取り出すようです。

class MyClass
  def hello
    puts "Hello World!"
  end
end

MyClass.new.instance_eval do
  hello
end


あまり使わないメソッドですが、最近使う機会があったので。
mrubyでもRuby 1.9と同様の動作でした。

■ 参考


Posted by あかさた
仕事でImageMagickで画像処理をしていたのですが、中間ファイルをいろいろ作る必要があったので、ファイルの掃除も考慮してくれるTempfileを組み合わせて使ってみました。

  • テスト環境
    • Ruby 1.9.3
    • RMagick 2.13.1
    • ImageMagick 6.7.9-10
  • Railsを使っているとアップロードされたファイルはTempfileとして扱われる
    • サイズによってはStringIOとして扱われていた時代もあった気がしますが・・・昔の話のはず
  • 画像処理で中間ファイルを作る場合、ごみを残さないTempfileは便利

require 'RMagick'
require 'tempfile'

# アップロードされたファイル
uploaded_file = params[:file]

# TempfileからImageを作成する
# from_blobはArrayを返すので、shiftして先頭の要素を取得
image = Magick::Image.from_blob(File.read(uploaded_file.path)).shift

# 画像フォーマットの確認
puts image.format

# なんかいろんな処理

# Tempfileの作成
tempfile = Tempfile.open(temp_filename)

# Tempfileへの書き込み
# フォーマットの指定が便利
image.write("png:" + tempfile.path)

# なんかいろんな処理

# Tempfileの削除
tempfile.close!


■ 参考


Posted by あかさた
メソッドのオーバーライドを禁止してみたい(というかjavaでいうところのfinalをやりたい)と思ったので、いろいろ調べてみたところ、クラスの定義に関するフックmethod_addedとinheritedを使うとできそうなので、コードを書いてみました。

  • method_addedでメソッドの定義が行われると、そのメソッドを削除して例外を発生させる
    • 継承してmethod_addedを上書きすると上記チェックを回避できてしまう
  • inheritedで例外を発生させて継承を禁止する
    • inheritedのチェックだけだと、同一クラスの再定義でメソッドを上書きできてしまう

method_addedでremove_methodをすると、上書き前のメソッドも削除されてしまうのが、ちょっと不便ですね。追加前をフックできるといいのですが。。。

module FinalClass
  class << self
    def included(base)
      base.extend ClassMethods
    end
  end
  
  module ClassMethods
    def method_added(name)
      remove_method name
      raise TypeError, "You cannot define a new method!"
    end
    
    def inherited(subklass)
      raise TypeError, "You cannot define a subclass!"
    end
  end
end

class A
  include FinalClass
end


ま、実務で使うかどうかといわれるとわかりませんが・・・。

■ 参考

本記事は上記2記事を足して割った感じです。


freezeの利用も検討しましたが、いろいろと抜け穴ができてしまったので断念しました。

  • クラスをfreezeすると、メソッドの再定義もできなくなる
  • ただし、いくつか抜け道がある
    • 継承するとメソッドのオーバーライドが可能(inheritedでサブクラスをfreezeするアプローチもあるが・・・)
    • クラスをdupするとメソッドのオーバーライドができてしまう(cloneはfreezeを引き継ぐので問題ない)

Posted by あかさた
このブログにAmazonのリンクを貼りたくなったので、ASINを入力するとAmazonから書籍のサムネイルや著者情報などを抜き出す機能を実装するために、Amazon Product Advertising APIを使ってみることにしました。

■ インストール

gemなので、コマンド一発です。

gem install amazon-ecs


■ 基本設定とAPI呼び出し

require 'amazon/ecs'

Amazon::Ecs.options = {
  :associate_tag => '[your associate tag]', 
  :AWS_access_key_id => '[your developer token]', 
  :AWS_secret_key => '[your secret access key]'
}

# API呼び出し
res = Amazon::Ecs.item_lookup("4088767624", :response_group => 'Small, ItemAttributes, Images', :country => 'jp')

# 返ってきたXMLを表示(res.doc.to_sでも多分OK)
puts res.marshal_dump


item_lookupメソッドに限りませんが、response_groupの設定は注意が必要です。Imagesを入れないとサムネイルのURLが、ItemAttributesを入れないと出版日などの情報が取れませんでした。

■ 情報の読込

適当に取り出しています。商品ページのURLがやたらと長くなってDBに入れたくない感じになったので、適当にURLを作ってしまいました。一応、タグが機能しているかAmazonアソシエイトのリンクの動作確認ツールで確認してあります。

amazon_tag = '[your associate tag]'

res.items.each do |item|
  element = item.get_element('ItemAttributes')
  
  data = {
    :asin => item.get('ASIN'), 
    :title => element.get("Title"), 
    :page_url => "http://www.amazon.co.jp/dp/#{item.get('ASIN')}?tag=#{amazon_tag}", 
    :isbn => element.get("ISBN"), 
    :author => element.get_array("Author").join(", "), 
    :product_group => element.get("ProductGroup"), 
    :manufacturer => element.get("Manufacturer"), 
    :publication_date => element.get("PublicationDate"), 
    
    # URL, Width, Heightの要素を持っている
    :small_image => item.get_hash("SmallImage"), 
    :medium_image => item.get_hash("MediumImage"), 
    :large_image => item.get_hash("LargeImage")
  }
end

# dataを表示する(省略)


■ 結果

以下のような感じで表示してみました。皇国の守護者ですな。古い作品ですが、なんで打ち切りになってしまったのか・・・。この作家が今描いているシュトヘルも面白いんだけど、このころの勢いにはなかなか追いつかないしなぁ・・・。

[書籍データの取得に失敗しました]

■ 感想

プログラム的に難しい個所はありませんが、Amazon Product Advertising APIをある程度把握しておく必要があるでしょう。

APIの呼び出し回数の制限(1時間2000回? 収益による増減もあるので少し難しい)を考慮してキャッシュしたり、ProductGroupによってとれる情報が異なる(例:本では著者が取れるが、ゲームでは取れないなど)ので、そういった差異を吸収するような表示を検討したりと、ちゃんとしたものを作る場合はそれなりに手間がかかるかと思われます。

■ 参考情報


Posted by あかさた
複数のアプリから参照するライブラリを書きたくなったので、勉強がてらgemを作ってみることにしました。gemテンプレートを生成して、処理を記述して、rspecでテストを書くところまでを書いています。

■ テンプレートの生成とgemspecの記述

以下でテンプレートの生成ができます。

bundle gem hello-akasata


hello-akasata.gemspecの内容です。TODOと書いてあるところを書き換えましょう。

# -*- encoding: utf-8 -*-
require File.expand_path('../lib/hello-akasata/version', __FILE__)

Gem::Specification.new do |gem|
  gem.authors       = ["akasata"]
  gem.email         = ["*@*.*"]
  gem.description   = %q{Test Gem.}
  gem.summary       = %q{Hello World!}
  gem.homepage      = ""

  gem.files         = `git ls-files`.split($\)
  gem.executables   = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
  gem.test_files    = gem.files.grep(%r{^(test|spec|features)/})
  gem.name          = "hello-akasata"
  gem.require_paths = ["lib"]
  gem.version       = Hello::Akasata::VERSION
  
  # 依存するgemがある場合
  # gem.add_dependency('log4r', '>= 1.0.5')
  gem.add_development_dependency('rspec')
end


lib/hello-akasata/version.rbの内容は以下の通りです。内容を変更したらここを変更しましょう。

module Hello
  module Akasata
    VERSION = "0.0.1"
  end
end


■ 処理の記述

lib/hello-akasata.rbの内容を書き換えてください。とりあえず、hello worldです。

require "hello-akasata/version"

module Hello
  module Akasata
    def self.hello
      "Hello akasata World!"
    end
  end
end


■ ビルド

rakeでビルドを行うことができます。

rake build # ビルドします
rake install # ビルドしてシステムにインストールします
rake release # これでRubyGems.orgに公開されます。今回はやってませんw


■ rspec

spec/spec_helper.rb

require 'rubygems'
require 'bundler/setup'
require 'hello-akasata'

RSpec.configure do |config|
  config.mock_framework = :rspec
end


spec/hello-akasata_spec.rb

require 'spec_helper'

describe Hello::Akasata, "::hello" do
  it "should return 'Hello akasata World!'" do
    Hello::Akasata.hello.should eq("Hello akasata World!")
  end
end


以下、rspecの実行です。

rspec


■ 参考文献

ASCIIcasts - “Episode 245 - Bundlerでgemを作る”
testing - Setup RSpec to test a gem (not Rails) - Stack Overflow

Posted by あかさた
Ruby(Rails)からメールを送信する必要のある仕事があったので、本採用するかは決めていませんが、Amazon SESをaws-sdk-rubyから使う方法について調査してみました。

■ 準備

1. マネジメントコンソールでEmailアドレスとドメインを登録してください
(ドメインの登録の際は、DKIMの設定も出力すると便利です。Easy DKIMという機能なのですが、ドメインの認証が完了した後、マネジメントコンソールで有効化すると、送信メールにDKIM署名を追加してくれます。迷惑メール扱いになる確率がいくらか減るでしょう。)

※ Request Production Accessするまでは、送信元、送信先ともに登録が必要です。

2. aws-sdk-rubyをインストールする

gem install aws-sdk


■ 手順

単純なので一気に書きます。

# encoding: utf-8
require 'rubygems'
require 'aws-sdk'
require 'nkf'

ses = AWS::SimpleEmailService.new(
  :access_key_id => 'YOUR_ACCESS_KEY_ID',
  :secret_access_key => 'YOUR_SECRET_ACCESS_KEY')

body_text = NKF.nkf '-jw', <<-EOS
こんにちは。
このメールはテストメールです。

おしまい
EOS

subject = NKF.nkf('-Mw', 'サブジェクトです')

from_email = "FROM_EMAIL_ADDRESS"
to_email = "TO_EMAIL_ADDRESS"

p "Verified?(FROM Email): " + ses.identities[from_email].verified?.to_s
p "Verified?(TO Email): " + ses.identities[to_email].verified?.to_s
p "Verified?(Domain): " + ses.identities['YOUR_DOMAIN'].verified?.to_s

ses.send_email(
  :subject => subject,
  :to => to_email,
  :from => from_email,
  :body_text => body_text,
  :body_text_charset => 'ISO-2022-JP'
)


■ 参考情報

RubyからAmazon SESでメールを送る - プログラマになりたい

詳しいです。ただし、Easy DKIMに関する記載が無い(この記事時点ではEasy DKIMがなかったのかも)ので、DKIM関係は独自署名の方法になっています。これはこれで押さえておいた方がいい情報ですが、Easy DKIMを使う方が楽ではあると思います。

AWS SDK for RubyのAPIリファレンス
AmazonSES-Easy DKIM設定サポート資料

Posted by あかさた
仕事でRubyでAmazon S3にアクセスする必要があったので、公式のgemであるaws-sdkを使ってみました。このgemはS3だけではなく、一通りのサービスにアクセスできるようなので、使いこなせばかなり有用かと思われます。

この記事では、S3へのファイルのアップロードを中心に以下の内容を扱っています。

・インストール
・基本設定
・ファイルのアップロード
・ファイルの暗号化
・アクセスコントロールの設定

■ インストール

gem install aws-sdk


■ 基本設定

アクセスキーを指定して、S3のインスタンスを生成します。

require 'aws-sdk'

AWS.config(
  :access_key_id => 'YOUR_ACCESS_KEY_ID',
  :secret_access_key => 'YOUR_SECRET_ACCESS_KEY')

s3 = AWS::S3.new

# 以下も可
# s3 = AWS::S3.new(
#   :access_key_id => 'YOUR_ACCESS_KEY_ID',
#   :secret_access_key => 'YOUR_SECRET_ACCESS_KEY')


■ ファイルのアップロード

ファイルをS3に格納します。

bucket = s3.buckets['yourbucketname']

filename = "fullpath/filename.ext"
basename = File.basename(filename)
o = bucket.objects[basename]
o.write(:file => filename)


■ ファイルの暗号化(クライアントサイド)

サーバーサイド(S3)でもできますが、とりあえずクライアントサイトでも簡単なので紹介します。

# キーの生成
my_key = OpenSSL::Cipher.new("AES-256-ECB").random_key

# オブジェクトの取得
o = bucket.objects[name]

# 暗号化して書込
o.write("MY TEXT", :encryption_key => my_key)

# 復号化して読込
puts o.read(:encryption_key => my_key)


公開鍵暗号を使いたいなら以下の通り。公開鍵で暗号化して、秘密鍵で復号化します。

# キーの生成
my_key = OpenSSL::PKey::RSA.new(1024)

# オブジェクトの取得
o = bucket.objects[name]

# 暗号化して書込
o.write("MY TEXT", :encryption_key => my_key)

# 復号化して読込
puts o.read(:encryption_key => my_key)


■ アクセスコントロールの設定

S3Objectクラスのacl=メソッドを使うと、アクセスコントロールを設定してS3上のオブジェクトに反映することができます。

o = bucket.objects[name]

# Canned ACL
o.acl = :public_read

# writeメソッドに渡すことも可能
o.write(:file => filename, :acl => :public_read)


以下、渡せるアクセスコントロールポリシーです。

  • :private
  • :public_read
  • :public_read_write
  • :authenticated_read
  • :bucket_owner_read
  • :bucket_owner_full_control

acl=には、AccessControlListを代入することもできます。changeメソッドを使ったやり方もあります。

o.acl.change do |acl|
  acl.grant(:full_control).to(:canonical_user_id => "...")
end


S3ObjectのchangeメソッドはS3Objectが返すAccessControlListのインスタンスが持つ特異メソッドなので、注意してください。ブロックの処理が完了すると、S3上のオブジェクトに反映します。(具体的にはS3Objectのacl=メソッドを呼び出します。)

ACLに関しては以下を参照してください。

acl=メソッド(AWS::S3::S3Object)
http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/S3/S3Object.html#acl%3D-instance_method

AWS::S3::AccessControlList
http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/S3/AccessControlList.html

■ 参考情報

Getting Started with the AWS SDK for Ruby(AWS)
AWS SDK for RubyのAPIリファレンス
aws/aws-sdk-ruby(GitHub)
aws-sdkのファイルアップロードのサンプル
AWS SDK for RubyでS3の操作をする - プログラマになりたい(ファイルの取得関係が詳しい)

Posted by あかさた