reliable-msg 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,114 @@
1
+ require "action_controller"
2
+
3
+ module ActionController #:nodoc:
4
+
5
+ class Base
6
+
7
+ # Convenience method for accessing queues from your Rails controller.
8
+ #
9
+ # Use this method in your controller class to create an attribute for accessing
10
+ # the named queue. The method can be called in one of three ways:
11
+ # * With a +Symbol+. Adds the named attribute to access a queue with the same name.
12
+ # * With a +String+. Adds the attribute <tt>queue</tt> to access the named queue.
13
+ # * With a +Symbol+ and a +String+. Adds the named attribute to access the named
14
+ # queue.
15
+ #
16
+ # For example
17
+ # queue 'default'
18
+ #
19
+ # def index
20
+ # queue.put "some message"
21
+ # render :text => "added message to queue 'default'"
22
+ # end
23
+ #
24
+ # :call-seq:
25
+ # queue symbol
26
+ # queue symbol, name
27
+ # queue name
28
+ #
29
+ def self.queue *args
30
+ raise ArgumentError, "Expecting a Symbol specifying the attribute name, a String specifying the queue name, or both" unless args.length > 0 && args.length <= 2
31
+ attr = name = nil
32
+ case args[0]
33
+ when String
34
+ raise ArgumentError, "When first argument is a String specifying the queue name, expecting to find only one argument" unless args.length == 1
35
+ attr = :queue
36
+ name = args[0]
37
+ when Symbol
38
+ attr = args[0]
39
+ if args.length == 1
40
+ name = args[0].to_s
41
+ else
42
+ raise ArgumetnError, "When first argument is a Symbol specifying the attribute name, expecting the second argument to be a String specifying the queue name or absent" unless args[1].instance_of?(String)
43
+ name = args[1]
44
+ end
45
+ else
46
+ raise ArgumentError, "Expecting first argument to be a Symbol or a String"
47
+ end
48
+ quoted = "\"" << name.gsub("\"", "\\\"") << "\""
49
+
50
+ module_eval(<<-EOS, __FILE__, __LINE__ + 1)
51
+ @@queue_#{attr.to_s} = ReliableMsg::Queue.new(#{quoted})
52
+ def #{attr.to_s}(*args)
53
+ raise ArgumentError, "Attribute #{attr} is accessed without any arguments, e.g. #{attr.to_s}.put(msg)" unless args.length == 0
54
+ @@queue_#{attr.to_s}
55
+ end
56
+ EOS
57
+ end
58
+
59
+
60
+ # Convenience method for accessing topics from your Rails controller.
61
+ #
62
+ # Use this method in your controller class to create an attribute for accessing
63
+ # the named topic. The method can be called in one of three ways:
64
+ # * With a +Symbol+. Adds the named attribute to access a topic with the same name.
65
+ # * With a +String+. Adds the attribute <tt>topic</tt> to access the named topic.
66
+ # * With a +Symbol+ and a +String+. Adds the named attribute to access the named
67
+ # topic.
68
+ #
69
+ # For example
70
+ # topic :notification
71
+ #
72
+ # def index
73
+ # :notification.put "something new"
74
+ # render :text => "added message to topic 'notification'"
75
+ # end
76
+ #
77
+ # :call-seq:
78
+ # topic symbol
79
+ # topic symbol, name
80
+ # topic name
81
+ #
82
+ def self.topic *args
83
+ raise ArgumentError, "Expecting a Symbol specifying the attribute name, a String specifying the topic name, or both" unless args.length > 0 && args.length <= 2
84
+ attr = name = nil
85
+ case args[0]
86
+ when String
87
+ raise ArgumentError, "When first argument is a String specifying the topic name, expecting to find only one argument" unless args.length == 1
88
+ attr = :topic
89
+ name = args[0]
90
+ when Symbol
91
+ attr = args[0]
92
+ if args.length == 1
93
+ name = args[0].to_s
94
+ else
95
+ raise ArgumetnError, "When first argument is a Symbol specifying the attribute name, expecting the second argument to be a String specifying the topic name or absent" unless args[1].instance_of?(String)
96
+ name = args[1]
97
+ end
98
+ else
99
+ raise ArgumentError, "Expecting first argument to be a Symbol or a String"
100
+ end
101
+ quoted = "\"" << name.gsub("\"", "\\\"") << "\""
102
+
103
+ module_eval(<<-EOS, __FILE__, __LINE__ + 1)
104
+ @@topic_#{attr.to_s} = ReliableMsg::Topic.new(#{quoted})
105
+ def #{attr.to_s}(*args)
106
+ raise ArgumentError, "Attribute #{attr} is accessed without any arguments, e.g. #{attr.to_s}.put(msg)" unless args.length == 0
107
+ @@topic_#{attr.to_s}
108
+ end
109
+ EOS
110
+ end
111
+
112
+ end
113
+
114
+ end
@@ -2,7 +2,7 @@
2
2
  # = selector.rb - Deferred expression evaluation selector
