ap4r 0.3.2 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,132 @@
1
+ # Author:: Kiwamu Kato
2
+ # Copyright:: Copyright (c) 2007 Future Architect Inc.
3
+ # Licence:: MIT Licence
4
+
5
+ require 'forwardable'
6
+ require 'async_helper'
7
+
8
+ module Ap4r #:nodoc:
9
+
10
+ # This +Client+ is the Rails plugin for asynchronous processing.
11
+ # Asynchronous logics are called via various protocols, such as XML-RPC,
12
+ # SOAP, HTTP POST, and more. Now default protocol is HTTP POST.
13
+ #
14
+ # Examples: The part of calling next asynchronous logics in a controller in the HelloWorld Sample.
15
+ #
16
+ # req = WorldRequest.new([:world_id => 1, :message => "World"})
17
+ # ap4r.async_to({:controller => 'async_world', :action => 'execute'},
18
+ # {:world_id => 1, :message => "World"},
19
+ # {:dispatch_mode => :HTTP}) # skippable
20
+ #
21
+ # render :action => 'response'
22
+ #
23
+ # Complement: Above +ap4r+ method is defiend init.rb in +%RAILS_ROOT%/vendor/plugin/ap4r/+.
24
+ #
25
+ class Client
26
+ extend Forwardable
27
+ include ::Ap4r::AsyncHelper::Base
28
+
29
+ def initialize controller
30
+ @controller = controller
31
+ end
32
+
33
+ def_delegators :@controller, :logger, :url_for
34
+
35
+ # Queue a message for next asynchronous logic. Some options are supported.
36
+ #
37
+ # Use options to specify target url, etc.
38
+ # Accurate meanings are defined by a individual converter class.
39
+ # * :controller (name of next logic)
40
+ # * :action (name of next logic)
41
+ #
42
+ # Use rm_options to pass parameter in queue-put.
43
+ # Listings below are AP4R extended options.
44
+ # See the reliable-msg docuememt for more details.
45
+ # * :target_url (URL of target, prevail over :controller)
46
+ # * :target_action (action of target, prevail over :action)
47
+ # * :target_method (HTTP method, e.g. "GET", "POST", etc.)
48
+ # * :dispatch_mode (protocol in dispatching)
49
+ # * :queue_name (prevail over :controller and :action)
50
+ #
51
+ # Object of argumemts (async_params, options and rm_options) will not be modified.
52
+ # Implementors (of this class and converters) should not modify them.
53
+ #
54
+ # Examples: the most simple
55
+ #
56
+ # ap4r.async_to({:controller => 'next_controller', :action => 'next_action'},
57
+ # {:world_id => 1, :message => "World"})
58
+ #
59
+ #
60
+ # Examples: taking block
61
+ #
62
+ # ap4r.async_to({:controller => 'next_controller', :action => 'next_action'}) do
63
+ # body :world_id, 1
64
+ # body :message, "World"
65
+ # end
66
+ #
67
+ #
68
+ # Examples: transmitting ActiveRecord object
69
+ #
70
+ # ap4r.async_to({:controller => 'next_controller', :action => 'next_action'}) do
71
+ # body :world, World.find(1)
72
+ # body :message, "World"
73
+ # end
74
+ #
75
+ #
76
+ # Examples: transmitting with xml format over http (now support text, json and yaml format).
77
+ #
78
+ # ap4r.async_to({:controller => 'next_controller', :action => 'next_action'}) do
79
+ # body :world, World.find(1)
80
+ # body :message, "World"
81
+ # format :xml
82
+ # end
83
+ #
84
+ #
85
+ # Examples: direct assignment for formatted message body
86
+ #
87
+ # ap4r.async_to({:controller => 'next_controller', :action => 'next_action'}) do
88
+ # world = World.find(1).to_xml :except => ...
89
+ # body_as_xml world
90
+ # end
91
+ #
92
+ #
93
+ # Examples: setting message header
94
+ #
95
+ # ap4r.async_to({:controller => 'next_controller', :action => 'next_action'}) do
96
+ # body :world_id, 1
97
+ # body :message, "World"
98
+ #
99
+ # header :priority, 1
100
+ # http_header "Content-type", ...
101
+ # end
102
+ #
103
+ alias :async_to :async_dispatch
104
+
105
+ # Provides at-least-once QoS level.
106
+ # +block+ are tipically composed of database accesses and +async_to+ calls.
107
+ # Database accesses are executed transactionallly by +active_record_class+'s transaction method.
108
+ # In the +block+, +async_to+ calls invoke NOT immediate queueing but just storing messages
109
+ # to the database (assumed to be the same one as application uses).
110
+ #
111
+ # If the execution of +block+ finishes successfully, database transaction is committed and
112
+ # forward process of each stored message begins.
113
+ # Forward process composed in two parts. First puts the message into a queue, secondary update
114
+ # or delete the entry from a management table.
115
+ #
116
+ # SAF (store and forward) processing like this guarantees that any message
117
+ # is never lost and keeps reasonable performance (without two phase commit).
118
+ #
119
+ # Examples: Just call async_to method in this block.
120
+ #
121
+ # ap4r.transaction do
122
+ # req = WorldRequest.new([:world_id => 1, :message => "World"})
123
+ # ap4r.async_to({:controller => 'async_world', :action => 'execute'},
124
+ # {:world_id => 1, :message => "World"})
125
+ #
126
+ # render :action => 'response'
127
+ # end
128
+ #
129
+ alias :transaction :transaction_with_saf
130
+
131
+ end
132
+ end
@@ -3,32 +3,22 @@
3
3
  # Licence:: MIT Licence
