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