3
3
  #
4
4
  # Author:: Assaf Arkin assaf@labnotes.org
5
- # Documentation:: http://trac.labnotes.org/cgi-bin/trac.cgi/wiki/RubyReliableMessaging
5
+ # Documentation:: http://trac.labnotes.org/cgi-bin/trac.cgi/wiki/Ruby/ReliableMessaging
6
6
  # Copyright:: Copyright (c) 2005 Assaf Arkin
7
7
  # License:: MIT and/or Creative Commons Attribution-ShareAlike
8
8
  #
@@ -10,98 +10,88 @@
10
10
  # presentation on domain specific languages, and the BlankSlate source code.
11
11
  #
12
12
  #--
13
- # Changes:
14
13
  #++
15
14
 
16
15
  module ReliableMsg #:nodoc:
17
16
 
17
+ # A message selector is used to retrieve specific messages from the queue
18
+ # by matching the message headers.
19
+ #
20
+ # The selector matches messages by calling the block for each potential
21
+ # message. The block can access (read-only) message headers by calling
22
+ # methods with the same name, or using <tt>[:symbol]</tt>. It returns true
23
+ # if a match is found.
24
+ #
25
+ # The following three examples are equivalent:
26
+ # selector = Queue::selector { priority > 2 }
27
+ # selector = queue.selector { priority > 2 }
28
+ # selector = Selector.new { [:priority] > 2 }
29
+ #
30
+ # The new function is always available and evaluates to the current time
31
+ # (in seconds from the Epoch).
32
+ #
33
+ # This example uses the delivery count and message creation date/time to
34
+ # implement a simple retry with back-out:
35
+ #
36
+ # MINUTE = 60
37
+ # HOUR = MINUTE * 60
38
+ # BACKOUT = [ 5 * MINUTE, HOUR, 4 * HOUR, 12 * HOUR ]
39
+ #
40
+ # selector = Queue::selector { delivered == 0 || BACKOUT[delivered - 1] + created <= now }
18
41
  class Selector
19
42
 
20
- # We're using DRb. Unless we support respond_to? and instance_eval?, DRb will
21
- # refuse to marshal the selector as an argument and attempt to create a remote
22
- # object instead.
23
- instance_methods.each { |name| undef_method name unless name =~ /^(__.*__)|respond_to\?|instance_eval$/ }
24
-
25
- def initialize &block
26
- # Call the block and hold the deferred value.
27
- @value = self.instance_eval &block
28
- end
29
-
30
- def method_missing symbol, *args
31
- if symbol == :__evaluate__
32
- # Evaluate the selector with the headers passed in the argument.
33
- @value.is_a?(Deferred) ? @value.__evaluate__(*args) : @value
34
- else
35
- # Create a deferred value for the missing method (a header).
36
- raise ArgumentError, "Can't pass arguments to header" unless args.empty?
37
- Header.new symbol
38
- end
39
- end
40
-
41
-
42
- class Deferred #:nodoc:
43
-
44
- # We're using DRb. Unless we support respond_to? and instance_eval?, DRb will
45
- # refuse to marshal the selector as an argument and attempt to create a remote
46
- # object instead.
47
- instance_methods.each { |name| undef_method name unless name =~ /^(__.*__)|respond_to\?|instance_eval$/ }
48
-
49
- def initialize target, operation, args
50
- @target = target
51
- @operation = operation
52
- @args = args
53
- end
54
-
55
- def coerce value
56
- [Constant.new(value), self]
57
- end
58
-
59
- def method_missing symbol, *args
60
- if symbol == :__evaluate__
61
-
62
- eval_args = @args.collect { |arg| arg.instance_of?(Deferred) ? arg.__evaluate__(*args) : arg }
63
- @target.__evaluate__(*args).send @operation, *eval_args
64
- else
65
- Deferred.new self, symbol, args
66
- end
67
- end
43
+ ERROR_INVALID_SELECTOR_BLOCK = "Selector must be created with a block accepting no arguments" #:nodoc:
68
44
 
