LibreOffice の力で PDF エクスポートする librepdf
現在大きな仕事を終えて、今は自身の知識の棚おろし中・・ノウハウの結晶化をしておきたいという個人的な動機から librepdf なんていう gem を公開しました。
https://github.com/hamajyotan/librepdf
ざっくりいうと、 LibreOffice の機能を用いて各種ファイルから PDF エクスポートできるというシロモノで、 MS Office 文書なんかも OK です。JRuby 専用、LibreOffice の UNO API を Java で叩いて、それを Ruby から使えるようにしています。まだまだまだ不安定ですが、きっと開示したほうがハッピーだなと思った次第です。
インストール
$ gem install librepdf
事前に LibreOffice をサービスとして起動する
$ /opt/libreoffice3.5/program/soffice.bin \ --accept="socket,host=127.0.0.1,port=8100,tcpNoDelay=1;urp;" \ --headless \ --invisible \ --nodefault \ --nofirststartwizard \ --nolockcheck \ --nologo \ --norestore
起動したら 8100 ポートのリスンを確認
$ netstat -ant | grep 8100 tcp 0 0 127.0.0.1:8100 0.0.0.0:* LISTEN
使い方サンプル
/home/hamajyotan/test.doc から、 /home/hamajyotan/test.pdf を作成
require 'rubygems' require 'librepdf' con = Connection.new "host" => "127.0.0.1", "port" => 8100 doc = con.load "file:///home/hamajyotan/test.doc" doc.convert_pdf "file:///home/hamajyotan/test.pdf" doc.close con.close
上記の後処理省力化的な書き方版
require 'rubygems' require 'librepdf' Librepdf::Connection.new "host" => "127.0.0.1", "port" => 8100 do |con| con.load "file:///home/hamajyotan/test.doc" do |doc| doc.convert_pdf "file:///home/hamajyotan/test.pdf" end end
僕はブレースの方が好み
require 'rubygems' require 'librepdf' Librepdf::Connection.new("host" => "127.0.0.1", "port" => 8100) { |con| con.load("file:///home/hamajyotan/test.doc") { |doc| doc.convert_pdf "file:///home/hamajyotan/test.pdf" } }
パスワード付きファイルを開く
require 'rubygems' require 'librepdf' Librepdf::Connection.new("host" => "127.0.0.1", "port" => 8100) { |con| con.load("file:///home/hamajyotan/test.doc", "Password" => "SECRETPASSWORD") { |doc| doc.convert_pdf "file:///home/hamajyotan/test.pdf" } }
ページを指定して PDF エクスポート
ページ範囲外の指定は例外が投げられます。
require 'rubygems' require 'librepdf' Librepdf::Connection.new("host" => "127.0.0.1", "port" => 8100) { |con| con.load("file:///home/hamajyotan/test.doc") { |doc| doc.convert_pdf "file:///home/hamajyotan/test1.pdf", "FilterData" => { "PageRange" => "1-1" } doc.convert_pdf "file:///home/hamajyotan/test2.pdf", "FilterData" => { "PageRange" => "2-2" } doc.convert_pdf "file:///home/hamajyotan/test3.pdf", "FilterData" => { "PageRange" => "3-3" } } }
クラスの説明
Librepdf::Connection
LibreOffice への接続を担う
- #initialize options = {}
- options ハッシュへは、 "host" と "port" が受理可能
- ブロックを渡すと、ブロック引数に自身のインスタンスを渡してブロック終了時にクローズする
- #closed?
- 接続を閉じていれば true, そうでなければ false
- #close
- 接続を閉じる
- #load input_url, options = {}
- ドキュメントをロードする
- ブロックを渡すと、ブロック引数にロードしたドキュメントのインスタンスを渡してブロック終了時にクローズする
- options ハッシュへは、 UNO の loadComponentFromURL の第4引数に渡すプロパティの一部が設定可能 ※
- ※ http://www.openoffice.org/api/docs/common/ref/com/sun/star/document/MediaDescriptor.html
Librepdf::Document
LibreOffice にロードされたドキュメントを示す、Librepdf::Connection#load の返り値として得る以外にインスタンス生成方法は無い。
正確には、このクラスのインスタンスではなく、ドキュメント種類に応じたサブクラスのインスタンスが生成される。例えば Librepdf::Document::Calc など。
- #closed?
- 接続を閉じていれば true, そうでなければ false
- #close
- 接続を閉じる
- #convert_pdf output_url, options = {}
- PDF エクスポートを行う
- options ハッシュへは、 UNO の storeToURL の第2引数に渡すプロパティの一部が設定可能
今後やりたいこと
- 例外が NativeException に丸められるのでどうにかする。
- LibreOffice を複数インスタンス起動している環境での接続プール
- ドキュメントのロード時に LibreOffice がブロックするのでマルチインスタンスはそれなりに有用
CentOS に gem kyotocabinet-ruby を入れる
導入した環境
$ cat /etc/redhat-release CentOS release 5.5 (Final) $ ruby -v ruby 1.8.6 (2010-02-05 patchlevel 399) [i686-linux] $ gem -v 1.3.7
新しめの gcc が必要
gcc 4.1 ではダメ
$ gcc --version gcc (GCC) 4.1.2 20080704 (Red Hat 4.1.2-50) Copyright (C) 2006 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
故に gcc44 をインストール
$ sudo yum -y install gcc44 gcc44-c++
kyotocabinet をインストール
$ wget http://fallabs.com/kyotocabinet/pkg/kyotocabinet-1.2.71.tar.gz $ tar zvxf kyotocabinet-1.2.71.tar.gz $ cd kyotocabinet-1.2.71 $ CC=gcc44 CXX=g++44 ./configure && make && sudo make install
(場合によっては) /usr/local/lib ディレクトリを ldconfig
$ sudo sh -c "echo /usr/local/lib > /etc/ld.so.conf.d/local.conf" $ sudo /sbin/ldconfig
gem kyotocabinet-ruby のインストール ⇒ 失敗
$ sudo gem install kyotocabinet-ruby Building native extensions. This could take a while... ERROR: Error installing kyotocabinet-ruby: ERROR: Failed to build gem native extension. /usr/local/bin/ruby extconf.rb setting variables ... $CFLAGS = -I. -I/usr/local/include -Wall -g -O2 -O2 $LDFLAGS = -L. -rdynamic -Wl,-export-dynamic -L. -L/usr/local/lib $libs = -lkyotocabinet -lz -lstdc++ -lrt -lpthread -lm -lc checking for kccommon.h... yes creating Makefile make g++ -I. -I/usr/local/lib/ruby/1.8/i686-linux -I/usr/local/lib/ruby/1.8/i686-linux -I. -DHAVE_KCCOMMON_H -D_FILE_OFFSET_BITS=64 -fPIC -I. -I/usr/local/include -Wall -g -O2 -O2 -c kyotocabinet.cc kyotocabinet.cc: In function ‘VALUE db_increment(int, VALUE*, VALUE)’: kyotocabinet.cc:2501: 警告: converting to ‘int64_t’ from ‘double’ kyotocabinet.cc:2531: 警告: converting to ‘int64_t’ from ‘double’ kyotocabinet.cc:2547: 警告: converting to ‘int64_t’ from ‘double’ kyotocabinet.cc: In function ‘VALUE db_match_prefix(int, VALUE*, VALUE)’: kyotocabinet.cc:3587: 警告: converting to ‘int64_t’ from ‘double’ kyotocabinet.cc: In function ‘VALUE db_match_regex(int, VALUE*, VALUE)’: kyotocabinet.cc:3663: 警告: converting to ‘int64_t’ from ‘double’ /usr/local/include/kcmap.h: At global scope: # # 途中省略... # Gem files will remain installed in /usr/local/lib/ruby/gems/1.8/gems/kyotocabinet-ruby-1.27.1 for inspection. Results logged to /usr/local/lib/ruby/gems/1.8/gems/kyotocabinet-ruby-1.27.1/gem_make.out
gem kyotocabinet-ruby のインストール ⇒ 成功
gem install の際にも gcc44 を用いてコンパイルする
$ sudo CC=gcc44 CXX=g++44 gem install kyotocabinet-ruby Building native extensions. This could take a while... Successfully installed kyotocabinet-ruby-1.27.1 1 gem installed
成功!
動作確認
$ irb irb(main):001:0> require 'rubygems'; require 'kyotocabinet' => true irb(main):002:0> KyotoCabinet => KyotoCabinet
ちゃんと見えてるっぽい
C++ で Ruby 拡張ライブラリ ( Data_Wrap_Struct とか )
とりあえず以下ダラダラのアウトプットはこちら
gist に置いといた https://gist.github.com/1437782
作りたいもの
- 僕は神になる。ヒト(クラス)を作ってみるぞ
- 人とは、名前と年齢を持っている存在である
- 人とは、名前と年齢を伝える為、ちゃんとご挨拶ができる存在である
作り方とか
C++ でヒトクラスを作る
human.hxx
/* * human.hxx */ #ifndef _INCLUDE_HUMAN_H_ #define _INCLUDE_HUMAN_H_ #include <string> #include <sstream> class Human { private: static const int _signature = 0x123f3f7c; public: Human(const char* name, int age); virtual ~Human() {} bool isLegal() const { return this->_sig == _signature; } std::string toString() const; std::string greet() const; private: int _sig; std::string _name; int _age; }; #endif // _INCLUDE_HUMAN_H_
human.cxx
/* * human.cxx */ #include "human.hxx" Human::Human(const char* name, int age) { this->_sig = _signature; this->_name = name; this->_age = age; } std::string Human::toString() const { std::ostringstream ret; ret << "Human [name=" << this->_name << ", age=" << this->_age << "]"; return ret.str(); } std::string Human::greet() const { std::ostringstream ret; ret << "My name is " << this->_name << ". I'm " << this->_age << " years old."; return ret.str(); }
C++ 製ヒトクラスはこんな感じで使える
#include <iostream> #include "human.hxx" void show(Human* p) { std::cout << p << std::endl; if (p->isLegal()) { // 正しいオブジェクトの場合のみ出力する std::cout << "\t" << p->toString() << std::endl << "\t" << p->greet() << std::endl; } } int main() { Human h("hamajyotan", 18); // ごく普通にインスタンス生成 show(&h); Human* p; // 初期化されていないポインタを与える show(p); // この場合、アドレスのみしか出力されない p = new Human("foo", 20); // new でインスタンス生成 show(p); delete p; return 0; }
実行結果例。 2回目の show 呼び出しではアドレスしか出力されていない
$ ./a.out 0xbfcc104c Human [name=hamajyotan, age=18] My name is hamajyotan. I'm 18 years old. 0xbfcc1078 0x8d00028 Human [name=foo, age=20] My name is foo. I'm 20 years old.
C++ 製ヒトクラスを元に Ruby 拡張ライブラリを書く
humanwrap.cxx
/** * humanwrap.cxx */ #include <new> // for replacement new #include "ruby.h" #include "human.hxx" /** * Ruby 製 Human クラスのインスタンスから * C++ 製 Human クラスのインスタンスのために割り当てたメモリ領域のポインタを得ます. */ static Human* getHuman(VALUE self) { Human* p; Data_Get_Struct(self, Human, p); return p; } /** * GC によりメモリが解放される際に呼び出されます. * * wrap_Human_init で replacement new を使い * C++ 製 Human の初期化を実施するため明示的なデストラクタ呼び出しが必要になります。 * * C++ 製クラスのためにメモリ領域が割り当てられたにも関わらず、 * #initialize の引数が不正だった場合など、 C++ 製 Human のインスタンスが作成されない経路も存在します * この場合ポインタは不正なアドレスを指しているため、デストラクタを呼び出さないようにします。 */ static void wrap_Human_free(Human* p) { if (p->isLegal()) p->~Human(); ruby_xfree(p); } /** * 初期化時にメモリを割り当てます. */ static VALUE wrap_Human_alloc(VALUE klass) { // Human インスタンスのためのメモリ空間を確保し self に関連付けます // 更に、 GC が実行された際のハンドラも登録します return Data_Wrap_Struct(klass, NULL, wrap_Human_free, ruby_xmalloc(sizeof(Human))); } /** * */ static VALUE wrap_Human_init(VALUE self, VALUE _name, VALUE _age) { // 以下 2行で #initialize に渡された引数の検証を行います // 引数が正しくない場合、例外が投げられるために C++ 製 Human クラスは初期化されません。 // // この場合も GC の際に wrap_Human_free が呼ばれますが、 // wrap_Human_free に渡されるポインタは初期化されていないために不正なアドレスを指します const char* name = StringValuePtr(_name); // 文字列でなければ例外が投げられます int age = NUM2INT(_age); // 数値でなければ例外が投げられます Human* p = getHuman(self); // メモリ領域を得ます new (p) Human(name, age); // replacement new により既存のメモリ空間にインスタンスを初期化します return Qnil; // 使われません、適当な値を返しておきます } /** * toString() を実行して結果を Ruby の String クラスに変換して返します. */ static VALUE wrap_Human_toString(VALUE self) { return rb_str_new2(getHuman(self)->toString().c_str()); } /** * greet() を実行して結果を Ruby の String クラスに変換して返します. */ static VALUE wrap_Human_greet(VALUE self) { return rb_str_new2(getHuman(self)->greet().c_str()); } /** * require 'human' の際に最初に実行されます. */ extern "C" void Init_human() { // Ruby の Human クラスを定義する VALUE c = rb_define_class("Human", rb_cObject); rb_define_alloc_func(c, wrap_Human_alloc); // 初期化時にメモリを確保する関数を登録する rb_define_private_method(c, "initialize", RUBY_METHOD_FUNC(wrap_Human_init), 2); // #initialize rb_define_method(c, "inspect", RUBY_METHOD_FUNC(wrap_Human_toString), 0); // #inspect rb_define_method(c, "to_s", RUBY_METHOD_FUNC(wrap_Human_toString), 0); // #to_s rb_define_method(c, "greet", RUBY_METHOD_FUNC(wrap_Human_greet), 0); // #greet }
extconf.rb
require "mkmf" $libs += " -lstdc++ " # for STL create_makefile("human")
拡張ライブラリを作成してみる
特定のディレクトリに 4 ファイルを集めて ruby extconf.rb
そして make
human.so が出来上がる。
$ ls /home/hamajyotan/tmp/ extconf.rb human.cxx human.hxx humanwrap.cxx $ ruby extconf.rb creating Makefile $ make g++ -I. -I/usr/local/include/ruby-1.9.1/i686-linux -I/usr/local/include/ruby-1.9.1/ruby/backward -I/usr/local/include/ruby-1.9.1 -I. -D_FILE_OFFSET_BITS=64 -fPIC -O3 -ggdb -Wextra -Wno-unused-parameter -Wno-parentheses -Wpointer-arith -Wwrite-strings -Wno-missing-field-initializers -Wno-long-long -o humanwrap.o -c humanwrap.cxx g++ -I. -I/usr/local/include/ruby-1.9.1/i686-linux -I/usr/local/include/ruby-1.9.1/ruby/backward -I/usr/local/include/ruby-1.9.1 -I. -D_FILE_OFFSET_BITS=64 -fPIC -O3 -ggdb -Wextra -Wno-unused-parameter -Wno-parentheses -Wpointer-arith -Wwrite-strings -Wno-missing-field-initializers -Wno-long-long -o human.o -c human.cxx g++ -shared -o human.so humanwrap.o human.o -L. -L/usr/local/lib -Wl,-R/usr/local/lib -L. -rdynamic -Wl,-export-dynamic -lstdc++ -lpthread -lrt -ldl -lcrypt -lm -lc $ ls Makefile extconf.rb human.cxx human.hxx human.o human.so humanwrap.cxx humanwrap.o $
reliable-msg を Ruby 1.9 系でも動くようにする
やりたいこと
タイトルの通り
reliable-msg を Ruby 1.9 系でも動くようにしたい
git でソースを作者様リポジトリから取ってくる
[hamajyotan@host ~]$ git clone https://github.com/assaf/reliable-msg.git Cloning into reliable-msg... remote: Counting objects: 129, done. remote: Compressing objects: 100% (63/63), done. remote: Total 129 (delta 67), reused 121 (delta 63) Receiving objects: 100% (129/129), 45.14 KiB, done. Resolving deltas: 100% (67/67), done. [hamajyotan@host ~]$
とりあえず Ruby 1.9 環境で rake テスト
[hamajyotan@host ~]$ cd reliable-msg [hamajyotan@host reliable-msg]$ ruby -v ruby 1.9.2p0 (2010-08-18 revision 29036) [i686-linux] [hamajyotan@host reliable-msg]$ rake (in /home/sakaguchi/reliable-msg) /usr/local/bin/ruby -w -I"lib" "/usr/local/lib/ruby/1.9.1/rake/rake_test_loader.rb" "test/test-rails.rb" "test/test-queue.rb" "test/test-topic.rb" /home/sakaguchi/reliable-msg/lib/reliable-msg/queue.rb: /home/sakaguchi/reliable-msg/lib/reliable-msg/queue.rb:332: Invalid retry (SyntaxError) rake aborted! Command failed with status (1): [/usr/local/bin/ruby -w -I"lib" "/usr/local...] /usr/local/lib/ruby/1.9.1/rake.rb:993:in `block in sh' /usr/local/lib/ruby/1.9.1/rake.rb:1008:in `call' /usr/local/lib/ruby/1.9.1/rake.rb:1008:in `sh' # # 途中省略。。 # /usr/local/lib/ruby/1.9.1/rake.rb:2013:in `top_level' /usr/local/lib/ruby/1.9.1/rake.rb:1992:in `run' /usr/local/bin/rake:31:in `<main>' [hamajyotan@host reliable-msg]$
構文エラーのようだ。
色々改修していく
lib/reliable-msg/queue.rb 332行目
retry が使われている
def next selector, &block load = false @mutex.synchronize do load ||= (@list.nil? || @list.empty?) @list = block.call() if load @list.each_with_index do |headers, idx| if selector.match headers @list.delete_at idx return headers[:id] end end unless load load = true retry end end return nil end
これは Ruby 1.9 ではダメ、代わりに redo を使いたいところ
#next メソッドが実行されている箇所を確認する
lib/reliable-msg/queue.rb 249行目 付近
# Validate the selector: nil, string or hash. selector = case selector when String {:id=>selector} when Hash, Selector, nil selector else raise ArgumentError, ERROR_INVALID_SELECTOR end # If using selector object, obtain a list of all message headers # for the queue (shared by all Queue/Selector objects accessing # the same queue) and run the selector on that list. Pick one # message and switch to an :id selector to retrieve it. if selector.is_a?(Selector) cached = @@headers_cache[@queue] ||= CachedHeaders.new id = cached.next(selector) do if tx tx[:qm].list :queue=>@queue, :tid=>tx[:tid] else repeated { |qm| qm.list :queue=>@queue } end end return nil unless id selector = {:id=>id} end
#next メソッドを呼んでいるレシーバの cached 更に、引数の selector
これらは共に変数なので再評価されても副作用はない。
よって、 retry をそのまま redo に変更しても問題なさそう。
参考 ⇒ http://d.hatena.ne.jp/hamajyotan/20110410/1302409054
lib/reliable-msg/client.rb 92行目と100行目
Selector.new &block
これは
Selector.new(&block)
こうする。
lib/reliable-msg/selector.rb 70行目
メタプログラミング Ruby 曰く、ブランクスレートしてるコード
instance_methods.each { |name| undef_method name unless name =~ /^(__.*__)|instance_eval$/ }
object_id を undef しちゃダメとの警告、故に例外に加える
instance_methods.each { |name| undef_method name unless name =~ /^(__.*__)|instance_eval|object_id$/ }
test/test-queue.rb 123, 131, 146, 160 行目
begin @queue.get do |msg| assert msg && msg.id == id1, "Block called without the message" raise AbortTransaction end flunk "Message not found in queue, or exception not propagated" rescue AbortTransaction end
ブロック外の変数 msg がブロック引数 msg とかぶっている
begin @queue.get do |m| assert m && m.id == id1, "Block called without the message" raise AbortTransaction end flunk "Message not found in queue, or exception not propagated" rescue AbortTransaction end
lib/reliable-msg/cli.rb 197行目
else raise InvalidUsage end rescue InvalidUsage puts USAGE end end
インデントがズレているゆえの警告
else raise InvalidUsage end rescue InvalidUsage puts USAGE end end
test/test-queue.rb 29行目
require 'lib/reliable-msg'
require できないので修正、他のテストスクリプトと合わせる。
require 'reliable-msg'
lib/queue-manager.rb 187, 191, 194行目
return if @@active == self Thread.critical = true if @@active.nil? @@active = self else Thread.critical = false raise RuntimeError, ERROR_QM_STARTED end Thread.critical = false
Thread.critical は無くなった
return if @@active == self # Thread.critical = true if @@active.nil? @@active = self else # Thread.critical = false raise RuntimeError, ERROR_QM_STARTED end # Thread.critical = false
lib/reliable-msg/queue.rb 70行目あたり
def initialize(queue = nil, options = nil) @priority, @expires, @max_deliveries, @delivery = nil # 追加
初期化されないインスタンス変数の参照警告に対する対応
端折るが、同警告の対応をちらほらと。
ハマりポイント
ブロック引数とブロック外引数が同名である場合の挙動
Ruby 1.8 系
irb(main):001:0> RUBY_VERSION => "1.8.6" irb(main):002:0> x = 5 => 5 irb(main):003:0> [1, 2, 3].each { |x| irb(main):004:1* puts x irb(main):005:1> } 1 2 3 => [1, 2, 3] irb(main):006:0> x => 3
Ruby 1.9 系
irb(main):001:0> RUBY_VERSION => "1.9.2" irb(main):002:0> x = 5 => 5 irb(main):003:0> [1, 2, 3].each { |x| irb(main):004:1* puts x irb(main):005:1> } 1 2 3 => [1, 2, 3] irb(main):006:0> x => 5
Ruby 1.8 系では、外側の引数へ影響を与え、 Ruby 1.9 系では、外側の引数はシャドーされる。
ReliableMsg で1箇所、 Ruby 1.8 系の挙動を期待するコードがあった。
改修後、 Ruby 1.9 系でテスト実行
[hamajyotan@host reliable-msg]$ ruby -v ruby 1.9.2p0 (2010-08-18 revision 29036) [i686-linux] [hamajyotan@host reliable-msg]$ rake (in /home/sakaguchi/reliable-msg) /usr/local/bin/ruby -w -I"lib" "/usr/local/lib/ruby/1.9.1/rake/rake_test_loader.rb" "test/test-rails.rb" "test/test-queue.rb" "test/test-topic.rb" Loaded suite /usr/local/lib/ruby/1.9.1/rake/rake_test_loader Started I, [2011-04-18T01:16:06.721649 #12139] INFO -- : Loaded queues configuration from: /home/sakaguchi/reliable-msg/queues.cfg I, [2011-04-18T01:16:06.721784 #12139] INFO -- : Using message store: disk I, [2011-04-18T01:16:06.723568 #12139] INFO -- : Accepting requests at: druby://localhost:6438 # # 途中省略。。 # I, [2011-04-18T01:16:25.856555 #12139] INFO -- : Stopped queue manager at: druby://localhost:6438 I, [2011-04-18T01:16:25.856668 #12139] INFO -- : Using message store: disk I, [2011-04-18T01:16:25.857284 #12139] INFO -- : Accepting requests at: druby://localhost:6438 I, [2011-04-18T01:16:25.857772 #12139] INFO -- : Stopped queue manager at: druby://localhost:6438 . Finished in 19.137064 seconds. 11 tests, 97 assertions, 0 failures, 0 errors, 0 skips Test run options: --seed 37415 [hamajyotan@host reliable-msg]$
すべてテストをパスした!!更に警告無し!!
gem 化して確認
gem 化
[hamajyotan@host reliable-msg]$ gem build reliable-msg.gemspec WARNING: bin/queues is missing #! line Successfully built RubyGem Name: reliable-msg Version: 1.2.0 File: reliable-msg-1.2.0.gem
インストール
[hamajyotan@host reliable-msg]$ sudo gem install reliable-msg-1.2.0.gem Successfully installed reliable-msg-1.2.0 1 gem installed
キューマネージャ起動、終了
[hamajyotan@host reliable-msg]$ queues manager start I, [2011-04-18T01:19:04.841320 #12398] INFO -- : Loaded queues configuration from: /home/sakaguchi/reliable-msg/queues.cfg I, [2011-04-18T01:19:04.841743 #12398] INFO -- : Using message store: disk I, [2011-04-18T01:19:04.845010 #12398] INFO -- : Accepting requests at: druby://localhost:6438 # ここで Ctrl+C ^CI, [2011-04-18T01:19:06.555624 #12398] INFO -- : Stopped queue manager at: druby://localhost:6438 [hamajyotan@host reliable-msg]$
動いた!!
とりあえず
fork して作業成果を配置。
https://github.com/hamajyotan/reliable-msg
ReliableMsgAgent で Ap4r と同様のディスパッチを実行する
ReliableMsgAgent で「ap4r と同様のメッセージディスパッチ」をする
- ap4r の I/F を用いてメッセージを put する
- put したメッセージは、 ap4r にディスパッチさせずに reliable-msg-agent に自律的に取得させる。要するに pull 型アプローチをとる ap4r のようなもの
- その際のディスパッチ方法として、 ap4r と同様のルールに則る。
ap4r の準備
以下、ap4r の作業ディレクトリを /home/hamajyotan/ap4r/ と仮定
ap4r のインストール
$ gem install ap4r --no-ri --no-rdoc
activesupport のインストール。 ap4r 初期化に必要
$ gem install activesupport -v "<3.0.0" --no-ri --no-rdoc
$ cd /home/hamajyotan/ap4r/ $ ap4r_setup . make application root directory [/home/hamajyotan/ap4r] ... make directories for AP4R [config, log, public, script, tmp] ... copy files from $GEM_HOME/gems/ap4r-0.3.7/config to /home/hamajyotan/ap4r/config ... copy files from $GEM_HOME/gems/ap4r-0.3.7/script to /home/hamajyotan/ap4r/script ... copy file from $GEM_HOME/gems/ap4r-0.3.7/fresh_rakefile to /home/hamajyotan/ap4r/Rakefile ... [/home/hamajyotan/ap4r] has successfully set up! $
以下のように /home/hamajyotan/ap4r/config/queues_notargets.cfg を作成する
ap4r が勝手にメッセージをディスパッチしないようにする
--- store: type: disk drb: host: port: 6438 acl: allow 127.0.0.1 allow ::1 allow 10.0.0.0/8 dispatchers: - targets: notargets threads: 1 #carriers: # - # source_uri: druby://another.host.local:6438 # threads: 1
ap4r の起動
$ cd /home/hamajyotan/ap4r/ $ ruby script/mongrel_ap4r start -A config/queues_notargets.cfg --- === === ** Starting AP4R Handler with config/queues_notargets.cfg Loaded queues configuration from: /home/hamajyotan/ap4r/config/queues_notargets.cfg Using message store: disk Accepting requests at: druby://localhost:6438 about to start dispatchers with config --- - threads: 1 targets: notargets start dispatcher: targets= #<ReliableMsg::MultiQueue:0x2b89a7d39e90>, index= 0) dispatch targets are : notargets; queue manager has forked dispatchers ** Signals ready. TERM => stop. USR2 => restart. INT => stop (no restart). ** Mongrel available at 0.0.0.0:7438 ** Use CTRL-C to stop. ** Mongrel start up process completed.
reliable-msg-agent の設定ファイルの準備
ap4r を起動したコンソールとは別窓で作業
以下、reliable-msg-agnet の作業ディレクトリを /home/hamajyotan/reliable-msg-agent/ と仮定
gem をインストールしたディレクトリ配下の resources/ ディレクトリから必要なファイルをコピーする
$ cp $GEM_HOME/gems/reliable-msg-agent-0.1.0/resources/agent.conf /home/hamajyotan/reliable-msg-agent/ $ cp $GEM_HOME/gems/reliable-msg-agent-0.1.0/resources/examples/ap4r-dispatch-agent.rb /home/hamajyotan/reliable-msg-agent/
agent.conf に設定を加える
コンフィグにエージェントの定義ファイルのパスを教える。
$ echo agent: /home/hamajyotan/reliable-msg-agent/ap4r-dispatch-agent.rb >> /home/hamajyotan/reliable-msg-agent/agent.conf
コンフィグの確認
--- logger: " Proc.new { |file| l = Logger.new(file); l.level = Logger::DEBUG; l } " consumers: - source_uri: druby://localhost:6438 every: 1.0 target: queue.agent threads: 1 agent: /home/hamajyotan/reliable-msg-agent/ap4r-dispatch-agent.rb
コンフィグを少し書き換える。
- modify_rules
- url
- ディスパッチ先 url http://localhost を http://localhost:9292 に書き換える
- url
--- logger: " Proc.new { |file| l = Logger.new(file); l.level = Logger::DEBUG; l } " consumers: - source_uri: druby://localhost:6438 every: 1.0 target: queue.agent threads: 1 modify_rules: url: " Proc.new { |url| url.port = 9292; url } " agent: /home/hamajyotan/reliable-msg-agent/ap4r-dispatch-agent.rb
ここでエージェントの #call メソッドの定義を確認しておく
# this script is evaluated by the context of ReliableMsg::Agnet::Agent class. require "yaml" require "ap4r" # # The method of processing the message is defined. # # if the evaluation result is nil or false, # it is considered that it failes. # # === Args # # +msg+ :: fetched message from reliable-msg queue. # +conf+ :: consumer configurations. # +options+ :: the options (it is still unused.) # def call msg, conf, options = {} # The following codes use the mechanism of sending the message by ap4r. dispatcher = Ap4r::Dispatchers.new nil, [], @logger @logger.debug { "dispatcher get message\n#{msg.to_yaml}" } response = dispatcher.send(:get_dispather_instance, msg.headers[:dispatch_mode], msg, conf).call @logger.debug { "dispatcher get response\n#{response.to_yaml}" } end
ap4r の実装の一部をコンフィグで埋め込んでいる、ほぼおまじない。
これにより、 ap4r で実装されている内容と同様のメッセージディスパッチが可能になる。※ ap4r 0.3.7 で確認
reliable-msg-agent の起動
$ reliable-msg-agent start -c /home/hamajyotan/reliable-msg-agent/agent.conf *** Starting ReliableMsg-Agent... I, [2011-04-12T01:22:28.678494 #9696] INFO -- : *** reliable-msg agent service starting... I, [2011-04-12T01:22:28.678822 #9696] INFO -- : --- starting workers. I, [2011-04-12T01:22:28.680012 #9696] INFO -- : *** reliable-msg agent service started.
適当な web サーバを準備する
更に別窓で作業する。
以下を満たせばなんでも良い
- (先に、コンフィグに設定している為)9292番ポートでリスンする
- ステータスコードに 200 で、レスポンスボディに true (を含む文字列) を返す
今回は rack で適当に作る
$ gem install rack --no-ri --no-rdoc
rack のコンフィグファイルを適当に書く
/home/hamajyotan/stub.ru
# # /home/hamajyotan/stub.ru # require "rubygems" require "rack" class Stub def call env [200, {"Content-Type" => "text/plain"}, ["true"]] end end run Stub.new
rack で web サーバ起動しておく
$ rackup /home/hamajyotan/stub.ru
ap4r へメッセージを put する
更に別窓で作業する。
以下の代わりに ap4r の rails プラグインを使って put しても良い
$ irb -rubygems -r reliable-msg irb(main):001:0> q = ReliableMsg::Queue.new "queue.agent" => #<ReliableMsg::Queue:0x2ac0e926f7a8 @queue="queue.agent"> irb(main):002:0> q.put "", {:dispatch_mode => :HTTP, irb(main):003:0> :target_method => :POST, irb(main):004:0> :target_url => "http://localhost/", irb(main):005:0> :queue => "queue.agent", irb(main):006:0> :delivery => :once} => "2f5e6c80-4688-012e-8954-f9a630fcd34e" irb(main):007:0> exit $
reliable-msg-agent を起動したコンソールを見てみる
- サービス起動
- メッセージ取得
- url 書き換え http://localhost/ -> http://localhost:9292
- http://localhost:9292 へアクセスして成功
の一連のログが出力されている模様
$ reliable-msg-agent start -c ./agent.conf *** Starting ReliableMsg-Agent... *** reliable-msg agent service starting... --- starting workers. *** reliable-msg agent service started. message fetched - <2f5e6c80-4688-012e-8954-f9a630fcd34e> dispatcher get message --- !ruby/object:ReliableMsg::Message headers: :dispatch_mode: :HTTP :target_url: http://localhost/ :created: 1302539965 :expires: :queue: queue.agent :delivery: :once :id: 2f5e6c80-4688-012e-8954-f9a630fcd34e :target_method: :POST :priority: 0 :max_deliveries: 5 id: 2f5e6c80-4688-012e-8954-f9a630fcd34e object: "" Ap4r::Dispatcher after modification --- !ruby/object:ReliableMsg::Message headers: :dispatch_mode: :HTTP :target_url: http://localhost:9292/ :created: 1302539965 :expires: :queue: queue.agent :delivery: :once :id: 2f5e6c80-4688-012e-8954-f9a630fcd34e :target_method: :POST :priority: 0 :max_deliveries: 5 id: 2f5e6c80-4688-012e-8954-f9a630fcd34e object: "" response status [200 OK] dispatcher get response --- !ruby/object:Net::HTTPOK body: "true" body_exist: true code: "200" header: content-type: - text/plain connection: - close date: - Mon, 11 Apr 2011 16:39:25 GMT transfer-encoding: - chunked http_version: "1.1" message: OK read: true socket:
stub.ru (webサーバ) を起動したコンソールを見てみる
アクセスがあった模様
$ rackup /home/hamajyotan/stub.ru 127.0.0.1 - - [12/Apr/2011 01:39:25] "POST / HTTP/1.1" 200 - 0.0023
[追記] http のタイムアウト
ap4r の dispatchers 機能をエージェントに記述しているため、コンフィグ consumers には、ap4r における dispatchers と同様のコンフィグが書ける。
試しに Ap4r::Dispachers に実装されている http タイムアウトを実験する。
http タイムアウトに関する設定を追記
reliable-msg-agent のコンフィグファイルに http タイムアウトに関する設定を追記
3秒以内に処理が終わらなければ失敗とみなす
/home/hamajyotan/reliable-msg-agent/agent.conf
--- logger: " Proc.new { |file| l = Logger.new(file); l.level = Logger::DEBUG; l } " consumers: - source_uri: druby://localhost:6438 every: 1.0 target: queue.agent threads: 1 modify_rules: url: " Proc.new { |url| url.port = 9292; url } " http: timeout: 3 agent: /home/hamajyotan/reliable-msg-agent/ap4r-dispatch-agent.rb
stub.ru が 10秒かかるようにしておく
# # /home/hamajyotan/stub.ru # require "rubygems" require "rack" class Stub def call env sleep 10 [200, {"Content-Type" => "text/plain"}, ["true"]] end end run Stub.new
上記設定でメッセージを put してみる
エージェントの処理がタイムアウトで落ちている
reliable-msg-agent では以下のログが出力される
# # 途中省略 # message fetched - <52222a00-4692-012e-8f7a-f9afb181b616> dispatcher get message --- !ruby/object:ReliableMsg::Message headers: :dispatch_mode: :HTTP :target_url: http://localhost/test/index.html :created: 1302544318 :expires: :queue: queue.agent :delivery: :once :id: 52222a00-4692-012e-8f7a-f9afb181b616 :target_method: :POST :priority: 0 :max_deliveries: 5 id: 52222a00-4692-012e-8f7a-f9afb181b616 object: "" Ap4r::Dispatcher after modification --- !ruby/object:ReliableMsg::Message headers: :dispatch_mode: :HTTP :target_url: http://localhost:9292/test/index.html :created: 1302544318 :expires: :queue: queue.agent :delivery: :once :id: 52222a00-4692-012e-8f7a-f9afb181b616 :target_method: :POST :priority: 0 :max_deliveries: 5 id: 52222a00-4692-012e-8f7a-f9afb181b616 object: "" set HTTP read timeout to 3s error in fetch-msg/agent-proc: execution expired /home/hamajyotan/.rvm/rubies/ruby-1.8.7-p302/lib/ruby/1.8/timeout.rb:64:in `rbuf_fill' /home/hamajyotan/.rvm/rubies/ruby-1.8.7-p302/lib/ruby/1.8/net/protocol.rb:134:in `rbuf_fill' /home/hamajyotan/.rvm/rubies/ruby-1.8.7-p302/lib/ruby/1.8/net/protocol.rb:116:in `readuntil' /home/hamajyotan/.rvm/rubies/ruby-1.8.7-p302/lib/ruby/1.8/net/protocol.rb:126:in `readline' /home/hamajyotan/.rvm/rubies/ruby-1.8.7-p302/lib/ruby/1.8/net/http.rb:2028:in `read_status_line' # # 途中省略 # msg-agent:75:in `send' /home/hamajyotan/.rvm/gems/ruby-1.8.7-p302/gems/reliable-msg-agent-0.1.0/bin/reliable-msg-agent:75 /home/hamajyotan/.rvm/gems/ruby-1.8.7-p302/bin/reliable-msg-agent:19:in `load' /home/hamajyotan/.rvm/gems/ruby-1.8.7-p302/bin/reliable-msg-agent:19
Ruby の retry はどこからリトライしているのか ?(Ruby 1.8系)
ちなみに
- 下記は、Ruby 1.9系では構文エラーになる。
知ってちょっとびっくりした
# # test.rb # class RetryTest def initialize @x = 0 puts "initialize!!" end def test x @x += 1 yield @x retry if x > @x end end puts RUBY_VERSION t = RetryTest.new t.test(3) { |i| puts "test - #{i}" }
$ ruby test.rb 1.8.7 initialize!! test - 1 test - 2 test - 3
RetryTest#test は
- @x をインクリメント
- ブロックを評価、その際に引数に @x を渡す
- @x が x を超えない場合は リトライ する
リトライってどこからしているの?
実行結果から、 #test が再実行されている事は明らかだが・・
# # test.rb # class RetryTest def initialize @x = 0 puts "initialize!!" end def test x @x += 1 yield @x retry if x > @x end end def hoge puts "#hoge called!" 3 end puts RUBY_VERSION t = RetryTest.new t.test(hoge) { |i| puts "test - #{i}" }
$ ruby test.rb 1.8.7 initialize!! #hoge called! test - 1 #hoge called! test - 2 #hoge called! test - 3
どうやら引数も再評価されている。これは予想の上だった。
更にびっくりした
インスタンス生成とメソッド呼び出しを1行で書いてみる
# # test.rb # class RetryTest def initialize @x = 0 puts "initialize!!" end def test x @x += 1 yield @x retry if x > @x end end def hoge puts "#hoge called!" 3 end puts RUBY_VERSION RetryTest.new.test(hoge) { |i| puts "test - #{i}" }
$ ruby test.rb 1.8.7 initialize!! #hoge called! test - 1 initialize!! #hoge called! test - 1 initialize!! #hoge called! test - 1 以下延々と続く・・・
リトライのたびにインスタンスが生成されている
引数の評価だけでなく、行全体が再度評価されている模様。
これは初見だとハマると思う。
Ruby 1.9 系ではこんなエラー
$ rvm 1.9.2 test.rb test.rb:9: Invalid retry test.rb: compile error (SyntaxError)
ReliableMsg からメッセージ拾って何か処理する ReliableMsgAgent
ReliableMsgAgent
まだまだ改善の余地があるが書いてみました。
こんな実装
- ReliableMsg に置いてあるメッセージを自律的に取得する
- メッセージに応じて何か処理をする
- 「何か処理」は自分で定義できる
ReliableMsg とは?
ReliableMsgAgent のインストール
$ gem install reliable-msg-agent
ちょっと試すその1
ReliableMsg のキューを起動しておく
$ queues manager start I, [2011-04-10T02:56:06.061758 #32480] INFO -- : Created queues configuration file in: /usr/lib/ruby/gems/1.8/gems/reliable-msg-1.1.0/queues.cfg I, [2011-04-10T02:56:06.062504 #32480] INFO -- : Using message store: disk I, [2011-04-10T02:56:06.074105 #32480] INFO -- : Accepting requests at: druby://localhost:6438
ReliableMsgAgent プロセス起動
別窓で
$ reliable-msg-agent start *** Starting ReliableMsg-Agent... I, [2011-04-10T02:57:54.933661 #32520] INFO -- : *** reliable-msg agent service starting... I, [2011-04-10T02:57:54.934218 #32520] INFO -- : --- starting workers. I, [2011-04-10T02:57:54.938412 #32520] INFO -- : *** reliable-msg agent service started.
ReliableMsg キューにメッセージを投げてみる
更に別窓で
$ irb -rubygems -r reliable-msg irb(main):001:0> q = ReliableMsg::Queue.new "queue.agent" => #<ReliableMsg::Queue:0x2abc07e020e8 @queue="queue.agent"> irb(main):002:0> q.put "(*^o^)=3" => "36086840-4501-012e-1595-f3f4f884137e" irb(main):003:0> exit $
ReliableMsgAgent を起動したコンソールを見る
$ reliable-msg-agent start *** Starting ReliableMsg-Agent... I, [2011-04-10T02:57:54.933661 #32520] INFO -- : *** reliable-msg agent service starting... I, [2011-04-10T02:57:54.934218 #32520] INFO -- : --- starting workers. I, [2011-04-10T02:57:54.938412 #32520] INFO -- : *** reliable-msg agent service started. I, [2011-04-10T03:00:43.902741 #32520] INFO -- : message fetched - <36086840-4501-012e-1595-f3f4f884137e> I, [2011-04-10T03:00:43.903403 #32520] INFO -- : message received --- !ruby/object:ReliableMsg::Message headers: :expires: :created: 1302372043 :delivery: :best_effort :id: 36086840-4501-012e-1595-f3f4f884137e :priority: 0 :max_deliveries: 5 id: 36086840-4501-012e-1595-f3f4f884137e object: (*^o^)=3
ReliableMsg からメッセージを拾って、内容をログ出力している。
ReliableMsgAgent を終了する
INT か TERM シグナルで止まる。
Ctrl+C
I, [2011-04-10T03:10:16.513932 #32520] INFO -- : *** reliable-msg agent service stopping... I, [2011-04-10T03:10:16.514195 #32520] INFO -- : --- stopping workers. I, [2011-04-10T03:10:16.514432 #32520] INFO -- : *** reliable-msg agent service stopped. $
ちょっと試すその2
- メッセージ取得時の動作を変更してみる。
- ReliableMsgAgent の既定の動作は、メッセージを取得してログ出力するのみ。
- エージェントの #call メソッドを(再)定義するとそれを変更できる。
設定ファイルの雛形を準備する
以下、作業ディレクトリを /home/hamajyotan/reliable-msg-agent/ と仮定
gem をインストールしたディレクトリ配下の resources/ ディレクトリから必要なファイルをコピーする
$ cp $GEM_HOME/gems/reliable-msg-agent-0.1.0/resources/agent.conf /home/hamajyotan/reliable-msg-agent/ $ cp $GEM_HOME/gems/reliable-msg-agent-0.1.0/resources/agent.rb /home/hamajyotan/reliable-msg-agent/
agent.conf に設定を加える
要するに、「agent.rb はここにあるよ」という事をコンフィグに書いている。
$ echo agent: /home/hamajyotan/reliable-msg-agent/agent.rb >> agent.conf
コンフィグファイルはこんな感じになっている
$ cat agent.conf --- logger: " Proc.new { |file| l = Logger.new(file); l.level = Logger::DEBUG; l } " consumers: - source_uri: druby://localhost:6438 every: 1.0 target: queue.agent threads: 1 agent: /home/hamajyotan/reliable-msg-agent/agent.rb $
agent.rb を見てみる
agent.rb は、エージェントのクラス実装の一部を成している。
# this script is evaluated by the context of ReliableMsg::Agnet::Agent class. require "yaml" # # The method of processing the message is defined. # # if the evaluation result is nil or false, # it is considered that it failes. # # === Args # # +msg+ :: fetched message from reliable-msg queue. # +conf+ :: consumer configurations. # +options+ :: the options (it is still unused.) # def call msg, conf, options = {} @logger.info { "message received\n#{msg.to_yaml}" } end
agent.rb を書き換える
例えば、メッセージでうけた名称のディレクトリを /tmp に作成する処理
# # /home/hamajyotan/reliable-msg-agent/agent.rb # require "fileutils" def call msg, conf, options = {} dir = File.join("/tmp", msg.object) FileUtils.mkdir_p dir @logger.info { "directory created - #{dir}" } end
ReliableMsgAgent の起動
今度は、コンフィグファイルを指定しつつ実行する
$ reliable-msg-agent start -c /home/hamajyotan/reliable-msg-agent/agent.conf *** Starting ReliableMsg-Agent... I, [2011-04-10T03:39:36.582628 #32659] INFO -- : *** reliable-msg agent service starting... I, [2011-04-10T03:39:36.582793 #32659] INFO -- : --- starting workers. I, [2011-04-10T03:39:36.584784 #32659] INFO -- : *** reliable-msg agent service started.
ReliableMsg キューにメッセージを投げてみる
別窓で
$ irb -rubygems -r reliable-msg irb(main):001:0> ReliableMsg::Queue.new("queue.agent").put("foo") => "f11a65e0-4506-012e-1595-f3f4f884137e" irb(main):002:0> ReliableMsg::Queue.new("queue.agent").put("bar") => "f93693a0-4506-012e-1595-f3f4f884137e" ruby-1.8.7-p302 :003 > exit $
ReliableMsgAgent を起動したコンソールを見る
$ reliable-msg-agent start -c /home/hamajyotan/reliable-msg-agent/agent.conf *** Starting ReliableMsg-Agent... I, [2011-04-10T03:39:36.582628 #32659] INFO -- : *** reliable-msg agent service starting... I, [2011-04-10T03:39:36.582793 #32659] INFO -- : --- starting workers. I, [2011-04-10T03:39:36.584784 #32659] INFO -- : *** reliable-msg agent service started. I, [2011-04-10T03:41:45.406074 #32659] INFO -- : message fetched - <f11a65e0-4506-012e-1595-f3f4f884137e> I, [2011-04-10T03:41:45.406460 #32659] INFO -- : directory created - /tmp/foo I, [2011-04-10T03:41:58.519396 #32659] INFO -- : message fetched - <f93693a0-4506-012e-1595-f3f4f884137e> I, [2011-04-10T03:41:58.519885 #32659] INFO -- : directory created - /tmp/bar
ReliableMsg からメッセージを拾って、ディレクトリを作成している模様。