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.
@@ -2,28 +2,28 @@
2
2
  # = queue.rb - Reliable queue client API
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
  #
9
9
  #--
10
- # Changes:
11
10
  #++
12
11
 
13
12
  require 'drb'
13
+ require 'reliable-msg/client'
14
14
  require 'reliable-msg/selector'
15
15
 
16
16
 
17
17
  module ReliableMsg
18
18
 
19
- # == Reliable Messaging Client API
19
+
20
+ # == Queue client API
20
21
  #
21
22
  # Use the Queue object to put messages in queues, or get messages from queues.
22
23
  #
23
24
  # You can create a Queue object that connects to a single queue by passing the
24
25
  # queue name to the initialized. You can also access other queues by specifying
25
- # the destination queue when putting a message, or selecting from a queue when
26
- # retrieving the message.
26
+ # the destination queue when putting a message.
27
27
  #
28
28
  # For example:
29
29
  # queue = Queue.new 'my-queue'
@@ -39,78 +39,51 @@ module ReliableMsg
39
39
  # end
40
40
  #
41
41
  # See Queue.get and Queue.put for more examples.
42
- class Queue
43
-
44
- ERROR_INVALID_SELECTOR = 'Selector must be message identifier (String), set of header name/value pairs (Hash), or nil' # :nodoc:
45
-
46
- ERROR_INVALID_TX_TIMEOUT = 'Invalid transaction timeout: must be a non-zero positive integer' # :nodoc:
47
-
48
- ERROR_INVALID_CONNECT_COUNT = 'Invalid connection count: must be a non-zero positive integer' # :nodoc:
49
-
50
- ERROR_SELECTOR_VALUE_OR_BLOCK = 'You can either pass a Selector object, or use a block' # :nodoc:
51
-
52
- # The default DRb port used to connect to the queue manager.
53
- DRB_PORT = 6438
54
-
55
- DEFAULT_DRB_URI = "druby://localhost:#{DRB_PORT}" #:nodoc:
56
-
57
- # The name of the dead letter queue (<tt>DLQ</tt>). Messages that expire or fail
58
- # to process are automatically sent to the dead letter queue.
59
- DLQ = DEAD_LETTER_QUEUE = 'dlq'
60
-
61
- # Number of times to retry a connecting to the queue manager.
62
- DEFAULT_CONNECT_RETRY = 5
63
-
64
- # Default transaction timeout.
65
- DEFAULT_TX_TIMEOUT = 120
66
-
67
- # Default number of re-delivery attempts.
68
- DEFAULT_MAX_RETRIES = 4;
42
+ class Queue < Client
69
43
 
70
- # Thread.current entry for queue transaction.
71
- THREAD_CURRENT_TX = :reliable_msg_tx #:nodoc:
44
+ # Caches queue headers locally. Used by queues that retrieve a list of
45
+ # headers for their selectors, and can be shared by queue/selector
46
+ # objects operating on the same queue.
47
+ @@headers_cache = {} #:nodoc:
72
48
 
73
- # DRb URI for queue manager. You can override this to change the URI globally,
74
- # for all Queue objects that are not instantiated with an alternative URI.
75
- @@drb_uri = DEFAULT_DRB_URI
49
+ # Default number of delivery attempts.
50
+ DEFAULT_MAX_DELIVERIES = 5;
76
51
 
77
- # Reference to the local queue manager. Defaults to a DRb object, unless
78
- # the queue manager is running locally.
79
- @@qm = nil #:nodoc:
80
-
81
- # Cache of queue managers referenced by their URI.
82
- @@qm_cache = {} #:nodoc:
52
+ INIT_OPTIONS = [:expires, :delivery, :priority, :max_deliveries, :drb_uri, :tx_timeout, :connect_count]
83
53
 
84
54
  # The optional argument +queue+ specifies the queue name. The application can
85
55
  # still put messages in other queues by specifying the destination queue