45
+ # Create a new selector that evaluates by calling the block.
46
+ #
47
+ # :call-seq:
48
+ # Selector.new { |headers| ... } -> selector
49
+ #
50
+ def initialize &block
51
+ raise ArgumentError, ERROR_INVALID_SELECTOR_BLOCK unless block && block.arity < 1
52
+ @block = block
69
53
  end
70
54
 
71
- class Header < Deferred #:nodoc:
72
55
 
73
- def initialize name
74
- @name = name
75
- end
56
+ # Matches the message headers with the selectors. Returns true
57
+ # if a match is made, false otherwise. May raise an error if
58
+ # there's an error in the expression.
59
+ #
60
+ # :call-seq:
61
+ # selector.match(headers) -> boolean
62
+ #
63
+ def match headers #:nodoc:
64
+ context = EvaluationContext.new headers
65
+ context.instance_eval(&@block)
66
+ end
67
+
68
+
69
+ end
70
+
76
71
 
77
- def coerce value
78
- [Constant.new(value), self]
79
- end
72
+ class EvaluationContext #:nodoc:
80
73
 
81
- def method_missing symbol, *args
82
- if symbol == :__evaluate__
83
- args[0][@name]
84
- else
85
- Deferred.new self, symbol, args
86
- end
87
- end
74
+ instance_methods.each { |name| undef_method name unless name =~ /^(__.*__)|instance_eval$/ }
88
75
 
76
+
77
+ def initialize headers
78
+ @headers = headers
89
79
  end
90
80
 
91
- class Constant < Deferred #:nodoc:
92
81
 
93
- def initialize value
94
- @value = value
95
- end
82
+ def now
83
+ Time.now.to_i
84
+ end
85
+
86
+
87
+ def [] symbol
88
+ @headers[symbol]
89
+ end
96
90
 
97
- def method_missing symbol, *args
98
- if symbol == :__evaluate__
99
- @value
100
- else
101
- Deferred.new self, symbol, args
102
- end
103
- end
104
91
 
92
+ def method_missing symbol, *args, &block
93
+ raise ArgumentError, "Wrong number of arguments (#{args.length} for 0)" unless args.empty?
94
+ @headers[symbol]
105
95
  end
106
96
 
107
97
  end
