reliable-msg 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+