86
- # name in the header, or get from other queues by specifying the queue name
87
- # in the selector.
88
- #
89
- # TODO: document options
90
- # * :expires
91
- # * :priority
92
- # * :max_retries
93
- # * :selector
94
- # * :drb_uri
95
- # * :tx_timeout
96
- # * :connect_count
56
+ # name in the header.
57
+ #
58
+ # The following options can be passed to the initializer:
59
+ # * <tt>:expires</tt> -- Message expiration in seconds. Default for new messages.
60
+ # * <tt>:delivery</tt> -- The message delivery mode. Default for new messages.
61
+ # * <tt>:priority</tt> -- The message priority. Default for new messages.
62
+ # * <tt>:max_deliveries</tt> -- Maximum number of attempts to deliver message.
63
+ # Default for new messages.
64
+ # * <tt>:drb_uri</tt> -- DRb URI for connecting to the queue manager. Only
65
+ # required when using a remote queue manager, or different port.
66
+ # * <tt>:tx_timeout</tt> -- Transaction timeout. See tx_timeout.
67
+ # * <tt>:connect_count</tt> -- Connection attempts. See connect_count.
97
68
  #
98
69
  # :call-seq:
99
70
  # Queue.new([name [,options]]) -> queue
100
71
  #
101
72
  def initialize queue = nil, options = nil
102
73
  options.each do |name, value|
74
+ raise RuntimeError, format(ERROR_INVALID_OPTION, name) unless INIT_OPTIONS.include?(name)
103
75
  instance_variable_set "@#{name.to_s}".to_sym, value
104
76
  end if options
105
77
  @queue = queue
106
78
  end
107
79
 
80
+
108
81
  # Put a message in the queue.
109
82
  #
110
83
  # The +message+ argument is required, but may be +nil+
111
84
  #
112
85
  # Headers are optional. Headers are used to provide the application with additional
113
- # information about the message, and can be used to retrieve messages (see Queue.put
86
+ # information about the message, and can be used to retrieve messages (see Queue.get
114
87
  # for discussion of selectors). Some headers are used to handle message processing
115
88
  # internally (e.g. <tt>:priority</tt>, <tt>:expires</tt>).
116
89
  #
@@ -121,15 +94,15 @@ module ReliableMsg
121
94
  # The following headers have special meaning:
122
95
  # * <tt>:delivery</tt> -- The message delivery mode.
123
96
  # * <tt>:queue</tt> -- Puts the message in the named queue. Otherwise, uses the queue
124
- # specified when creating the Queue object.
97
+ # specified when creating this Queue object.
125
98
  # * <tt>:priority</tt> -- The message priority. Messages with higher priority are
126
99
  # retrieved first.
127
100
  # * <tt>:expires</tt> -- Message expiration in seconds. Messages do not expire unless
128
101
  # specified. Zero or +nil+ means no expiration.
129
102
  # * <tt>:expires_at</tt> -- Specifies when the message expires (timestamp). Alternative
130
103
  # to <tt>:expires</tt>.
131
- # * <tt>:max_retries</tt> -- Maximum number of attempts to re-deliver message, afterwhich
132
- # message moves to the DLQ. Minimum is 0 (deliver only once), default is 4 (deliver
104
+ # * <tt>:max_deliveries</tt> -- Maximum number of attempts to deliver message, afterwhich
105
+ # message moves to the DLQ. Minimum is 1 (deliver only once), default is 5 (deliver
133
106
  # up to 5 times).
134
107
  #
135
108
  # Headers can be set on a per-queue basis when the Queue is created. This only affects
@@ -139,8 +112,8 @@ module ReliableMsg
139
112
  # * <tt>:best_effort</tt> -- Attempt to deliver the message once. If the message expires or
140
113
  # cannot be delivered, discard the message. The is the default delivery mode.
141
114
  # * <tt>:repeated</tt> -- Attempt to deliver until message expires, or up to maximum
142
- # re-delivery count (see <tt>:max_retries</tt>). Afterwards, move message to dead-letter
143
- # queue.
115
+ # delivery attempts (see <tt>:max_deliveries</tt>). Afterwards, move message to
116
+ # dead-letter queue.
144
117
  # * <tt>:once</tt> -- Attempt to deliver message exactly once. If message expires, or
145
118
  # first delivery attempt fails, move message to dead-letter queue.
