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.
- data/README +5 -1
- data/Rakefile +30 -23
- data/changelog.txt +32 -0
- data/lib/reliable-msg.rb +10 -5
- data/lib/reliable-msg/cli.rb +47 -3
- data/lib/reliable-msg/client.rb +213 -0
- data/lib/reliable-msg/message-store.rb +128 -49
- data/lib/reliable-msg/mysql.sql +7 -1
- data/lib/reliable-msg/queue-manager.rb +263 -58
- data/lib/reliable-msg/queue.rb +100 -253
- data/lib/reliable-msg/rails.rb +114 -0
- data/lib/reliable-msg/selector.rb +65 -75
- data/lib/reliable-msg/topic.rb +215 -0
- data/test/test-queue.rb +35 -5
- data/test/test-rails.rb +59 -0
- data/test/test-topic.rb +102 -0
- metadata +54 -41
- data/lib/uuid.rb +0 -384
- data/test/test-uuid.rb +0 -48
data/lib/reliable-msg/queue.rb
CHANGED
@@ -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/
|
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
|
-
|
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
|
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
|
-
#
|
71
|
-
|
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
|
-
#
|
74
|
-
|
75
|
-
@@drb_uri = DEFAULT_DRB_URI
|
49
|
+
# Default number of delivery attempts.
|
50
|
+
DEFAULT_MAX_DELIVERIES = 5;
|
76
51
|
|
77
|
-
|
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
|
87
|
-
#
|
88
|
-
#
|
89
|
-
#
|
90
|
-
# *
|
91
|
-
# *
|
92
|
-
# *
|
93
|
-
#
|
94
|
-
# *
|
95
|
-
#
|
96
|
-
# *
|
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.
|
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
|
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>:
|
132
|
-
# message moves to the DLQ. Minimum is
|
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
|
-
#
|
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
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
-
#
|
183
|
-
# msg = queue.get :
|
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,
|
191
|
-
# selector = Queue.selector { priority >= 2
|
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>:
|
203
|
-
# first
|
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,
|
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
|
-
#
|
359
|
-
|
360
|
-
|
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
|
-
#
|
451
|
-
#
|
452
|
-
#
|
453
|
-
#
|
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
|
-
|
466
|
-
@
|
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
|
-
#
|
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
|
-
#
|
482
|
-
#
|
483
|
-
def
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
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
|
|