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.
@@ -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