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