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.
@@ -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/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: Messages retrieved in order after queue manager recovers when using MySQL.
13
10
  #++
14
11
 
15
12
  require 'thread'
16
- require 'uuid'
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
- def select queue, &block
102
- queue = @queues[queue]
103
- return nil unless queue
104
- queue.each do |headers|
105
- selected = block.call(headers)
106
- if selected
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
- queue = @queues[insert[:queue]] ||= []
134
- headers = insert[:headers]
135
- # Add element based on priority, higher priority comes first.
136
- priority = headers[:priority]
137
- added = false
138
- queue.each_index do |idx|
139
- if queue[idx][:priority] < priority
140
- queue[idx, 0] = headers
141
- added = true
142
- break
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
- queue = @queues[delete[:queue]]
150
- id = delete[:id]
151
- queue.delete_if { |headers| headers[:id] == id }
152
- @cache.delete id
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, "The path '#{@path}' is not a directory" unless File.directory?(@path)
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, "The path '#{@path}' is not a directory" unless File.directory?(@path)
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, "Cannot write to master index file '#{index}'" unless File.writable?(index)
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.truncate file.pos
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 and
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
- def load id, queue
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
- exists = false
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
- exists = true if row[0] == @queues_table
457
+ requires -= 1 if row[0] == @queues_table || row[0] == @topics_table
404
458
  end
405
459
  end
406
- if exists
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
- 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]}')"
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
- def load id, queue
554
+
555
+ def load id, type, queue_or_topic
487
556
  message = nil
488
- connection.query "SELECT object FROM `#{@queues_table}` WHERE id='#{id}'" do |result|
489
- message = if row = result.fetch_row
490
- row[0]
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