146
119
  #
@@ -155,10 +128,13 @@ module ReliableMsg
155
128
  def put message, headers = nil
156
129
  tx = Thread.current[THREAD_CURRENT_TX]
157
130
  # Use headers supplied by callers, or defaults for this queue.
158
- headers ||= {}
159
- headers.fetch(:priority, @priority || 0)
160
- headers.fetch(:expires, @expires)
161
- headers.fetch(:max_retries, @max_retries || DEFAULT_MAX_RETRIES)
131
+ defaults = {
132
+ :priority => @priority || 0,
133
+ :expires => @expires,
134
+ :max_deliveries => @max_deliveries || DEFAULT_MAX_DELIVERIES,
135
+ :delivery => @delivery || :best_effort
136
+ }
137
+ headers = headers ? defaults.merge(headers) : defaults
162
138
  # Serialize the message before sending to queue manager. We need the
163
139
  # message to be serialized for storage, this just saves duplicate
164
140
  # serialization when using DRb.
@@ -172,6 +148,7 @@ module ReliableMsg
172
148
  end
173
149
  end
174
150
 
151
+
175
152
  # Get a message from the queue.
176
153
  #
177
154
  # Call with no arguments to retrieve the next message in the queue. Call with a message
@@ -179,30 +156,26 @@ module ReliableMsg
179
156
  # that matches.
180
157
  #
181
158
  # Selectors specify which headers to match. For example, to retrieve all messages in the
182
- # queue 'my-queue' with priority 2:
183
- # msg = queue.get :queue=>'my-queue', :priority=>2
159
+ # with priority 2:
160
+ # msg = queue.get :priority=>2
184
161
  # To put and get the same message:
185
162
  # mid = queue.put obj
186
163
  # msg = queue.get mid # or queue.get :id=>mid
187
164
  # assert(msg.obj == obj)
188
165
  #
189
166
  # More complex selector expressions can be generated using Queue.selector. For example,
190
- # to retrieve the next message with priority 2 or higher, received in the last 60 seconds:
191
- # selector = Queue.selector { priority >= 2 and received > Time.new.to_i - 60 }
167
+ # to retrieve the next message with priority 2 or higher, created in the last 60 seconds:
168
+ # selector = Queue.selector { priority >= 2 && created > now - 60 }
192
169
  # msg = queue.get selector
193
- # You can also specify selectors for a Queue to be used by default for all Queue.get calls
194
- # on that Queue object. For example:
195
- # queue.selector= { priority >= 2 and received > Time.new.to_i - 60 }
196
- # msg = queue.get # default selector applies
197
170
  #
198
171
  # The following headers have special meaning:
199
172
  # * <tt>:id</tt> -- The message identifier.
200
173
  # * <tt>:queue</tt> -- Select a message originally delivered to the named queue. Only used
201
174
  # when retrieving messages from the dead-letter queue.
202
- # * <tt>:retry</tt> -- Specifies the retry count for the message. Zero when the message is
203
- # first delivered, and incremented after each re-delivery attempt.
175
+ # * <tt>:redelivery</tt> -- Specifies the re-delivery count for this message. Nil if the
176
+ # message is delivered (get) for the first time, one on the first attempt to re-deliver,
177
+ # and incremented once for each subsequent attempt.
204
178
  # * <tt>:created</tt> -- Indicates timestamp (in seconds) when the message was created.
205
- # * <tt>:received</tt> -- Indicates timestamp (in seconds) when the message was received.
206
179
  # * <tt>:expires_at</tt> -- Indicates timestamp (in seconds) when the message will expire,
207
180
  # +nil+ if the message does not expire.
208
181
  #
@@ -265,13 +238,27 @@ module ReliableMsg
265
238
  selector = case selector
266
239
  when String
267
240
  {:id=>selector}
268
- when Hash, Array, Selector
241
+ when Hash, Selector, nil
269
242
  selector
270
- when nil
271
- @selector
272
243
  else
273
244
  raise ArgumentError, ERROR_INVALID_SELECTOR
274
245
  end