4
4
 
5
5
  require 'reliable-msg'
6
- #require 'ap4r/stored_message'
6
+ require 'ap4r/stored_message'
7
+ require 'message_builder'
7
8
 
8
9
  module Ap4r
9
10
 
10
- # This +AsyncController+ is the Rails plugin for asynchronous processing.
11
- # Asynchronous logics are called via various protocols, such as XML-RPC,
12
- # SOAP, HTTP POST, and more. Now default protocol is HTTP POST.
11
+ # This +AsyncHelper+ is included to +Ap4rClient+ and works the Rails plugin
12
+ # for asynchronous processing.
13
13
  #
14
- # Examples: The part of calling next asynchronous logics in a controller in the HelloWorld Sample.
15
- #
16
- # req = WorldRequest.new([:world_id => 1, :message => "World"})
17
- # async_dispatch(req,
18
- # {:controller => 'async_world', :action => 'execute'},
19
- # :dispatch_mode => :XMLRPC }) # skippable
20
- #
21
- # render :action => 'response'
22
- #
23
- module AsyncController
14
+ module AsyncHelper
24
15
 
25
16
  module Base
26
17
  Converters = {}
27
18
 
28
- # TODO: constant or class variable, whick is better? 2007/05/02 by shino
29
19
  DRUBY_HOST = ENV['AP4R_DRUBY_HOST'] || 'localhost'
30
20
  DRUBY_PORT = ENV['AP4R_DRUBY_PORT'] || '6438'
31
- DRUBY_URI = "druby://#{DRUBY_HOST}:#{DRUBY_PORT}"
21
+ DRUBY_URI = "druby://#{DRUBY_HOST}:#{DRUBY_PORT}"
32
22
 
33
23
  @@default_dispatch_mode = :HTTP
34
24
  @@default_rm_options = { :delivery => :once, :dispatch_mode => @@default_dispatch_mode }
@@ -36,30 +26,7 @@ module Ap4r
36
26
 
37
27
  mattr_accessor :default_dispatch_mode, :default_rm_options, :default_queue_prefix, :saf_delete_mode
38
28
 
39
- # Provides at-least-once QoS level.
40
- # +block+ are tipically composed of database accesses and +async_dispatch+ calls.
41
- # Database accesses are executed transactionallly by +active_record_class+'s transaction method.
42
- # In the +block+, +async_dispatch+ calls invoke NOT immediate queueing but just storing messages
43
- # to the database (assumed to be the same one as application uses).
44
- #
45
- # If the execution of +block+ finishes successfully, database transaction is committed and
46
- # forward process of each stored message begins.
47
- # Forward process composed in two parts. First puts the message into a queue, secondary update
48
- # or delete the entry from a management table.
49
- #
50
- # SAF (store and forward) processing like this guarantees that any message
51
- # is never lost and keeps reasonable performance (without two phase commit).
52
- #
53
- # Examples: Just call async_dispath method in this block.
54
- #
55
- # transaction_with saf do
56
- # req = WorldRequest.new([:world_id => 1, :message => "World"})
57
- # async_dispatch(req,
58
- # {:controller => 'async_world', :action => 'execute'},
59
- # :dispatch_mode => :XMLRPC }) # skippable
60
- #
61
- # render :action => 'response'
62
- # end
29
+ # This method is aliased as ::Ap4r::Client#transaction
63
30
  #