@@ -0,0 +1,215 @@
1
+ #
2
+ # = topic.rb - Publish to topic API
3
+ #
4
+ # Author:: Assaf Arkin assaf@labnotes.org
5
+ # Documentation:: http://trac.labnotes.org/cgi-bin/trac.cgi/wiki/Ruby/ReliableMessaging
6
+ # Copyright:: Copyright (c) 2005 Assaf Arkin
7
+ # License:: MIT and/or Creative Commons Attribution-ShareAlike
8
+ #
9
+ #--
10
+ #++
11
+
12
+ require 'drb'
13
+ require 'reliable-msg/client'
14
+ require 'reliable-msg/selector'
15
+
16
+
17
+ module ReliableMsg
18
+
19
+ # == Pub/Sub Topic API
20
+ #
21
+ # Use the Topic object to publish a message on a topic, get messages from topics.
22
+ #
23
+ # You can create a Topic object that connects to a single topic by passing the
24
+ # topic name to the initialized. You can also access other topics by specifying
25
+ # the destination topic when putting a message.
26
+ #
27
+ # For example:
28
+ # topic = Topic.new 'my-topic'
29
+ # # Publish a message on the topic, expiring in 30 seconds.
30
+ # msg = 'lorem ipsum'
31
+ # mid = topic.put msg, :expires=>30
32
+ # # Retrieve and process a message on the topic.
33
+ # topic.get do |msg|
34
+ # if msg.id == mid
35
+ # print "Retrieved same message"
36
+ # end
37
+ # print "Message text: #{msg.object}"
38
+ # end
39
+ #
40
+ # See Topic.get and Topic.put for more examples.
41
+ class Topic < Client
42
+
43
+ INIT_OPTIONS = [:expires, :drb_uri, :tx_timeout, :connect_count]
44
+
45
+ # The optional argument +topic+ specifies the topic name. The application can
46
+ # still publish messages on other topics by specifying the destination topics
47
+ # name in the header.
48
+ #
49
+ # The following options can be passed to the initializer:
50
+ # * <tt>:expires</tt> -- Message expiration in seconds. Default for new messages.
51
+ # * <tt>:drb_uri</tt> -- DRb URI for connecting to the queue manager. Only
52
+ # required when using a remote queue manager, or different port.
53
+ # * <tt>:tx_timeout</tt> -- Transaction timeout. See tx_timeout.
54
+ # * <tt>:connect_count</tt> -- Connection attempts. See connect_count.
55
+ #
56
+ # :call-seq:
57
+ # Topic.new([name [,options]]) -> topic
58
+ #
59
+ def initialize topic = nil, options = nil
60
+ options.each do |name, value|
61
+ raise RuntimeError, format(ERROR_INVALID_OPTION, name) unless INIT_OPTIONS.include?(name)
62
+ instance_variable_set "@#{name.to_s}".to_sym, value
63
+ end if options
64
+ @topic = topic
65
+ @seen = nil
66
+ end
67
+
68
+
69
+ # Publish a message on the topic.
70
+ #
71
+ # The +message+ argument is required, but may be +nil+
72
+ #
73
+ # Headers are optional. Headers are used to provide the application with additional
74
+ # information about the message, and can be used to retrieve messages (see Topic.get
75
+ # for discussion of selectors). Some headers are used to handle message processing
76
+ # internally (e.g. <tt>:expires</tt>).
77
+ #
78
+ # Each header uses a symbol for its name. The value may be string, numeric, true/false
79
+ # or nil. No other objects are allowed. To improve performance, keep headers as small
80
+ # as possible.
81
+ #
82
+ # The following headers have special meaning:
83
+ # * <tt>:topic</tt> -- Publish the onn the named topic. Otherwise, uses the topic
84
+ # specified when creating this Topic object.
85
+ # * <tt>:expires</tt> -- Message expiration in seconds. Messages do not expire unless
86
+ # specified. Zero or +nil+ means no expiration.
87
+ # * <tt>:expires_at</tt> -- Specifies when the message expires (timestamp). Alternative
88
+ # to <tt>:expires</tt>.
89
+ #
90
+ # Headers can be set on a per-topic basis when the Topic is created. This only affects
91
+ # messages put through that Topic object.
92
+ #
93
+ # For example:
94
+ # topic.put updates
95
+ # topic.put notice, :expires=>10
96
+ # topic.put object, :topic=>'other-topic'
97
+ #
98
+ # :call-seq:
99
+ # topic.put(message[, headers])
100
+ #
101
+ def put message, headers = nil
102
+ tx = Thread.current[THREAD_CURRENT_TX]
103
+ # Use headers supplied by callers, or defaults for this topic.
104
+ defaults = {
105
+ :expires=> @expires
106
+ }
107
+ headers = headers ? defaults.merge(headers) : defaults
108
+ # Serialize the message before sending to queue manager. We need the
109
+ # message to be serialized for storage, this just saves duplicate
110
+ # serialization when using DRb.
111
+ message = Marshal::dump message
112
+ # If inside a transaction, always send to the same queue manager, otherwise,
113
+ # allow repeated() to try and access multiple queue managers.
114
+ if tx
115
+ tx[:qm].publish(:message=>message, :headers=>headers, :topic=>(headers[:topic] || @topic), :tid=>tx[:tid])
116
+ else
117
+ repeated { |qm| qm.publish :message=>message, :headers=>headers, :topic=>(headers[:topic] || @topic) }
118
+ end
119
+ end
120
+
121
+
122
+ # Get a message on the topic.
123
+ #
124
+ # Call with no arguments to retrieve the last message published on the topic. Call with
125
+ # selectors to retrieve only matching messages. See also Queue.get.
126
+ #
127
+ # The following headers have special meaning:
128
+ # * <tt>:id</tt> -- The message identifier.
129
+ # * <tt>:created</tt> -- Indicates timestamp (in seconds) when the message was created.
130
+ # * <tt>:expires_at</tt> -- Indicates timestamp (in seconds) when the message will expire,
131
+ # +nil+ if the message does not expire.
132
+ #
133
+ # Call this method without a block to return the message. The returned object is of type
134
+ # Message, or +nil+ if no message is found.
135
+ #
136
+ # Call this method in a block to retrieve and process the message. The block is called with
137
+ # the Message object, returning the result of the block. Returns +nil+ if no message is found.
138
+ #
139
+ # All operations performed on the topic inside the block are part of the same transaction.
140
+ # See Queue.get for discussion about transactions. Note that retry counts and delivery modes
141
+ # do not apply to Topics. A message remains published on the topic until replaced by another
142
+ # message.
143
+ #
144
+ # :call-seq:
145
+ # topic.get([selector]) -> msg or nil
146
+ # topic.get([selector]) {|msg| ... } -> obj
147
+ #
148
+ # See: Message
149
+ #
150
+ def get selector = nil, &block
151
+ tx = old_tx = Thread.current[THREAD_CURRENT_TX]
152
+ # If block, begin a new transaction.
153
+ if block
154
+ tx = {:qm=>qm}
155
+ tx[:tid] = tx[:qm].begin tx_timeout
156
+ Thread.current[THREAD_CURRENT_TX] = tx
157
+ end
158
+ result = begin
159
+ # Validate the selector: nil or hash.
160
+ selector = case selector
161
+ when Hash, Selector, nil
162
+ selector
163
+ else
164
+ raise ArgumentError, ERROR_INVALID_SELECTOR
165
+ end
166
+ # If inside a transaction, always retrieve from the same queue manager,
167
+ # otherwise, allow repeated() to try and access multiple queue managers.
168
+ message = if tx
169
+ tx[:qm].retrieve :seen=>@seen, :topic=>@topic, :selector=>(selector.is_a?(Selector) ? nil : selector), :tid=>tx[:tid]
170
+ else
171
+ repeated { |qm| qm.retrieve :seen=>@seen, :topic=>@topic, :selector=>(selector.is_a?(Selector) ? nil : selector) }
172
+ end
173
+ # Result is either message, or result from processing block. Note that
174
+ # calling block may raise an exception. We deserialize the message here
175
+ # for two reasons:
176
+ # 1. It creates a distinct copy, so changing the message object and returning
177
+ # it to the queue (abort) does not affect other consumers.
178
+ # 2. The message may rely on classes known to the client but not available
179
+ # to the queue manager.
180
+ result = if message
181
+ # Do not process message unless selector matches. Do not mark
182
+ # message as seen either, since we may retrieve it if the selector
183
+ # changes.
184
+ if selector.is_a?(Selector)
185
+ return nil unless selector.match message[:headers]
186
+ end
187
+ @seen = message[:id]
188
+ message = Message.new(message[:id], message[:headers], Marshal::load(message[:message]))
189
+ block ? block.call(message) : message
190
+ end
191
+ rescue Exception=>error
192
+ # Abort the transaction if we started it. Propagate error.
193
+ qm.abort(tx[:tid]) if block
194
+ raise error
195
+ ensure
196
+ # Resume the old transaction.
197
+ Thread.current[THREAD_CURRENT_TX] = old_tx if block
198
+ end
199
+ # Commit the transaction and return the result. We do this outside the main
200
+ # block, since we don't abort in case of error (commit is one-phase) and we
201
+ # don't retain the transaction association, it completes by definition.
202
+ qm.commit(tx[:tid]) if block
203
+ result
204
+ end
205
+
206
+
207
+ # Returns the topic name.
208
+ def name
209
+ @topic
210
+ end
211
+
212
+ end
213
+
214
+ end
215
+