246
+ # If using selector object, obtain a list of all message headers
247
+ # for the queue (shared by all Queue/Selector objects accessing
248
+ # the same queue) and run the selector on that list. Pick one
249
+ # message and switch to an :id selector to retrieve it.
250
+ if selector.is_a?(Selector)
251
+ cached = @@headers_cache[@queue] ||= CachedHeaders.new
252
+ id = cached.next(selector) do
253
+ if tx
254
+ tx[:qm].list :queue=>@queue, :tid=>tx[:tid]
255
+ else
256
+ repeated { |qm| qm.list :queue=>@queue }
257
+ end
258
+ end
259
+ return nil unless id
260
+ selector = {:id=>id}
261
+ end
275
262
  # If inside a transaction, always retrieve from the same queue manager,
276
263
  # otherwise, allow repeated() to try and access multiple queue managers.
277
264
  message = if tx
@@ -305,196 +292,56 @@ module ReliableMsg
305
292
  result
306
293
  end
307
294
 
308
- # Returns the transaction timeout (in seconds).
309
- #
310
- # :call-seq:
311
- # queue.tx_timeout -> numeric
312
- #
313
- def tx_timeout
314
- @tx_timeout || DEFAULT_TX_TIMEOUT
315
- end
316
-
317
- # Sets the transaction timeout (in seconds). Affects future transactions started
318
- # by Queue.get. Use +nil+ to restore the default timeout.
319
- #
320
- # :call-seq:
321
- # queue.tx_timeout = timeout
322
- # queue.tx_timeout = nil
323
- #
324
- def tx_timeout= timeout
325
- if timeout
326
- raise ArgumentError, ERROR_INVALID_TX_TIMEOUT unless timeout.instance_of?(Integer) and timeout > 0
327
- @tx_timeout = timeout
328
- else
329
- @tx_timeout = nil
330
- end
331
- end
332
-
333
- # Returns the number of connection attempts, before operations fail.
334
- #
335
- # :call-seq:
336
- # queue.connect_count -> numeric
337
- #
338
- def connect_count
339
- @connect_count || DEFAULT_CONNECT_RETRY
340
- end
341
-
342
- # Sets the number of connection attempts, before operations fail. The minimum is one.
343
- # Use +nil+ to restore the default connection count.
344
- #
345
- # :call-seq:
346
- # queue.connect_count = count
347
- # queue.connect_count = nil
348
- #
349
- def connect_count= count
350
- if count
351
- raise ArgumentError, ERROR_INVALID_CONNECT_COUNT unless count.instance_of?(Integer) and count > 0
352
- @connect_count = count
353
- else
354
- @connect_count = nil
355
- end
356
- end
357
295
 
