前回のおさらい
前回は、
すなわち async_
メソッドを使って
今回は、
アプリケーションの拡張の方向性
2つの拡張を行います。
異常系に対する備え
メッセージングを利用してアプリケーションを非同期化する際には、
データベースやネットワークの障害、
下図は、

また次の図は、

業務DBへのCRUDの反映とメッセージの生成をアトミックに行い、
非同期処理もテストする
テスト駆動開発
筆者はあるプロジェクトでTDDを実践し、
この連載では、
Rubyには、Test::Unit
ラリブラリが付属しており、
- オブジェクト倶楽部2006クリスマスイベントでTDDとRSpecについて話をしました - 角谷HTML化計画 (2006-12-21)
- Rubyリファレンスマニュアル - Test::Unit
- Rubyist Magazine - RubyOnRails を使ってみる 【第 6 回】 テストの書き方
非同期処理を含むアプリケーションでも、
- (Rails付属の)
functionalテスト - アクション単体のテストとして、
メッセージングでの通信無しにテストを行います。 - asyncテスト
- ネットワーク通信やメッセージDBへのアクセスを含め、
実動作となるべく近いやり方でテストを行います。
次ページから、
SAF機能を利用してみましょう
前回非同期化したアプリケーションをベースに拡張します。Rails、modify_
の設定はコメントアウトしておいてください。
ベースとなるサンプルの一連のコードは以下からも取得できます。
% svn checkout http://ap4r.rubyforge.org/svn/tags/200709_gihyo_async_shop
準備
本題にはいる前にAP4Rのアップデートをしましょう。最新バージョンのap4r-0.
% gem update ap4r
また、
% ruby script/plugin install http://ap4r.rubyforge.org/svn/tags/ap4r-0.3.3/samples/HelloWorld/vendor/plugins/ap4r
SAF用テーブルの準備
SAF機能でメッセージを保管するテーブルを用意します。すでにordersテーブルとpaymentsテーブルの作成に使ったmigrationファイルがあるので、
% cd as_rails % ruby script/generate migration create_table_for_saf exists db/migrate create db/migrate/003_create_table_for_saf.rb
db/
class CreateTableForSaf < ActiveRecord::Migration
def self.up
create_table :stored_messages do |t|
t.column :duplication_check_id, :string, :null => false
t.column :queue, :string, :null => false
t.column :headers, :binary, :null => false
t.column :object, :binary, :null => false
t.column :status, :integer, :null => false
t.column :created_at, :datetime, :null => false
t.column :updated_at, :datetime, :null => false
end
end
def self.down
drop_table :stored_messages
end
end
RubyGemsでインストールされたAP4Rのなかにもmigrationファイルのサンプルがあります
ap4r.transaction
準備は整ったので、
- 修正前
-
class AsyncShopController < ApplicationController (省略) def order begin Order.
transaction do @order = Order. new(params[:order]) @order. save! ap4r. async_ to({:action => 'payment'}, {:order_ id => @order. id}) flash[:notice] = 'Order was successfully created.' redirect_ to :action => 'index' end rescue Exception flash[:notice] = 'Order was failed.' render :action => 'order_ form' end end (省略) end - 修正後
-
class AsyncShopController < ApplicationController (省略) def order begin ap4r.
transaction do @order = Order. new(params[:order]) @order. save! ap4r. async_ to({:action => 'payment'}, {:order_ id => @order. id}) flash[:notice] = 'Order was successfully created.' redirect_ to :action => 'index' end rescue Exception flash[:notice] = 'Order was failed.' render :action => 'order_ form' end end (省略) end
違いに気づいていただけたでしょうか?
def order
begin
Order.transaction do
が
def order
begin
ap4r.transaction do
に変わっているだけです。
修正は以上ですが、ap4r.
メソッドの動きをもう少しみてみましょう。
ap4r.transaction
の裏側
上の図から分かるように、ap4r.
メソッドの部分では、
次に、
こうして、
実行
実行してみましょう。RailsとAP4Rを起動してください。
% cd as_rails % ruby script/server
% cd as_ap4r % ruby script/mongrel_ap4r start -A config/queues_mysql.cfg
以下のURLにアクセスします。
「New order」
無事に注文結果が表示されたでしょうか?
ユーザーから見た動きは、
SAF用テーブルに保管されているもの
さて、
業務DBに作成したSAF用のテーブルを確認します。
% cd as_rails % ruby script/console >> y Ap4r::StoredMessage.find(:all) --- [] => nil
Storeされたはずのメッセージ情報がなにもありません。実は、
今回は、
Ap4r::AsyncHelper::Base.saf_delete_mode = :logical
画面より注文処理を実行し、
% ruby script/console >> Ap4r::AsyncHelper::Base.saf_delete_mode => :logical >> y Ap4r::StoredMessage.find(:all) --- - !ruby/object:Ap4r::StoredMessage attributes: status: "1" updated_at: 2007-09-03 15:24:48 duplication_check_id: 4b68b9c0-3c14-012a-cfa0-0016cb9ad524 id: "13" queue: queue.async_shop.payment object: "\x04\b\"\x10order_id=13" created_at: 2007-09-03 15:24:48 headers: "\x04\b{\n\ :\rdelivery:\tonce:\x0Fqueue_name\"\x1Dqueue.async_shop.payment \x12dispatch_mode:\tHTTP:\x0Ftarget_url\"-http://localhost:3000/async_shop/payment :\x12target_method\"\tPOST"=> nil
SAF用テーブル内に1件のレコードがありました。statusが各メッセージのForward状況を示しています。
status = 0 | 未処理 |
---|---|
status = 1 | 処理済 |
最後に、
>> Ap4r::StoredMessage.destroy_all => [#<Ap4r::StoredMessage:0x356748c @attr... (省略)
あわせて、
% mysql -u ap4r -p ap4r mysql> delete from reliable_msg_queues;
異常系での挙動確認
異常系でも安心してメッセージングができるよう拡張したので、
一例として、

この状況を擬似的にコードでつくると、ap4r.
メソッドのあとに例外処理をいれることで実現できます。ap4r.
を使用していない場合は、
では、ap4r.
を利用した以下の例ではどうでしょう。
def order
begin
ap4r.transaction do
@order = Order.new(params[:order])
@order.save!
ap4r.async_to({:action => 'payment'},
{:order_id => @order.id})
raise "for SAF test"
(省略)
end
end
実際にコードに一行、
ユーザーの画面には、
% cd as_rails % ruby script/console >> Ap4r::AsyncHelper::Base.saf_delete_mode => :logical >> y Ap4r::StoredMessage.find(:all) --- [] => nil
Storeされたメッセージはないようです。
% mysql -u ap4r -p ap4r mysql> select id, queue from reliable_msg_queues; Empty set (0.00 sec)
SAF用テーブルに Storeされていなかったので、
今回は、
次ページからは安心非同期のもうひとつの柱、
AP4R におけるテストサポート
AP4Rでのテストの方法には、
テストの動作概要
1つ目は、

このテスト方法の利点は、
2つ目は、

この2種類のテストは、
functionalテストの書き方と実行
functionalテストでAP4R特有なところは以下の2点です。
- キューイングをスタブ化するコードを読み込む
- キューイングされたメッセージに対してアサーションを書く
まず、
require 'ap4r/queue_put_stub'
テストコードは、
ここでは、order
アクションを呼び出して、
def test_order
post :order, :order => {:item => "introduction to AP4R"}
assert_response :redirect
assert_redirected_to :action => 'index'
messages = @controller.ap4r.queued_messages # ... (1)
assert_equal 1, messages.keys.size, "should have messages in just ONE queue"
assert messages.key?("queue.async_shop.payment"), "queue name is INCORRECT" # ... (2)
assert_equal 1, messages["queue.async_shop.payment"].size,
"should have just ONE message for payment"
assert_match /order_id=\d+/, messages["queue.async_shop.payment"].first[:body],
"parameter order_id should be included with a numeric value" # ... (3)
end
(1)では、@controller.
)Hash
オブジェクトは、:body
):headers
)
---
queue.async_shop.payment:
- :body: order_id=1
:headers:
:target_method: POST
:delivery: :once
:dispatch_mode: :HTTP
:queue_name: queue.async_shop.payment
:target_url: http://test.host/async_shop/payment
このオブジェクトに対し、order_
というキーを含むかどうか(3)を確認しています。
functionalテストの実行は、
% rake test:functionals または % ruby test/functional/async_shop_controller_test.rb
テストが成功した場合、
Finished in 0.066875 seconds. 2 tests, 7 assertions, 0 failures, 0 errors
このようにキューイングをスタブ化することで、
asyncテストの書き方と実行
非同期処理のテストで実際に通信を行うには、
まとめると、
- テストのクライアントなるプロセス:
Test::Unit
を実行 - Railsプロセス: mongrel_
railsを実行 - AP4Rプロセス: mongrel_
ap4rを実行
これらのプロセス間の関係は、
テストの実行方法の前に、
ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + "/../../config/environment")
require "ap4r/service_handler"
ap4r_test_helper = Ap4r::ServiceHandler.new
require 'test_help'
class Test::Unit::TestCase
self.use_transactional_fixtures = false
self.use_instantiated_fixtures = false
# Add more helper methods to be used by all tests here...
cattr_accessor :ap4r_helper
def ap4r_helper
@@ap4r_helper
end
def with_services(&block)
ap4r_helper.with_services(&block)
end
end
Test::Unit::TestCase.ap4r_helper = ap4r_test_helper
require "#{File.dirname(__FILE__)}/ap4r_test_helper"
require 'net/http'
# Test cases with ap4r integration.
# some comments:
# - Clearance of data in a database and queues should be included.
# - Workspaces of ap4r and rails should have some conventions for convinience.
# - Think about transition to RSpec.
# - HTTP session holding support is needed?
class AsyncShopTest < Test::Unit::TestCase
def test_http_dispatch
ap4r_helper.stop_dispatchers # ... (1)
assert_rows_added(Order, 1) { # ... (3)
do_order # ... (2)
}
assert_rows_added(Payment, 1) { # ... (6)
ap4r_helper.start_dispatchers # ... (4)
ap4r_helper.wait_all_done # ... (5)
}
end
private
# Requests to <tt>async_shop/order</tt>.
def do_order(item_name = "test item")
Net::HTTP.start("localhost", 3000, nil, nil) do |http|
http.request_post("/async_shop/order",
"order[item]=#{item_name}") do |res|
#nop
end
end
end
def assert_rows_added(model, rows)
rows_before = model.count
yield
rows_after = model.count
assert_equal rows, rows_after - rows_before, "table '#{model.table_name}' should count up by #{rows}"
end
end
テストメソッド test_
の中で行っていることは以下のようになります。番号は、
- AP4Rサービスを操作する
ap4r_
を用いて、helper 非同期処理の呼び出しスレッドを停止させます。 - Railsへ、
HTTP POSTのリクエストを発行し、 orders
テーブルに 1 行追加されたことを確認します。- 非同期処理の呼び出しスレッドを開始させ、
- 非同期処理が終わるまで待機し、
payments
テーブルに 1 行追加されたことを確認します。
それでは、
Railsプロセス、AP4Rプロセスの起動
まずは、
Unix系のOSでは、
設定ファイルは下記のとおりです。
ap4r:
root_dir: ../as_ap4r
config_file: config/queues_mysql.cfg
rails:
% cd as_rails % rake test:asyncs:arrange
Windowsでは、
- 1つ目のコマンドプロンプト
-
% cd as_
rails % ruby script/ server -e test - 2つ目のコマンドプロンプト
-
% cd as_
ap4r % ruby script/ mongrel_ ap4r start -A config/ test_ queues. cfg
テストの実行
テストの実行は、
% rake test:asyncs:run または % ruby test/async/async_shop_test.rb
テストが成功すると、
Finished in 5.530491 seconds. 1 tests, 2 assertions, 0 failures, 0 errors
今回は、payment
アクションも実行されているため、
後片付け
起動しているプロセスを終了させます。Unix系のOSではRakeタスクで終了させることが出来ます。
% cd as_rails % rake test:asyncs:cleanup
Windowsでは、Ctrl-C
を押すことで、
アクション単体のテストを手軽に行うためのfunctionalテストと、
テストサポートは追加されたばかりの機能で、
第3回では、
最終回となる次回は、