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/mysql.sql
CHANGED
@@ -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/
|
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
|
-
|
21
|
-
require '
|
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"=>
|
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
|
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
|
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
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
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
|
-
|
257
|
+
rescue Exception=>error
|
258
|
+
retry
|
182
259
|
end
|
183
|
-
rescue Exception=>error
|
184
|
-
retry
|
185
260
|
end
|
186
|
-
end
|
187
261
|
|
188
|
-
|
189
|
-
|
190
|
-
|
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
|
-
|
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
|
-
|
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
|
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[:
|
333
|
+
headers[:created] = time
|
249
334
|
headers[:delivery] ||= :best_effort
|
250
|
-
headers[:
|
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.
|
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
|
-
|
296
|
-
|
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
|
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 !=
|
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 !=
|
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] =
|
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][:
|
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
|
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
|