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.
@@ -1,4 +1,3 @@
1
- DROP TABLE IF EXISTS `reliable_msg_queues`;
2
1
  CREATE TABLE `reliable_msg_queues` (
3
2
  `id` varchar(255) NOT NULL default '',
4
3
  `queue` varchar(255) NOT NULL default '',
@@ -6,3 +5,10 @@ CREATE TABLE `reliable_msg_queues` (
6
5
  `object` blob NOT NULL,
7
6
  PRIMARY KEY (`id`)
8
7
  ) ENGINE=InnoDB DEFAULT CHARSET=binary;
8
+
9
+ CREATE TABLE `reliable_msg_topics` (
10
+ `topic` varchar(255) NOT NULL default '',
11
+ `headers` text NOT NULL,
12
+ `object` blob NOT NULL,
13
+ PRIMARY KEY (`topic`)
14
+ ) ENGINE=InnoDB DEFAULT CHARSET=binary;
@@ -2,14 +2,11 @@
2
2
  # = queue-manager.rb - Queue manager
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
- # 11/11/05
12
- # Fixed: Stop/start queue manager.
13
10
  #++
14
11
 
15
12
  require 'singleton'
@@ -17,12 +14,19 @@ require 'drb'
17
14
  require 'drb/acl'
18
15
  require 'thread'
19
16
  require 'yaml'
20
- require 'uuid'
21
- require 'reliable-msg/queue'
17
+ begin
18
+ require 'uuid'
19
+ rescue LoadError
20
+ require 'rubygems'
21
+ require_gem 'uuid'
22
+ end
23
+ require 'reliable-msg/client'
22
24
  require 'reliable-msg/message-store'
23
25
 
26
+
24
27
  module ReliableMsg
25
28
 
29
+
26
30
  class Config #:nodoc:
27
31
 
28
32
  CONFIG_FILE = "queues.cfg"
@@ -30,10 +34,15 @@ module ReliableMsg
30
34
  DEFAULT_STORE = MessageStore::Disk::DEFAULT_CONFIG
31
35
 
32
36
  DEFAULT_DRB = {
33
- "port"=>Queue::DRB_PORT,
37
+ "port"=>Client::DRB_PORT,
34
38
  "acl"=>"allow 127.0.0.1"
35
39
  }
36
40
 
41
+ INFO_LOADED_CONFIG = "Loaded queues configuration from: %s" #:nodoc:
42
+
43
+ INFO_CREATED_CONFIG = "Created queues configuration file in: %s" #:nodoc:
44
+
45
+
37
46
  def initialize file, logger = nil
38
47
  @logger = logger
39
48
  # If no file specified, attempt to look for file in current directory.
@@ -50,6 +59,7 @@ module ReliableMsg
50
59
  @config = {}
51
60
  end
52
61
 
62
+
53
63
  def load_no_create
54
64
  if File.exist?(@file)
55
65
  @config= {}
@@ -62,6 +72,7 @@ module ReliableMsg
62
72
  end
63
73
  end
64
74
 
75
+
65
76
  def load_or_create
66
77
  if File.exist?(@file)
67
78
  @config= {}
@@ -70,17 +81,18 @@ module ReliableMsg
70
81
  @config.merge! doc
71
82
  end
72
83
  end
73
- @logger.info "Loaded queues configuration from: #{@file}"
84
+ @logger.info format(INFO_LOADED_CONFIG, @file)
74
85
  else
75
86
  @config = {
76
87
  "store" => DEFAULT_STORE,
77
88
  "drb" => DEFAULT_DRB
78
89
  }
79
90
  save
80
- @logger.info "Created queues configuration file in: #{@file}"
91
+ @logger.info format(INFO_CREATED_CONFIG, @file)
81
92
  end
82
93
  end
83
94
 
95
+
84
96
  def create_if_none
85
97
  if File.exist?(@file)
86
98
  false
@@ -94,20 +106,24 @@ module ReliableMsg
94
106
  end
95
107
  end
96
108
 
109
+
97
110
  def exist?
98
111
  File.exist?(@file)
99
112
  end
100
113
 
114
+
101
115
  def path
102
116
  @file
103
117
  end
104
118
 
119
+
105
120
  def save
106
121
  File.open @file, "w" do |output|
107
122
  YAML::dump @config, output
108
123
  end
109
124
  end
110
125
 
126
+
111
127
  def method_missing symbol, *args
112
128
  if symbol.to_s[-1] == ?=
113
129
  @config[symbol.to_s[0...-1]] = *args
@@ -119,6 +135,22 @@ module ReliableMsg
119
135
  end
120
136
 
121
137
 
138
+ # The QueueManager handles message storage and delivery. Applications connect to the QueueManager
139
+ # either locally or remotely using the client API objects Queue and Topic.
140
+ #
141
+ # You can start a QueueManager from the command line using
142
+ # queues manager start
143
+ # Or from code using
144
+ # qm = QueueManager.new
145
+ # qm.start
146
+ #
147
+ # A Ruby process can only allow one active QueueManager at any given time. Do not run more than
148
+ # one QueueManager connected to the same database or file system storage, as this will cause the
149
+ # queue managers to operate on different messages and queues. Instead, use a single QueueManager
150
+ # and connect to it remotely using DRb.
151
+ #
152
+ # The client API (Queue and Topic) will automatically connect to any QueueManager running in the
153
+ # same Ruby process, or if not found, to a QueueManager running in a different process using DRb.
122
154
  class QueueManager
123
155
 
124
156
  TX_TIMEOUT_CHECK_EVERY = 30
@@ -127,12 +159,41 @@ module ReliableMsg
127
159
 
128
160
  ERROR_RECEIVE_MISSING_QUEUE = "You must specify a queue to retrieve the message from" #:nodoc:
129
161
 
162
+ ERROR_PUBLISH_MISSING_TOPIC = "You must specify a destination topic for the message" #:nodoc:
163
+
164
+ ERROR_RETRIEVE_MISSING_TOPIC = "You must specify a topic to retrieve the message from" #:nodoc:
165
+
130
166
  ERROR_INVALID_HEADER_NAME = "Invalid header '%s': expecting the name to be a symbol, found object of type %s" #:nodoc:
131
167
 
132
168
  ERROR_INVALID_HEADER_VALUE = "Invalid header '%s': expecting the value to be %s, found object of type %s" #:nodoc:
133
169
 
134
170
  ERROR_NO_TRANSACTION = "Transaction %s has completed, or was aborted" #:nodoc:
135
171
 
172
+ ERROR_QM_STARTED = "Queue manager already started for this process: stop the other queue manager before starting a new one" #:nodoc:
173
+
174
+ ERROR_QM_NOT_STARTED = "Queue manager not active" #:nodoc:
175
+
176
+ INFO_MESSAGE_STORE = "Using message store: %s" #:nodoc:
177
+
178
+ INFO_ACCEPTING_DRB = "Accepting requests at: %s" #:nodoc:
179
+
180
+ INFO_QM_STOPPED = "Stopped queue manager at: %s" #:nodoc:
181
+
182
+ WARN_TRANSACTION_TIMEOUT = "Timeout: aborting transaction %s" #:nodoc:
183
+
184
+ WARN_TRANSACTION_ABORTED = "Transaction %s aborted by client" #:nodoc:
185
+
186
+ @@active = nil #:nodoc:
187
+
188
+
189
+ # Create a new QueueManager with the specified options. Once created, you can
190
+ # start the QueueManager with QueueManager.start.
191
+ #
192
+ # Accepted options are:
193
+ # * <tt>:logger</tt> -- The logger to use. If not specified, will log messages
194
+ # to STDOUT.
195
+ # * <tt>:config</tt> -- The configuration file to use. If not specified, will
196
+ # use <tt>queues.cfg</tt>.
136
197
  def initialize options = nil
137
198
  options ||= {}
138
199
  # Locks prevent two transactions from seeing the same message. We use a mutex
@@ -148,72 +209,96 @@ module ReliableMsg
148
209
  @config.load_or_create
149
210
  end
150
211
 
212
+
213
+ # Starts the QueueManager. This method will block until the QueueManager has
214
+ # successfully started, and raise an exception if the QueueManager fails to start
215
+ # or if another QueueManager was already started in this process.
151
216
  def start
152
217
  @mutex.synchronize do
153
- return if @started
154
-
155
- # Get the message store based on the configuration, or default store.
156
- @store = MessageStore::Base.configure(@config.store || Config::DEFAULT_STORE, @logger)
157
- @logger.info "Using message store #{@store.type}"
158
- @store.activate
159
-
160
- # Get the DRb URI from the configuration, or use the default. Create a DRb server.
161
- drb = Config::DEFAULT_DRB
162
- drb.merge(@config.drb) if @config.drb
163
- drb_uri = "druby://localhost:#{drb['port']}"
164
- @drb_server = DRb::DRbServer.new drb_uri, self, :tcp_acl=>ACL.new(drb["acl"].split(" "), ACL::ALLOW_DENY), :verbose=>true
165
- @logger.info "Accepting requests at '#{drb_uri}'"
166
-
167
- # Create a background thread to stop timed-out transactions.
168
- @timeout_thread = Thread.new do
169
- begin
170
- while true
171
- time = Time.new.to_i
172
- @transactions.each_pair do |tid, tx|
173
- if tx[:timeout] <= time
174
- begin
175
- @logger.warn "Timeout: aborting transaction #{tid}"
176
- abort tid
177
- rescue
218
+ return if @@active == self
219
+ Thread.critical = true
220
+ if @@active.nil?
221
+ @@active = self
222
+ else
223
+ Thread.critical = false
224
+ raise RuntimeError, ERROR_QM_STARTED
225
+ end
226
+ Thread.critical = false
227
+
228
+ begin
229
+ # Get the message store based on the configuration, or default store.
230
+ @store = MessageStore::Base.configure(@config.store || Config::DEFAULT_STORE, @logger)
231
+ @logger.info format(INFO_MESSAGE_STORE, @store.type)
232
+ @store.activate
233
+
234
+ # Get the DRb URI from the configuration, or use the default. Create a DRb server.
235
+ drb = Config::DEFAULT_DRB
236
+ drb.merge(@config.drb) if @config.drb
237
+ drb_uri = "druby://localhost:#{drb['port']}"
238
+ @drb_server = DRb::DRbServer.new drb_uri, self, :tcp_acl=>ACL.new(drb["acl"].split(" "), ACL::ALLOW_DENY)
239
+ @logger.info format(INFO_ACCEPTING_DRB, drb_uri)
240
+
241
+ # Create a background thread to stop timed-out transactions.
242
+ @timeout_thread = Thread.new do
243
+ begin
244
+ while true
245
+ time = Time.new.to_i
246
+ @transactions.each_pair do |tid, tx|
247
+ if tx[:timeout] <= time
248
+ begin
249
+ @logger.warn format(WARN_TRANSACTION_TIMEOUT, tid)
250
+ abort tid
251
+ rescue
252
+ end
178
253
  end
179
254
  end
255
+ sleep TX_TIMEOUT_CHECK_EVERY
180
256
  end
181
- sleep TX_TIMEOUT_CHECK_EVERY
257
+ rescue Exception=>error
258
+ retry
182
259
  end
183
- rescue Exception=>error
184
- retry
185
260
  end
186
- end
187
261
 
188
- # Associate this queue manager with the local Queue class, instead of using DRb.
189
- Queue.send :qm=, self
190
- @started = true
262
+ # Associate this queue manager with the local Queue class, instead of using DRb.
263
+ Client.send :qm=, self
264
+ nil
265
+ rescue Exception=>error
266
+ @@active = nil if @@active == self
267
+ raise error
268
+ end
191
269
  end
192
270
  end
193
271
 
272
+
273
+ # Stops the QueueManager. Once stopped, you can start the same QueueManager again,
274
+ # or start a different QueueManager.
194
275
  def stop
195
276
  @mutex.synchronize do
196
- return unless @started
197
- @started = false
198
-
277
+ raise RuntimeError, ERROR_QM_NOT_STARTED unless @@active == self
199
278
  # Prevent transactions from timing out while we take down the server.
200
279
  @timeout_thread.terminate
201
280
  # Shutdown DRb server to prevent new requests from being processed.\
202
- Queue.send :qm=, nil
281
+ Client.send :qm=, nil
203
282
  drb_uri = @drb_server.uri
204
283
  @drb_server.stop_service
205
284
  # Deactivate the message store.
206
285
  @store.deactivate
207
286
  @store = nil
208
287
  @drb_server = @store = @timeout_thread = nil
209
- @logger.info "Stopped queue manager at '#{drb_uri}'"
288
+ @logger.info format(INFO_QM_STOPPED, drb_uri)
289
+ @@active = nil
210
290
  end
291
+ true
211
292
  end
212
293
 
294
+
295
+ # Returns true if the QueueManager is receiving remote requests.
213
296
  def alive?
214
297
  @drb_server && @drb_server.alive?
215
298
  end
216
299
 
300
+
301
+ # Called by client to queue a message.
217
302
  def queue args
218
303
  # Get the arguments of this call.
219
304
  message, headers, queue, tid = args[:message], args[:headers], args[:queue].downcase, args[:tid]
@@ -245,10 +330,9 @@ module ReliableMsg
245
330
 
246
331
  # Set the message headers controlled by the queue.
247
332
  headers[:id] = id
248
- headers[:received] = time
333
+ headers[:created] = time
249
334
  headers[:delivery] ||= :best_effort
250
- headers[:retry] = 0
251
- headers[:max_retries] = integer headers[:max_retries], 0, Queue::DEFAULT_MAX_RETRIES
335
+ headers[:max_deliveries] = integer headers[:max_deliveries], 1, Queue::DEFAULT_MAX_DELIVERIES
252
336
  headers[:priority] = integer headers[:priority], 0, 0
253
337
  if expires_at = headers[:expires_at]
254
338
  raise ArgumentError, format(ERROR_INVALID_HEADER_VALUE, :expires_at, "an integer", expires_at.class) unless expires_at.is_a?(Integer)
@@ -272,6 +356,34 @@ module ReliableMsg
272
356
  end
273
357
 
274
358
 
359
+ # Called by client to list queue headers.
360
+ def list args
361
+ # Get the arguments of this call.
362
+ queue = args[:queue].downcase
363
+ raise ArgumentError, ERROR_SEND_MISSING_QUEUE unless queue and queue.instance_of?(String) and !queue.empty?
364
+
365
+ return @mutex.synchronize do
366
+ list = @store.get_headers queue
367
+ now = Time.now.to_i
368
+ list.inject([]) do |list, headers|
369
+ if queue != Client::DLQ && ((headers[:expires_at] && headers[:expires_at] < now) || (headers[:redelivery] && headers[:redelivery] >= headers[:max_deliveries]))
370
+ expired = {:id=>headers[:id], :queue=>queue, :headers=>headers}
371
+ if headers[:delivery] == :once || headers[:delivery] == :repeated
372
+ @store.transaction { |inserts, deletes, dlqs| dlqs << expired }
373
+ else # :best_effort
374
+ @store.transaction { |inserts, deletes, dlqs| deletes << expired }
375
+ end
376
+ else
377
+ # Need to clone headers (shallow, values are frozen) when passing in same process.
378
+ list << headers.clone
379
+ end
380
+ list
381
+ end
382
+ end
383
+ end
384
+
385
+
386
+ # Called by client to enqueue message.
275
387
  def enqueue args
276
388
  # Get the arguments of this call.
277
389
  queue, selector, tid = args[:queue].downcase, args[:selector], args[:tid]
@@ -284,7 +396,7 @@ module ReliableMsg
284
396
  # transaction. We can wrap everything with a mutex, but it's faster to
285
397
  # release the locks mutex as fast as possibe.
286
398
  message = @mutex.synchronize do
287
- message = @store.select queue do |headers|
399
+ message = @store.get_message queue do |headers|
288
400
  not @locks.has_key?(headers[:id]) and case selector
289
401
  when nil
290
402
  true
@@ -292,8 +404,8 @@ module ReliableMsg
292
404
  headers[:id] == selector
293
405
  when Hash
294
406
  selector.all? { |name, value| headers[name] == value }
295
- when Selector
296
- selector.__evaluate__ headers
407
+ else
408
+ raise RuntimeError, "Internal error"
297
409
  end
298
410
  end
299
411
  if message
@@ -304,11 +416,11 @@ module ReliableMsg
304
416
  # Nothing to do if no message found.
305
417
  return unless message
306
418
 
307
- # If the message has expired, or maximum retry count elapsed, we either
419
+ # If the message has expired, or maximum delivery count elapsed, we either
308
420
  # discard the message, or send it to the DLQ. Since we're out of a message,
309
421
  # we call to get a new one. (This can be changed to repeat instead of recurse).
310
422
  headers = message[:headers]
311
- if queue != Queue::DLQ && ((headers[:expires_at] && headers[:expires_at] < Time.now.to_i) || (headers[:retry] > headers[:max_retries]))
423
+ if queue != Client::DLQ && ((headers[:expires_at] && headers[:expires_at] < Time.now.to_i) || (headers[:redelivery] && headers[:redelivery] >= headers[:max_deliveries]))
312
424
  expired = {:id=>message[:id], :queue=>queue, :headers=>headers}
313
425
  if headers[:delivery] == :once || headers[:delivery] == :repeated
314
426
  @store.transaction { |inserts, deletes, dlqs| dlqs << expired }
@@ -324,7 +436,7 @@ module ReliableMsg
324
436
  if tid
325
437
  tx = @transactions[tid]
326
438
  raise RuntimeError, format(ERROR_NO_TRANSACTION, tid) unless tx
327
- if queue != Queue::DLQ && headers[:delivery] == :once
439
+ if queue != Client::DLQ && headers[:delivery] == :once
328
440
  # Exactly once delivery: immediately move message to DLQ, so if
329
441
  # transaction aborts, message is not retrieved again. Do not
330
442
  # release lock here, to prevent message retrieved from DLQ.
@@ -332,7 +444,7 @@ module ReliableMsg
332
444
  @store.transaction do |inserts, deletes, dlqs|
333
445
  dlqs << delete
334
446
  end
335
- delete[:queue] = Queue::DLQ
447
+ delete[:queue] = Client::DLQ
336
448
  tx[:deletes] << delete
337
449
  else
338
450
  # At most once delivery: delete message if transaction commits.
@@ -359,12 +471,101 @@ module ReliableMsg
359
471
  return :id=>message[:id], :headers=>message[:headers].clone, :message=>message[:message]
360
472
  end
361
473
 
474
+
475
+ # Called by client to publish message.
476
+ def publish args
477
+ # Get the arguments of this call.
478
+ message, headers, topic, tid = args[:message], args[:headers], args[:topic].downcase, args[:tid]
479
+ raise ArgumentError, ERROR_PUBLISH_MISSING_TOPIC unless topic and topic.instance_of?(String) and !topic.empty?
480
+ time = Time.new.to_i
481
+ id = args[:id] || UUID.new
482
+ created = args[:created] || time
483
+
484
+ # Validate and freeze the headers. The cloning ensures that the headers we hold in memory
485
+ # are not modified by the caller. The validation ensures that the headers we hold in memory
486
+ # can be persisted safely. Basic types like string and integer are allowed, but application types
487
+ # may prevent us from restoring the index. Strings are cloned since strings may be replaced.
488
+ headers = if headers
489
+ copy = {}
490
+ headers.each_pair do |name, value|
491
+ raise ArgumentError, format(ERROR_INVALID_HEADER_NAME, name, name.class) unless name.instance_of?(Symbol)
492
+ case value
493
+ when String, Numeric, Symbol, true, false, nil
494
+ copy[name] = value.freeze
495
+ else
496
+ raise ArgumentError, format(ERROR_INVALID_HEADER_VALUE, name, "a string, numeric, symbol, true/false or nil", value.class)
497
+ end
498
+ end
499
+ copy
500
+ else
501
+ {}
502
+ end
503
+
504
+ # Set the message headers controlled by the topic.
505
+ headers[:id] = id
506
+ headers[:created] = time
507
+ if expires_at = headers[:expires_at]
508
+ raise ArgumentError, format(ERROR_INVALID_HEADER_VALUE, :expires_at, "an integer", expires_at.class) unless expires_at.is_a?(Integer)
509
+ elsif expires = headers[:expires]
510
+ raise ArgumentError, format(ERROR_INVALID_HEADER_VALUE, :expires, "an integer", expires.class) unless expires.is_a?(Integer)
511
+ headers[:expires_at] = Time.now.to_i + expires if expires > 0
512
+ end
513
+ # Create an insertion record for the new message.
514
+ insert = {:id=>id, :topic=>topic, :headers=>headers, :message=>message}
515
+ if tid
516
+ tx = @transactions[tid]
517
+ raise RuntimeError, format(ERROR_NO_TRANSACTION, tid) unless tx
518
+ tx[:inserts] << insert
519
+ else
520
+ @store.transaction do |inserts, deletes, dlqs|
521
+ inserts << insert
522
+ end
523
+ end
524
+ end
525
+
526
+
527
+ # Called by client to retrieve message from topic.
528
+ def retrieve args
529
+ # Get the arguments of this call.
530
+ seen, topic, selector, tid = args[:seen], args[:topic].downcase, args[:selector], args[:tid]
531
+ id, headers = nil, nil
532
+ raise ArgumentError, ERROR_RETRIEVE_MISSING_TOPIC unless topic and topic.instance_of?(String) and !topic.empty?
533
+
534
+ # Very simple, we really only select one message and nothing to lock.
535
+ message = @store.get_last topic, seen do |headers|
536
+ case selector
537
+ when nil
538
+ true
539
+ when Hash
540
+ selector.all? { |name, value| headers[name] == value }
541
+ else
542
+ raise RuntimeError, "Internal error"
543
+ end
544
+ end
545
+ # Nothing to do if no message found.
546
+ return unless message
547
+
548
+ # If the message has expired, we discard the message. This being the most recent
549
+ # message on the topic, we simply return nil.
550
+ headers = message[:headers]
551
+ if (headers[:expires_at] && headers[:expires_at] < Time.now.to_i)
552
+ expired = {:id=>message[:id], :topic=>topic, :headers=>headers}
553
+ @store.transaction { |inserts, deletes, dlqs| deletes << expired }
554
+ return nil
555
+ end
556
+ return :id=>message[:id], :headers=>message[:headers].clone, :message=>message[:message]
557
+ end
558
+
559
+
560
+ # Called by client to begin a transaction.
362
561
  def begin timeout
363
562
  tid = UUID.new
364
563
  @transactions[tid] = {:inserts=>[], :deletes=>[], :timeout=>Time.new.to_i + timeout}
365
564
  tid
366
565
  end
367
566
 
567
+
568
+ # Called by client to commit a transaction.
368
569
  def commit tid
369
570
  tx = @transactions[tid]
370
571
  raise RuntimeError, format(ERROR_NO_TRANSACTION, tid) unless tx
@@ -385,6 +586,8 @@ module ReliableMsg
385
586
  end
386
587
  end
387
588
 
589
+
590
+ # Called by client to abort a transaction.
388
591
  def abort tid
389
592
  tx = @transactions[tid]
390
593
  raise RuntimeError, format(ERROR_NO_TRANSACTION, tid) unless tx
@@ -393,13 +596,15 @@ module ReliableMsg
393
596
  @mutex.synchronize do
394
597
  tx[:deletes].each do |delete|
395
598
  @locks.delete delete[:id]
396
- delete[:headers][:retry] += 1
599
+ delete[:headers][:redelivery] = (delete[:headers][:redelivery] || 0) + 1
600
+ # TODO: move to DLQ if delivery count or expires
397
601
  end
398
602
  end
399
603
  @transactions.delete tid
400
- @logger.warn "Transaction #{tid} aborted"
604
+ @logger.warn format(WARN_TRANSACTION_ABORTED, tid)
401
605
  end
402
606
 
607
+
403
608
  private
404
609
  def integer value, minimum, default
405
610
  return default unless value