64
31
  def transaction_with_saf(active_record_class = ::Ap4r::StoredMessage, *objects, &block)
65
32
 
@@ -78,7 +45,7 @@ module Ap4r
78
45
  # Once some error occured, such as disconnect reliable-msg or server crush,
79
46
  # which is smart to keep to put a message or stop to do it?
80
47
  # In the case of being many async messages, the former strategy is not so good.
81
- #
48
+ #
82
49
  # TODO: add delayed forward mode 2007/05/02 by shino
83
50
  Thread.current[:stored_messages].each {|k,v|
84
51
  __queue_put(v[:queue_name], v[:queue_message], v[:queue_headers])
@@ -106,27 +73,10 @@ module Ap4r
106
73
  Thread.current[:stored_messages] = nil
107
74
  end
108
75
 
109
- # Queue a message for next asynchronous logic. Some options are supported.
110
- #
111
- # Use options to specify target url, etc.
112
- # Accurate meanings are defined by a individual converter class.
113
- # * :controller (name of next logic)
114
- # * :action (name of next logic)
76
+ # This method is aliased as ::Ap4r::Client#async_to
115
77
  #
116
- # Use rm_options to pass parameter in queue-put.
117
- # Listings below are AP4R extended options.
118
- # See the reliable-msg docuememt for more details.
119
- # * :target_url (URL of target, prevail over :controller)
120
- # * :target_action (action of target, prevail over :action)
121
- # * :target_method (HTTP method, e.g. "GET", "POST", etc.)
122
- # * :dispatch_mode (protocol in dispatching)
123
- # * :queue_name (prevail over :controller and :action)
124
- #
125
- # Object of argumemts (async_params, options and rm_options) will not be modified.
126
- # Implementors (of this class and converters) should not modify them.
127
- #
128
- def async_dispatch(url_options = {}, async_params = nil, rm_options = nil)
129
- # TODO: add :url to url_options to specify :target_url shortly 2007/05/02 by shino
78
+ def async_dispatch(url_options = {}, async_params = {}, rm_options = {}, &block)
79
+
130
80
  if logger.debug?
131
81
  logger.debug("url_options: ")
132
82
  logger.debug(url_options.inspect)
@@ -138,7 +88,10 @@ module Ap4r
138
88
 
139
89
  # TODO: clone it, 2006/10/16 shino
140
90
  url_options ||= {}
141
- url_options[:controller] ||= self.controller_path.gsub("/", ".")
91
+ url_options[:controller] ||= @controller.controller_path.gsub("/", ".")
92
+ url_options[:url] ||= {:controller => url_options[:controller], :action => url_options[:action]}
93
+ url_options[:url][:controller] ||= url_options[:controller] if url_options[:url].kind_of?(Hash)
94
+
142
95
  rm_options = @@default_rm_options.merge(rm_options || {})
143
96
 
144
97
  # Only async_params is not cloned. options and rm_options are cloned before now.
@@ -150,6 +103,21 @@ module Ap4r
150
103
  queue_message = converter.make_params
151
104
  queue_headers = converter.make_rm_options
152
105
 
106
+ message_builder = ::Ap4r::MessageBuilder.new(queue_name, queue_message, queue_headers)
107
+ if block_given?
108
+ message_builder.instance_eval(&block)
109
+ end
110
+ queue_name = message_builder.queue_name
111
+ queue_headers = message_builder.message_headers
112
+ # TODO: proces flow of Converter and MessageBuilder should (probably) be reversed 2007/09/19 by shino
113
+ # This branching is ad-hoc fix
114
+ if queue_headers[:dispatch_mode] == :HTTP
115
+ queue_message = message_builder.format_message_body
116
+ else
117
+ queue_message = message_builder.message_body
118
+ end
119
+
120
+
153
121
  if Thread.current[:use_saf]
154
122
  stored_message = ::Ap4r::StoredMessage.store(queue_name, queue_message, queue_headers)
155
123
 
@@ -174,9 +142,14 @@ module Ap4r
174
142
  end
175
143
 
176
144
  def __get_queue_name(options, rm_options)
177
- rm_options[:queue_name] ||=
178
- @@default_queue_prefix.clone.concat(options[:controller].to_s).concat('.').concat(options[:action].to_s)
179
- rm_options[:queue_name]
145
+ if options[:url].kind_of?(Hash)
146
+ rm_options[:queue] ||=
147
+ @@default_queue_prefix.clone.concat(options[:url][:controller].to_s).concat('.').concat(options[:url][:action].to_s)
148
+ else
149
+ rm_options[:queue] ||=
150
+ @@default_queue_prefix.clone.chomp(".").concat(URI::parse(options[:url]).path.gsub("/", "."))
151
+ end
152
+ rm_options[:queue]
180
153
  end
181
154
 
182
155
  end
@@ -193,7 +166,7 @@ module Ap4r
193
166
  # add self to a Converters list.
194
167
  def self.dispatch_mode(mode_symbol)
195
168
  self.const_set(:DISPATCH_MODE, mode_symbol)
196
- ::Ap4r::AsyncController::Base::Converters[mode_symbol] = self
169
+ ::Ap4r::AsyncHelper::Base::Converters[mode_symbol] = self
197
170
  end
198
171
 
199
172
  def initialize(url_options, async_params, rm_options, url_for_handler)
@@ -220,6 +193,7 @@ module Ap4r
220
193
  private
221
194
  # helper method for <tt>ActionController#url_for</tt>
222
195
  def url_for(url_for_options, *parameter_for_method_reference)
196
+ return url_for_options if url_for_options.kind_of?(String)
223
197
  @url_for_handler.url_for(url_for_options, *parameter_for_method_reference)
224
198
  end
225
199
 
@@ -233,7 +207,7 @@ module Ap4r
233
207
  end
234
208
 
235
209
  def make_rm_options
236
- @rm_options[:target_url] ||= url_for(@url_options)
210
+ @rm_options[:target_url] ||= url_for(@url_options[:url])
237
211
  @rm_options[:target_method] ||= 'POST'
238
212
  #TODO: make option key to specify HTTP headers, 2006/10/16 shino
239
213
  @rm_options
@@ -254,14 +228,12 @@ module Ap4r
254
228
  end
255
229
 
256
230
  def action_api_name
257
- action_method_name = @url_options[:action]
231
+ action_method_name = @url_options[:url][:action]
258
232
  action_method_name.camelcase
259
233
  end
260
234
 
261
235
  def options_without_action
262
- new_opts = @url_options.dup
263
- new_opts[:action] = nil
264
- new_opts
236
+ @url_options[:url].reject{ |k,v| k == :action }
265
237
  end
266
238
 
267
239
  end
@@ -0,0 +1,181 @@
1
+ # Author:: Kiwamu Kato
2
+ # Copyright:: Copyright (c) 2007 Future Architect Inc.
3
+ # Licence:: MIT Licence
4
+
5
+ require 'active_record'
6
+
7
+ module Ap4r #:nodoc:
8
+
9
+ # This +MessageBuilder+ is the class for formatting message body.
10
+ # Current support formats are text, xml, json and yaml,
11
+ # and the formatted messages are sent over HTTP.
12
+ #
13
+ # Using +format+ method, this class automatically changes the format of
14
+ # the given message body and adds appropriate +Content-type+ to http header.
15
+ # Or using +body_as_*+ methods, you can directly assign formatted message body.
16
+ class MessageBuilder
17
+
18
+ def initialize(queue_name, queue_message, queue_headers)
19
+ @queue_name = queue_name
20
+ @message_body = queue_message
21
+ @message_headers = queue_headers
22
+ @format = nil
23
+ @message_body_with_format = nil
24
+ @to_xml_options = {:root => "root"}
25
+ end
26
+
27
+ attr_accessor :queue_name, :message_body, :message_headers
28
+ attr_reader :format, :to_xml_options
29
+
30
+ # Sets message body in async_to block.
31
+ # The first argument is key and the second one is value.
32
+ #
33
+ # options are for to_xml conversion on Array and Hash, ActiveRecord objects.
34
+ #
35
+ def body(k, v, options = { })
36
+ k ||= v.class
37
+ if v.kind_of? ActiveRecord::Base
38
+ @message_body[k.to_sym] = v
39
+ @to_xml_options = @to_xml_options.merge(options)
40
+ else
41
+ @message_body[k.to_sym] = v
42
+ end
43
+ end
44
+
45
+ # Sets message header in async_to block.
46
+ # The first argument is key and the second one is value.
47
+ #
48
+ # Now supports following keys:
49
+ # :expire
50
+ # :priority
51
+ # :delivery
52
+ # :max_deliveries
53
+ # :dispatch_mode
54
+ # :target_method
55
+ # :target_url
56
+ # :id
57
+ #
58
+ # For details, please refer the reliable-msg.
59
+ #
60
+ def header(k, v)
61
+ @message_headers[k.to_sym] = v
62
+ end
63
+
64
+ # Sets http header in async_to block such as 'Content_type'.
65
+ # The first argument is key and the second one is value.
66
+ #
67
+ def http_header(k, v)
68
+ @message_headers["http_header_#{k}".to_sym] = v
69
+ end
70
+
71
+ # Sets format message serialization.
72
+ # As to the format, automatically sets content-type.
73
+ # Unless any format, content-type is defined as "application/x-www-form-urlencoded".
74
+ #
75
+ def format(v)
76
+ case @format = v
77
+ when :text
78
+ set_content_type("text/plain")
79
+ when :xml
80
+ set_content_type("text/xml application/x-xml")
81
+ when :json
82
+ set_content_type("application/json")
83
+ when :yaml
84
+ set_content_type("text/plain text/yaml")
85
+ else
86
+ set_content_type("application/x-www-form-urlencoded")
87
+ end
88
+ end
89
+
90
+ # Sets text format message. No need to use +format+.
91
+ #
92
+ def body_as_text(text)
93
+ @message_body_with_format = text
94
+ format :text
95
+ end
96
+
97
+ # Sets xml format message. No need to use +format+.
98
+ #
99
+ def body_as_xml(xml)
100
+ @message_body_with_format = xml
101
+ format :xml
102
+ end
103
+
104
+ # Sets json format message. No need to use +format+.
105
+ #
106
+ def body_as_json(json)
107
+ @message_body_with_format = json
108
+ format :json
109
+ end
110
+
111
+ # Sets yaml format message. No need to use +format+.
112
+ #
113
+ def body_as_yaml(yaml)
114
+ @message_body_with_format = yaml
115
+ format :yaml
116
+ end
117
+
118
+ # Return converted message body, as to assigned format.
119
+ #
120
+ def format_message_body
121
+ return @message_body_with_format if @message_body_with_format
122
+
123
+ case @format
124
+ when :text
125
+ return @message_body.to_s
126
+ when :xml
127
+ return @message_body.to_xml @to_xml_options
128
+ when :json
129
+ return @message_body.to_json
130
+ when :yaml
131
+ return @message_body.to_yaml
132
+ else
133
+ @message_body.each do |k,v|
134
+ if v.kind_of? ActiveRecord::Base
135
+ @message_body[k] = v.attributes
136
+ end
137
+ end
138
+ return query_string(@message_body)
139
+ end
140
+ end
141
+
142
+ private
143
+ def query_string(hash)
144
+ build_query_string(hash, nil, nil)
145
+ end
146
+
147
+ def build_query_string(hash, query = nil, top = nil)
148
+ query ||= []
149
+ top ||= ""
150
+
151
+ _top = top.dup
152
+
153
+ hash.each do |k,v|
154
+ top = _top
155
+ top += top == "" ? "#{urlencode(k.to_s)}" : "[#{urlencode(k.to_s)}]"
156
+ if v.kind_of? Hash
157
+ build_query_string(v, query, top)
158
+ elsif v.kind_of? Array
159
+ v.each do |e|
160
+ query << "#{top}[]=#{urlencode(e.to_s)}"
161
+ end
162
+ else
163
+ query << "#{top}=#{urlencode(v.to_s)}"
164
+ end
165
+ end
166
+ query.join('&')
167
+ end
168
+
169
+ def simple_url_encoded_form_data(params, sep = '&')
170
+ params.map {|k,v| "#{urlencode(k.to_s)}=#{urlencode(v.to_s)}" }.join(sep)
171
+ end
172
+
173
+ def urlencode(str)
174
+ str.gsub(/[^a-zA-Z0-9_\.\-]/n) {|s| sprintf('%%%02x', s[0]) }
175
+ end
176
+
177
+ def set_content_type(type)
178
+ http_header("Content-type", type)
179
+ end
180
+ end
181
+ end