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