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
@@ -2,26 +2,24 @@
|
|
2
2
|
# = message-store.rb - Queue manager storage adapters
|
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: Messages retrieved in order after queue manager recovers when using MySQL.
|
13
10
|
#++
|
14
11
|
|
15
12
|
require 'thread'
|
16
|
-
require '
|
17
|
-
require 'reliable-msg/queue'
|
13
|
+
require 'reliable-msg/client'
|
18
14
|
|
19
15
|
module ReliableMsg
|
20
16
|
|
17
|
+
|
21
18
|
module MessageStore
|
22
19
|
|
23
20
|
ERROR_INVALID_MESSAGE_STORE = "No message store '%s' available (note: case is not important)" #:nodoc:
|
24
21
|
|
22
|
+
|
25
23
|
# Base class for message store.
|
26
24
|
class Base
|
27
25
|
|
@@ -29,10 +27,12 @@ module ReliableMsg
|
|
29
27
|
|
30
28
|
@@stores = {} #:nodoc:
|
31
29
|
|
30
|
+
|
32
31
|
def initialize logger
|
33
32
|
@logger = logger
|
34
33
|
end
|
35
34
|
|
35
|
+
|
36
36
|
# Returns the message store type name.
|
37
37
|
#
|
38
38
|
# :call-seq:
|
@@ -42,6 +42,7 @@ module ReliableMsg
|
|
42
42
|
raise RuntimeException, "Not implemented"
|
43
43
|
end
|
44
44
|
|
45
|
+
|
45
46
|
# Set up the message store. Create files, database tables, etc.
|
46
47
|
#
|
47
48
|
# :call-seq:
|
@@ -50,6 +51,7 @@ module ReliableMsg
|
|
50
51
|
def setup
|
51
52
|
end
|
52
53
|
|
54
|
+
|
53
55
|
# Returns the message store configuration as a hash.
|
54
56
|
#
|
55
57
|
# :call-seq:
|
@@ -59,6 +61,7 @@ module ReliableMsg
|
|
59
61
|
raise RuntimeException, "Not implemented"
|
60
62
|
end
|
61
63
|
|
64
|
+
|
62
65
|
# Activates the message store. Call this method before using the
|
63
66
|
# message store.
|
64
67
|
#
|
@@ -68,10 +71,12 @@ module ReliableMsg
|
|
68
71
|
def activate
|
69
72
|
@mutex = Mutex.new
|
70
73
|
@queues = {Queue::DLQ=>[]}
|
74
|
+
@topics = {}
|
71
75
|
@cache = {}
|
72
76
|
# TODO: add recovery logic
|
73
77
|
end
|
74
78
|
|
79
|
+
|
75
80
|
# Deactivates the message store. Call this method when done using
|
76
81
|
# the message store.
|
77
82
|
#
|
@@ -79,9 +84,10 @@ module ReliableMsg
|
|
79
84
|
# store.deactivate
|
80
85
|
#
|
81
86
|
def deactivate
|
82
|
-
@mutex = @queues = @cache = nil
|
87
|
+
@mutex = @queues = @topics = @cache = nil
|
83
88
|
end
|
84
89
|
|
90
|
+
|
85
91
|
def transaction &block
|
86
92
|
result = block.call inserts = [], deletes = [], dlqs= []
|
87
93
|
begin
|
@@ -92,26 +98,44 @@ module ReliableMsg
|
|
92
98
|
# Empty the cache and reload the queue, before raising the error.
|
93
99
|
@cache = {}
|
94
100
|
@queues = {Queue::DLQ=>[]}
|
101
|
+
@topics = {}
|
95
102
|
load_index
|
96
103
|
raise error
|
97
104
|
end
|
98
105
|
result
|
99
106
|
end
|
100
107
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
if
|
108
|
+
|
109
|
+
def get_message queue, &block
|
110
|
+
messages = @queues[queue]
|
111
|
+
return nil unless messages
|
112
|
+
messages.each do |headers|
|
113
|
+
if block.call(headers)
|
107
114
|
id = headers[:id]
|
108
|
-
message = @cache[id] || load(id, queue)
|
115
|
+
message = @cache[id] || load(id, :queue, queue)
|
109
116
|
return {:id=>id, :headers=>headers, :message=>message}
|
110
117
|
end
|
111
118
|
end
|
112
119
|
return nil
|
113
120
|
end
|
114
121
|
|
122
|
+
|
123
|
+
def get_headers queue
|
124
|
+
return @queues[queue] || []
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
def get_last topic, seen, &block
|
129
|
+
headers = @topics[topic]
|
130
|
+
return nil if headers.nil? || headers[:id] == seen
|
131
|
+
if block.call(headers)
|
132
|
+
id = headers[:id]
|
133
|
+
message = @cache[id] || load(id, :topic, topic)
|
134
|
+
{:id=>id, :headers=>headers, :message=>message}
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
|
115
139
|
# Returns a message store from the specified configuration (previously
|
116
140
|
# created with configure).
|
117
141
|
#
|
@@ -125,31 +149,40 @@ module ReliableMsg
|
|
125
149
|
cls.new config, logger
|
126
150
|
end
|
127
151
|
|
152
|
+
|
128
153
|
protected
|
129
154
|
|
130
155
|
def update inserts, deletes, dlqs
|
131
156
|
@mutex.synchronize do
|
132
157
|
inserts.each do |insert|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
queue[idx
|
141
|
-
|
142
|
-
|
158
|
+
if insert[:queue]
|
159
|
+
queue = @queues[insert[:queue]] ||= []
|
160
|
+
headers = insert[:headers]
|
161
|
+
# Add element based on priority, higher priority comes first.
|
162
|
+
priority = headers[:priority]
|
163
|
+
added = false
|
164
|
+
queue.each_index do |idx|
|
165
|
+
if queue[idx][:priority] < priority
|
166
|
+
queue[idx, 0] = headers
|
167
|
+
added = true
|
168
|
+
break
|
169
|
+
end
|
143
170
|
end
|
171
|
+
queue << headers unless added
|
172
|
+
@cache[insert[:id]] = insert[:message]
|
173
|
+
elsif insert[:topic]
|
174
|
+
@topics[insert[:topic]] = insert[:headers]
|
144
175
|
end
|
145
|
-
queue << headers unless added
|
146
|
-
@cache[insert[:id]] = insert[:message]
|
147
176
|
end
|
148
177
|
deletes.each do |delete|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
178
|
+
if delete[:queue]
|
179
|
+
queue = @queues[delete[:queue]]
|
180
|
+
id = delete[:id]
|
181
|
+
queue.delete_if { |headers| headers[:id] == id }
|
182
|
+
@cache.delete id
|
183
|
+
elsif delete[:topic]
|
184
|
+
@topics.delete delete[:topic]
|
185
|
+
end
|
153
186
|
end
|
154
187
|
dlqs.each do |dlq|
|
155
188
|
queue = @queues[dlq[:queue]]
|
@@ -185,6 +218,11 @@ module ReliableMsg
|
|
185
218
|
"path"=>DEFAULT_PATH
|
186
219
|
}
|
187
220
|
|
221
|
+
ERROR_PATH_NOT_DIR = "The path %s is not a directory" #:nodoc:
|
222
|
+
|
223
|
+
ERROR_FAILED_WRITE_MASTER = "Cannot write to master index file %s" #:nodoc:
|
224
|
+
|
225
|
+
|
188
226
|
def initialize config, logger
|
189
227
|
super logger
|
190
228
|
@fsync = config['fsync']
|
@@ -197,13 +235,15 @@ module ReliableMsg
|
|
197
235
|
@path = File.expand_path(config['path'] || DEFAULT_PATH)
|
198
236
|
end
|
199
237
|
|
238
|
+
|
200
239
|
def type
|
201
240
|
TYPE
|
202
241
|
end
|
203
242
|
|
243
|
+
|
204
244
|
def setup
|
205
245
|
if File.exist?(@path)
|
206
|
-
raise RuntimeError,
|
246
|
+
raise RuntimeError, format(ERROR_PATH_NOT_DIR, @path) unless File.directory?(@path)
|
207
247
|
false
|
208
248
|
else
|
209
249
|
Dir.mkdir @path
|
@@ -211,17 +251,19 @@ module ReliableMsg
|
|
211
251
|
end
|
212
252
|
end
|
213
253
|
|
254
|
+
|
214
255
|
def configuration
|
215
256
|
{ "type"=>TYPE, "path"=>@path }
|
216
257
|
end
|
217
258
|
|
259
|
+
|
218
260
|
def activate
|
219
261
|
super
|
220
262
|
Dir.mkdir @path unless File.exist?(@path)
|
221
|
-
raise RuntimeError,
|
263
|
+
raise RuntimeError, format(ERROR_PATH_NOT_DIR, @path) unless File.directory?(@path)
|
222
264
|
index = "#{@path}/master.idx"
|
223
265
|
if File.exist? index
|
224
|
-
raise RuntimeError,
|
266
|
+
raise RuntimeError, format(ERROR_FAILED_WRITE_MASTER, index) unless File.writable?(index)
|
225
267
|
@file = File.open index, "r+"
|
226
268
|
@file.flock File::LOCK_EX
|
227
269
|
@file.binmode # Things break if you forget binmode on Windows.
|
@@ -237,7 +279,9 @@ module ReliableMsg
|
|
237
279
|
end
|
238
280
|
end
|
239
281
|
|
282
|
+
|
240
283
|
def deactivate
|
284
|
+
@file.flock File::LOCK_UN
|
241
285
|
@file.close
|
242
286
|
@file_map.each_pair do |id, map|
|
243
287
|
map[1].close if map[1]
|
@@ -249,6 +293,7 @@ module ReliableMsg
|
|
249
293
|
super
|
250
294
|
end
|
251
295
|
|
296
|
+
|
252
297
|
protected
|
253
298
|
|
254
299
|
def update inserts, deletes, dlqs
|
@@ -271,7 +316,8 @@ module ReliableMsg
|
|
271
316
|
# (message and file have different IDs).
|
272
317
|
file.sysseek 0, IO::SEEK_SET
|
273
318
|
file.syswrite insert[:message]
|
274
|
-
file.
|
319
|
+
file.flush
|
320
|
+
file.truncate insert[:message].length
|
275
321
|
file.flush
|
276
322
|
@mutex.synchronize do
|
277
323
|
@file_map[insert[:id]] = [name, file]
|
@@ -301,7 +347,7 @@ module ReliableMsg
|
|
301
347
|
file_map = {}
|
302
348
|
@file_map.each_pair { |id, name_file| file_map[id] = name_file[0] }
|
303
349
|
file_free = @file_free.collect { |name_file| name_file[0] }
|
304
|
-
image = Marshal::dump({:queues=>@queues, :file_map=>file_map, :file_free=>file_free})
|
350
|
+
image = Marshal::dump({:queues=>@queues, :topics=>@topics, :file_map=>file_map, :file_free=>file_free})
|
305
351
|
length = image.length
|
306
352
|
# Determine if we can store the new image before the last one (must have
|
307
353
|
# enough space from header), or append it to the end of the last one.
|
@@ -323,6 +369,7 @@ module ReliableMsg
|
|
323
369
|
end
|
324
370
|
end
|
325
371
|
|
372
|
+
|
326
373
|
def load_index
|
327
374
|
@file.sysseek 0, IO::SEEK_SET
|
328
375
|
last_block = @file.sysread(8).hex
|
@@ -332,17 +379,19 @@ module ReliableMsg
|
|
332
379
|
# master index.
|
333
380
|
@file.sysseek last_block, IO::SEEK_SET
|
334
381
|
length = @file.sysread(8).hex
|
335
|
-
# Load the index image and create the queues, file_free
|
336
|
-
# file_map structures.
|
382
|
+
# Load the index image and create the queues, topics, file_free
|
383
|
+
# and file_map structures.
|
337
384
|
image = Marshal::load @file.sysread(length)
|
338
385
|
@queues = image[:queues]
|
386
|
+
@topics = image[:topics]
|
339
387
|
image[:file_free].each { |name| @file_free << [name, nil] }
|
340
388
|
image[:file_map].each_pair { |id, name| @file_map[id] = [name, nil] }
|
341
389
|
@last_block, @last_block_end = last_block, last_block + length + 8
|
342
390
|
end
|
343
391
|
end
|
344
392
|
|
345
|
-
|
393
|
+
|
394
|
+
def load id, type, queue
|
346
395
|
# Find the file from the message/file mapping.
|
347
396
|
map = @file_map[id]
|
348
397
|
return nil unless map # TODO: Error?
|
@@ -371,6 +420,7 @@ module ReliableMsg
|
|
371
420
|
require 'active_record/vendor/mysql'
|
372
421
|
end
|
373
422
|
|
423
|
+
|
374
424
|
class MySQL < Base #:nodoc:
|
375
425
|
|
376
426
|
TYPE = self.name.split('::').last.downcase
|
@@ -383,29 +433,31 @@ module ReliableMsg
|
|
383
433
|
# Reference to an open MySQL connection held in the current thread.
|
384
434
|
THREAD_CURRENT_MYSQL = :reliable_msg_mysql #:nodoc:
|
385
435
|
|
436
|
+
|
386
437
|
def initialize config, logger
|
387
438
|
super logger
|
388
439
|
@config = { :host=>config['host'], :username=>config['username'], :password=>config['password'],
|
389
440
|
:database=>config['database'], :port=>config['port'], :socket=>config['socket'] }
|
390
441
|
@prefix = config['prefix'] || DEFAULT_PREFIX
|
391
442
|
@queues_table = "#{@prefix}queues"
|
443
|
+
@topics_table = "#{@prefix}topics"
|
392
444
|
end
|
393
445
|
|
446
|
+
|
394
447
|
def type
|
395
448
|
TYPE
|
396
449
|
end
|
397
450
|
|
451
|
+
|
398
452
|
def setup
|
399
453
|
mysql = connection
|
400
|
-
|
454
|
+
requires = 2 # Number of tables used by reliable-msg.
|
401
455
|
mysql.query "SHOW TABLES" do |result|
|
402
456
|
while row = result.fetch_row
|
403
|
-
|
457
|
+
requires -= 1 if row[0] == @queues_table || row[0] == @topics_table
|
404
458
|
end
|
405
459
|
end
|
406
|
-
if
|
407
|
-
false
|
408
|
-
else
|
460
|
+
if requires > 0
|
409
461
|
sql = File.open File.join(File.dirname(__FILE__), "mysql.sql"), "r" do |input|
|
410
462
|
input.readlines.join
|
411
463
|
end
|
@@ -415,6 +467,7 @@ module ReliableMsg
|
|
415
467
|
end
|
416
468
|
end
|
417
469
|
|
470
|
+
|
418
471
|
def configuration
|
419
472
|
config = { "type"=>TYPE, "host"=>@config[:host], "username"=>@config[:username],
|
420
473
|
"password"=>@config[:password], "database"=>@config[:database] }
|
@@ -424,11 +477,13 @@ module ReliableMsg
|
|
424
477
|
config
|
425
478
|
end
|
426
479
|
|
480
|
+
|
427
481
|
def activate
|
428
482
|
super
|
429
483
|
load_index
|
430
484
|
end
|
431
485
|
|
486
|
+
|
432
487
|
def deactivate
|
433
488
|
Thread.list.each do |thread|
|
434
489
|
if conn = thread[THREAD_CURRENT_MYSQL]
|
@@ -439,6 +494,7 @@ module ReliableMsg
|
|
439
494
|
super
|
440
495
|
end
|
441
496
|
|
497
|
+
|
442
498
|
protected
|
443
499
|
|
444
500
|
def update inserts, deletes, dlqs
|
@@ -446,9 +502,15 @@ module ReliableMsg
|
|
446
502
|
mysql.query "BEGIN"
|
447
503
|
begin
|
448
504
|
inserts.each do |insert|
|
449
|
-
|
505
|
+
if insert[:queue]
|
506
|
+
mysql.query "INSERT INTO `#{@queues_table}` (id,queue,headers,object) VALUES('#{connection.quote insert[:id]}','#{connection.quote insert[:queue]}',BINARY '#{connection.quote Marshal::dump(insert[:headers])}',BINARY '#{connection.quote insert[:message]}')"
|
507
|
+
else
|
508
|
+
mysql.query "REPLACE `#{@topics_table}` (topic,headers,object) VALUES('#{connection.quote insert[:topic]}',BINARY '#{connection.quote Marshal::dump(insert[:headers])}',BINARY '#{connection.quote insert[:message]}')"
|
509
|
+
end
|
510
|
+
end
|
511
|
+
ids = deletes.inject([]) do |array, delete|
|
512
|
+
delete[:queue] ? array << "'#{delete[:id]}'" : array
|
450
513
|
end
|
451
|
-
ids = deletes.collect {|delete| "'#{delete[:id]}'" }
|
452
514
|
if !ids.empty?
|
453
515
|
mysql.query "DELETE FROM `#{@queues_table}` WHERE id IN (#{ids.join ','})"
|
454
516
|
end
|
@@ -463,6 +525,7 @@ module ReliableMsg
|
|
463
525
|
super
|
464
526
|
end
|
465
527
|
|
528
|
+
|
466
529
|
def load_index
|
467
530
|
connection.query "SELECT id,queue,headers FROM `#{@queues_table}`" do |result|
|
468
531
|
while row = result.fetch_row
|
@@ -481,18 +544,33 @@ module ReliableMsg
|
|
481
544
|
queue << headers unless added
|
482
545
|
end
|
483
546
|
end
|
547
|
+
connection.query "SELECT topic,headers FROM `#{@topics_table}`" do |result|
|
548
|
+
while row = result.fetch_row
|
549
|
+
@topics[row[0]] = Marshal::load row[1]
|
550
|
+
end
|
551
|
+
end
|
484
552
|
end
|
485
553
|
|
486
|
-
|
554
|
+
|
555
|
+
def load id, type, queue_or_topic
|
487
556
|
message = nil
|
488
|
-
|
489
|
-
|
490
|
-
row
|
557
|
+
if type == :queue
|
558
|
+
connection.query "SELECT object FROM `#{@queues_table}` WHERE id='#{id}'" do |result|
|
559
|
+
message = if row = result.fetch_row
|
560
|
+
row[0]
|
561
|
+
end
|
562
|
+
end
|
563
|
+
else
|
564
|
+
connection.query "SELECT object FROM `#{@topics_table}` WHERE topic='#{queue_or_topic}'" do |result|
|
565
|
+
message = if row = result.fetch_row
|
566
|
+
row[0]
|
567
|
+
end
|
491
568
|
end
|
492
569
|
end
|
493
570
|
message
|
494
571
|
end
|
495
572
|
|
573
|
+
|
496
574
|
def connection
|
497
575
|
Thread.current[THREAD_CURRENT_MYSQL] ||= Mysql.new @config[:host], @config[:username], @config[:password],
|
498
576
|
@config[:database], @config[:port], @config[:socket]
|
@@ -501,6 +579,7 @@ module ReliableMsg
|
|
501
579
|
end
|
502
580
|
|
503
581
|
rescue LoadError
|
582
|
+
# do nothing
|
504
583
|
end
|
505
584
|
|
506
585
|
end
|