358
- # If called with no block, returns the selector associated with this Queue
359
- # (see Queue.selector=). If called with a block, creates and returns a new
360
- # selector (similar to Queue::selector).
361
- #
362
- # :call-seq:
363
- # queue.selector -> selector
364
- # queue.selector { ... } -> selector
365
- #
366
- def selector &block
367
- block ? Selector.new(&block) : @selector
368
- end
369
-
370
- # Sets a default selector for this Queue. Affects all calls to Queue.get on this
371
- # Queue object that do not specify a selector.
372
- #
373
- # You can pass a Selector object, a block expression, or +nil+ if you no longer
374
- # want to use the default selector. For example:
375
- # queue.selector= { priority >= 2 and received > Time.new.to_i - 60 }
376
- # 10.times do
377
- # p queue.get
378
- # end
379
- # queue.selector= nil
380
- #
381
- # :call-seq:
382
- # queue.selector = selector
383
- # queue.selector = { ... }
384
- # queue.selector = nil
385
- #
386
- def selector= value = nil, &block
387
- raise ArgumentError, ERROR_SELECTOR_VALUE_OR_BLOCK if (value && block)
388
- if value
389
- raise ArgumentError, ERROR_SELECTOR_VALUE_OR_BLOCK unless value.instance_of?(Selector)
390
- @selector = value
391
- elsif block
392
- @selector = Selector.new &block
393
- else
394
- @selector = nil
395
- end
396
- end
397
-
398
- # Create and return a new selector based on the block expression. For example:
399
- # selector = Queue.selector { priority >= 2 and received > Time.new.to_i - 60 }
400
- #
401
- # :call-seq:
402
- # Queue.selector { ... } -> selector
403
- #
404
- def self.selector &block
405
- raise ArgumentError, ERROR_NO_SELECTOR_BLOCK unless block
406
- Selector.new &block
407
- end
408
-
409
- private
410
-
411
- # Returns the active queue manager. You can override this method to implement
412
- # load balancing.
413
- def qm
414
- if uri = @drb_uri
415
- # Queue specifies queue manager's URI: use that queue manager.
416
- @@qm_cache[uri] ||= DRbObject.new(nil, uri)
417
- else
418
- # Use the same queue manager for all queues, and cache it.
419
- # Create only the first time.
420
- @@qm ||= DRbObject.new(nil, @@drb_uri || DEFAULT_DRB_URI)
421
- end
422
- end
423
-
424
- # Called to execute the operation repeatedly and avoid connection failures. This only
425
- # makes sense if we have a load balancing algorithm.
426
- def repeated &block
427
- count = connect_count
428
- begin
429
- block.call qm
430
- rescue DRb::DRbConnError=>error
431
- warn error
432
- warn error.backtrace
433
- retry if (count -= 1) > 0
434
- raise error
435
- end
436
- end
437
-
438
- class << self
439
- private
440
- # Sets the active queue manager. Used when the queue manager is running in the
441
- # same process to bypass DRb calls.
442
- def qm= qm
443
- @@qm = qm
444
- end
296
+ # Returns the queue name.
297
+ def name
298
+ @queue
445
299
  end
446
300
 
447
301
  end
448
302
 
449
303
 
450
- # == Retrieved Message
451
- #
452
- # Returned from Queue.get holding the last message retrieved from the
453
- # queue and providing access to the message identifier, headers and object.
454
- #
455
- # For example:
456
- # while queue.get do |msg|
457
- # print "Message #{msg.id}"
458
- # print "Headers: #{msg.headers.inspect}"
459
- # print msg.object
460
- # true
461
- # end
462
- class Message
304
+ # Locally cached headers for a queue. Used with the Selector object to
305
+ # retrieve the headers once, and share them. This effectively acts as a
306
+ # cursor into the queue, and saves I/O by retrieving a new list only
307
+ # when it's empty.
308
+ class CachedHeaders #:nodoc:
463
309
 
464
-
465
- def initialize id, headers, object # :nodoc:
466
- @id, @object, @headers = id, object, headers
310
+ def initialize
311
+ @list = nil
312
+ @mutex = Mutex.new
467
313
  end
468
314
 
469
- # Returns the message identifier.
470
- #
471
- # :call-seq:
472
- # msg.id -> id
473
- #
474
- def id
475
- @id
476
- end
477
315
 
478
- # Returns the message object.
316
+ # Find the next matching message in the queue based on the
317
+ # selector. The argument is a Selector object which filters out
318
+ # messages. The block is called to load a list of headers from
319
+ # the queue manager, returning an Array of headers (Hash).
320
+ # Returns the identifier of the first message found.
479
321
  #
480
322
  # :call-seq:
481
- # msg.object -> obj
482
- #
483
- def object
484
- @object
485
- end
486
-
487
- # Returns the message headers.
488
- #
489
- # :call-seq:
490
- # msg.headers -> hash
491
- #
492
- def headers
493
- @headers
323
+ # obj.next(selector) { } -> id or nil
324
+ #
325
+ def next selector, &block
326
+ load = false
327
+ @mutex.synchronize do
328
+ load ||= (@list.nil? || @list.empty?)
329
+ @list = block.call() if load
330
+ @list.each_with_index do |headers, idx|
331
+ if selector.match headers
332
+ @list.delete_at idx
333
+ return headers[:id]
334
+ end
335
+ end
336
+ unless load
337
+ load = true
338
+ retry
339
+ end
340
+ end
341
+ return nil
494
342
  end
495
343
 
496
344
  end
497
345
 
498
-
499
346
  end
500
347