beetle 0.3.0.rc.17 → 0.3.0.rc.18

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -39,7 +39,7 @@ More information can be found on the {project website}[http://xing.github.com/be
39
39
  b.publish :test, "I'm a test message"
40
40
 
41
41
  === Subscribing
42
- b.listen
42
+ b.listen_queues
43
43
 
44
44
  === Examples
45
45
 
@@ -65,16 +65,16 @@ To set up a redundant messaging system you will need
65
65
  At runtime, Beetle will use
66
66
  * {uuid4r}[http://github.com/skaes/uuid4r]
67
67
  (which needs ossp-uuid)
68
- * {bunny}[http://github.com/celldee/bunny]
69
- * {redis}[http://github.com/ezmobius/redis-rb]
70
- * {amqp}[http://github.com/tmm1/amqp]
68
+ * {bunny}[http://github.com/ruby-amqp/bunny]
69
+ * {redis}[http://github.com/redis/redis-rb]
70
+ * {amqp}[http://github.com/ruby-amqp/amqp]
71
71
  (which is based on {eventmachine}[http://github.com/eventmachine/eventmachine])
72
72
  * {daemons}[http://daemons.rubyforge.org/]
73
73
  * activesupport
74
74
 
75
75
  For development, you'll need
76
76
  * {mocha}[http://github.com/floehopper/mocha]
77
- * {rcov}[http://github.com/relevance/rcov]
77
+ * {rcov}[http://github.com/relevance/rcov] (for ruby 1.8.7)
78
78
  * {cucumber}[http://github.com/aslakhellesoy/cucumber]
79
79
  * {daemon_controller}[http://github.com/FooBarWidget/daemon_controller]
80
80
 
data/Rakefile CHANGED
@@ -53,20 +53,35 @@ namespace :beetle do
53
53
  end
54
54
 
55
55
  namespace :rabbit do
56
- def start(node_name, port)
56
+ def start(node_name, port, web_port)
57
57
  script = File.expand_path(File.dirname(__FILE__)+"/script/start_rabbit")
58
- puts "starting rabbit #{node_name} on port #{port}"
58
+ # on my machine, the rabbitmq user is not be allowed to access my files.
59
+ # so we need to put the config file under /tmp
60
+ config_file = "/tmp/beetle-testing-rabbitmq-#{node_name}"
61
+
62
+ create_config_file config_file, web_port
63
+
64
+ puts "starting rabbit #{node_name} on port #{port}, web management port #{web_port}"
59
65
  puts "type ^C a RETURN to abort"
60
66
  sleep 1
61
- exec "sudo #{script} #{node_name} #{port}"
67
+ exec "sudo #{script} #{node_name} #{port} #{config_file}"
68
+ end
69
+
70
+ def create_config_file(config_file, web_port)
71
+ File.open("#{config_file}.config",'w') do |f|
72
+ f.puts "["
73
+ f.puts " {rabbitmq_management, [{listener, [{port, #{web_port}}]}]}"
74
+ f.puts "]."
75
+ end
62
76
  end
77
+
63
78
  desc "start rabbit instance 1"
64
79
  task :start1 do
65
- start "rabbit1", 5672
80
+ start "rabbit1", 5672, 15672
66
81
  end
67
82
  desc "start rabbit instance 2"
68
83
  task :start2 do
69
- start "rabbit2", 5673
84
+ start "rabbit2", 5673, 15673
70
85
  end
71
86
  desc "reset rabbit instances (deletes all data!)"
72
87
  task :reset do
@@ -109,7 +124,7 @@ end
109
124
 
110
125
  require 'rdoc/task'
111
126
 
112
- Rake::RDocTask.new do |rdoc|
127
+ RDoc::Task.new do |rdoc|
113
128
  rdoc.rdoc_dir = 'site/rdoc'
114
129
  rdoc.title = 'Beetle'
115
130
  rdoc.main = 'README.rdoc'
data/beetle.gemspec CHANGED
@@ -36,11 +36,10 @@ Gem::Specification.new do |s|
36
36
  s.add_runtime_dependency("bunny", ["= 0.7.9"])
37
37
  s.add_runtime_dependency("redis", ["= 3.0.1"])
38
38
  s.add_runtime_dependency("hiredis", ["= 0.4.5"])
39
- s.add_runtime_dependency("amq-client", ["= 0.9.3"])
40
- s.add_runtime_dependency("amq-protocol", ["= 0.9.3"])
41
- s.add_runtime_dependency("amqp", ["= 0.9.6"])
39
+ s.add_runtime_dependency("amq-client", ["= 0.9.10"])
40
+ s.add_runtime_dependency("amq-protocol", ["= 1.0.1"])
41
+ s.add_runtime_dependency("amqp", ["= 0.9.8"])
42
42
  s.add_runtime_dependency("activesupport", [">= 2.3.4"])
43
43
  s.add_runtime_dependency("eventmachine_httpserver", [">= 0.2.1"])
44
44
  s.add_runtime_dependency("daemons", [">= 1.0.10"])
45
45
  end
46
-
@@ -166,11 +166,6 @@ appendfsync everysec
166
166
 
167
167
  ############################### ADVANCED CONFIG ###############################
168
168
 
169
- # Glue small output buffers together in order to send small replies in a
170
- # single TCP packet. Uses a bit more CPU but most of the times it is a win
171
- # in terms of number of queries per second. Use 'yes' if unsure.
172
- glueoutputbuf yes
173
-
174
169
  # Use object sharing. Can save a lot of memory if you have many common
175
170
  # string in your dataset, but performs lookups against the shared objects
176
171
  # pool so it uses more CPU and can be a bit slower. Usually it's a good
data/lib/beetle.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  $:.unshift(File.expand_path('..', __FILE__))
2
- require 'bunny'
2
+ require 'amq/client' # defines AMQ::Client::Settings
3
+ require 'bunny' # which bunny picks up
4
+ require 'qrack/errors' # needed by the publisher
3
5
  require 'uuid4r'
4
6
  require 'redis/connection/hiredis' # require *before* redis as specified in the redis-rb gem docs
5
7
  require 'redis'
@@ -0,0 +1,50 @@
1
+ require 'optparse'
2
+ require 'beetle'
3
+
4
+ module Beetle
5
+ module Commands
6
+ # Command to garbage collect the deduplication store
7
+ #
8
+ # Usage: beetle garbage_collect_deduplication_store [options]
9
+ #
10
+ # options:
11
+ # --redis-servers LIST Required (e.g. 192.168.0.1:6379,192.168.0.2:6379)
12
+ # --config-file PATH Path to an external yaml config file
13
+ # -v, --verbose
14
+ # -h, --help Show this message
15
+ #
16
+ class GarbageCollectDeduplicationStore
17
+ # parses command line options and starts Beetle::RedisConfigurationServer as a daemon
18
+ def self.execute
19
+ opts = OptionParser.new
20
+ opts.banner = "Usage: beetle garbage_collect_deduplication_store [options]"
21
+ opts.separator ""
22
+
23
+ opts.on("--redis-servers LIST", Array, "Required (e.g. 192.168.0.1:6379,192.168.0.2:6379)") do |val|
24
+ Beetle.config.redis_servers = val.join(",")
25
+ end
26
+
27
+ opts.on("--config-file PATH", String, "Path to an external yaml config file") do |val|
28
+ Beetle.config.config_file = val
29
+ end
30
+
31
+ opts.on("--redis-db N", Integer, "Redis database used for GC") do |val|
32
+ Beetle.config.redis_db = val.to_i
33
+ end
34
+
35
+ opts.on("-v", "--verbose") do |val|
36
+ Beetle.config.logger.level = Logger::DEBUG
37
+ end
38
+
39
+ opts.on_tail("-h", "--help", "Show this message") do
40
+ puts opts
41
+ exit
42
+ end
43
+
44
+ opts.parse!(ARGV)
45
+
46
+ DeduplicationStore.new.garbage_collect_keys_using_master_and_slave
47
+ end
48
+ end
49
+ end
50
+ end
@@ -48,16 +48,69 @@ module Beetle
48
48
  key =~ /^(msgid:[^:]*:[-0-9a-f]*):.*$/ && $1
49
49
  end
50
50
 
51
+ # garbage collect using master and slave (and redis-cli)
52
+ def garbage_collect_keys_using_master_and_slave(now = Time.now.to_i)
53
+ logger.info "garbage collecting obsolete redis keys from #{@config.redis_servers}"
54
+ info = RedisServerInfo.new(@config, {})
55
+ info.refresh
56
+ unless connection = info.slaves.first
57
+ logger.warn "no slave available, falling back to master."
58
+ connection = redis
59
+ end
60
+ file = "/tmp/beetle_redis_expire_keys_#{$$}.txt"
61
+ cmd = "redis-cli -h #{connection.host} -p #{connection.port} -n #{@config.redis_db} keys 'msgid:*:expires' > #{file}"
62
+ logger.info "retrieving expire keys: '#{cmd}'"
63
+ if system(cmd)
64
+ garbage_collect_keys_from_file(file, now)
65
+ else
66
+ logger.error "could not retrieve expire keys"
67
+ end
68
+ rescue => e
69
+ logger.error "#{e.class}(#{e})"
70
+ ensure
71
+ system("rm -f #{file}")
72
+ end
73
+
51
74
  # garbage collect keys in Redis (always assume the worst!)
52
75
  def garbage_collect_keys(now = Time.now.to_i)
53
76
  keys = redis.keys("msgid:*:expires")
54
77
  threshold = now + @config.gc_threshold
55
78
  keys.each do |key|
56
- expires_at = redis.get key
57
- if expires_at && expires_at.to_i < threshold
79
+ gc_key(key, threshold)
80
+ end
81
+ end
82
+
83
+ # garbage collect keys from a given file
84
+ def garbage_collect_keys_from_file(file_name, now = Time.now.to_i)
85
+ threshold = now + @config.gc_threshold
86
+ expired = total = 0
87
+ File.foreach(file_name) do |line|
88
+ line.chomp!
89
+ if line =~ /^msgid:.*:expires$/
90
+ total += 1
91
+ expired += 1 if gc_key(line, threshold)
92
+ end
93
+ end
94
+ logger.info "expired #{expired} keys out of #{total}"
95
+ end
96
+
97
+ # garbage collect a single key if it's older than given threshold
98
+ def gc_key(key, threshold)
99
+ expires_at = redis.get key
100
+ if expires_at
101
+ if expires_at.to_i < threshold
58
102
  msg_id = msg_id(key)
103
+ # puts "expiring: #{msg_id}"
59
104
  redis.del(*keys(msg_id))
105
+ return true
106
+ else
107
+ # keys has future expiry date
108
+ # puts "#{msg_id} expires on #{Time.at(expires_at.to_i)}"
109
+ return false
60
110
  end
111
+ else
112
+ # key might have been deleted in the meantime
113
+ return false
61
114
  end
62
115
  end
63
116
 
@@ -263,10 +263,12 @@ module Beetle
263
263
  elsif !timed_out?
264
264
  RC::HandlerNotYetTimedOut
265
265
  elsif attempts_limit_reached?
266
+ completed!
266
267
  ack!
267
268
  logger.warn "Beetle: reached the handler execution attempts limit: #{attempts_limit} on #{msg_id}"
268
269
  RC::AttemptsLimitReached
269
270
  elsif exceptions_limit_reached?
271
+ completed!
270
272
  ack!
271
273
  logger.warn "Beetle: reached the handler exceptions limit: #{exceptions_limit} on #{msg_id}"
272
274
  RC::ExceptionsLimitReached
@@ -306,10 +308,12 @@ module Beetle
306
308
  def handler_failed!(result)
307
309
  increment_exception_count!
308
310
  if attempts_limit_reached?
311
+ completed!
309
312
  ack!
310
313
  logger.debug "Beetle: reached the handler execution attempts limit: #{attempts_limit} on #{msg_id}"
311
314
  RC::AttemptsLimitReached
312
315
  elsif exceptions_limit_reached?
316
+ completed!
313
317
  ack!
314
318
  logger.debug "Beetle: reached the handler exceptions limit: #{exceptions_limit} on #{msg_id}"
315
319
  RC::ExceptionsLimitReached
@@ -14,7 +14,7 @@ module Beetle
14
14
  @instances ||= @config.redis_servers.split(/ *, */).map{|s| Redis.from_server_string(s, @options)}
15
15
  end
16
16
 
17
- # fetches the server from the insatnces whith the given <tt>server</tt> string
17
+ # fetches the server from the instances whith the given <tt>server</tt> string
18
18
  def find(server)
19
19
  instances.find{|r| r.server == server}
20
20
  end
@@ -1,3 +1,3 @@
1
1
  module Beetle
2
- VERSION = "0.3.0.rc.17"
2
+ VERSION = "0.3.0.rc.18"
3
3
  end
data/script/start_rabbit CHANGED
@@ -21,9 +21,12 @@ export RABBITMQ_NODENAME=$1
21
21
  export RABBITMQ_NODE_PORT=$2
22
22
  # Defaults to 5672.
23
23
 
24
+ export RABBITMQ_CONFIG_FILE=$3
24
25
  # RABBITMQ_CLUSTER_CONFIG_FILE
25
26
  # Defaults to /etc/rabbitmq/rabbitmq_cluster.config. If this file is present it
26
27
  # is used by the server to auto-configure a RabbitMQ cluster. See the clustering
27
28
  # guide at <http://www.rabbitmq.com/clustering.html> for details.
28
29
 
29
30
  rabbitmq-server
31
+
32
+ rm -f $RABBITMQ_CONFIG_FILE
@@ -111,4 +111,26 @@ module Beetle
111
111
  end
112
112
  end
113
113
 
114
+ class GarbageCollectionTest < Test::Unit::TestCase
115
+ def setup
116
+ @store = DeduplicationStore.new
117
+ end
118
+
119
+ test "never tries to delete message keys when expire key doesn not exist" do
120
+ key = "foo"
121
+ @store.redis.del key
122
+ assert !@store.gc_key(key, 0)
123
+ end
124
+
125
+ test "rescues exeptions and logs an error" do
126
+ RedisServerInfo.expects(:new).raises("foo")
127
+ assert_nothing_raised { @store.garbage_collect_keys_using_master_and_slave }
128
+ end
129
+
130
+ test "logs an error when system command fails" do
131
+ @store.stubs(:system).returns(false)
132
+ @store.logger.expects(:error)
133
+ @store.garbage_collect_keys_using_master_and_slave
134
+ end
135
+ end
114
136
  end
@@ -93,6 +93,16 @@ module Beetle
93
93
  @store.garbage_collect_keys(Time.now.to_i+1)
94
94
  end
95
95
 
96
+ test "should be able to garbage collect expired keys using master and slave" do
97
+ Beetle.config.expects(:gc_threshold).returns(0)
98
+ header = header_with_params({:ttl => 0})
99
+ message = Message.new("somequeue", header, 'foo', :store => @store)
100
+ assert !message.key_exists?
101
+ assert message.key_exists?
102
+ @store.redis.expects(:del).with(*@store.keys(message.msg_id))
103
+ @store.garbage_collect_keys_using_master_and_slave(Time.now.to_i+1)
104
+ end
105
+
96
106
  test "should not garbage collect not yet expired keys" do
97
107
  Beetle.config.expects(:gc_threshold).returns(0)
98
108
  header = header_with_params({:ttl => 0})
@@ -348,7 +358,7 @@ module Beetle
348
358
 
349
359
  proc = lambda {|*args| raise "crash"}
350
360
  s = sequence("s")
351
- message.expects(:completed!).never
361
+ message.expects(:completed!).once
352
362
  header.expects(:ack)
353
363
  assert_equal RC::ExceptionsLimitReached, message.__send__(:process_internal, proc)
354
364
  end
@@ -363,7 +373,7 @@ module Beetle
363
373
 
364
374
  proc = lambda {|*args| raise "crash"}
365
375
  s = sequence("s")
366
- message.expects(:completed!).never
376
+ message.expects(:completed!).once
367
377
  header.expects(:ack)
368
378
  assert_equal RC::AttemptsLimitReached, message.__send__(:process_internal, proc)
369
379
  end
@@ -765,4 +775,3 @@ module Beetle
765
775
  end
766
776
 
767
777
  end
768
-
@@ -324,5 +324,12 @@ module Beetle
324
324
  @server.beetle.expects(:publish).with(:system_notification, ({:message => msg}).to_json)
325
325
  @server.client_started(payload)
326
326
  end
327
+
328
+ test "should not send a system notification when receiving a client started message from a known client" do
329
+ payload = {"id" => "rc-client-1"}
330
+ msg = "Received client_started message from unknown id 'unknown-client'"
331
+ @server.beetle.expects(:publish).never
332
+ @server.client_started(payload)
333
+ end
327
334
  end
328
335
  end
@@ -22,4 +22,4 @@ begin
22
22
  end
23
23
  rescue LoadError => e
24
24
  # do nothing
25
- end unless ENV['TM_FILENAME']
25
+ end if $stdout.tty?
data/test/test_helper.rb CHANGED
@@ -12,17 +12,13 @@ require 'active_support/testing/declarative'
12
12
  require File.expand_path(File.dirname(__FILE__) + '/../lib/beetle')
13
13
  require File.expand_path(File.dirname(__FILE__) + '/colorized_test_output')
14
14
 
15
- # we can remove this hack which is needed only for testing
16
- require 'qrack/errors'
17
-
18
-
19
15
  class Test::Unit::TestCase
20
16
  extend ActiveSupport::Testing::Declarative
21
17
  end
22
18
 
23
-
24
19
  Beetle.config.logger = Logger.new(File.dirname(__FILE__) + '/../test.log')
25
20
  Beetle.config.redis_server = "localhost:6379"
21
+ Beetle.config.redis_servers = "localhost:6379,localhost:6380"
26
22
 
27
23
 
28
24
  def header_with_params(opts = {})
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: beetle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0.rc.17
4
+ version: 0.3.0.rc.18
5
5
  prerelease: 6
6
6
  platform: ruby
7
7
  authors:
@@ -13,7 +13,7 @@ authors:
13
13
  autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
- date: 2012-11-21 00:00:00.000000000 Z
16
+ date: 2013-01-12 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: uuid4r
@@ -86,7 +86,7 @@ dependencies:
86
86
  requirements:
87
87
  - - '='
88
88
  - !ruby/object:Gem::Version
89
- version: 0.9.3
89
+ version: 0.9.10
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
@@ -94,7 +94,7 @@ dependencies:
94
94
  requirements:
95
95
  - - '='
96
96
  - !ruby/object:Gem::Version
97
- version: 0.9.3
97
+ version: 0.9.10
98
98
  - !ruby/object:Gem::Dependency
99
99
  name: amq-protocol
100
100
  requirement: !ruby/object:Gem::Requirement
@@ -102,7 +102,7 @@ dependencies:
102
102
  requirements:
103
103
  - - '='
104
104
  - !ruby/object:Gem::Version
105
- version: 0.9.3
105
+ version: 1.0.1
106
106
  type: :runtime
107
107
  prerelease: false
108
108
  version_requirements: !ruby/object:Gem::Requirement
@@ -110,7 +110,7 @@ dependencies:
110
110
  requirements:
111
111
  - - '='
112
112
  - !ruby/object:Gem::Version
113
- version: 0.9.3
113
+ version: 1.0.1
114
114
  - !ruby/object:Gem::Dependency
115
115
  name: amqp
116
116
  requirement: !ruby/object:Gem::Requirement
@@ -118,7 +118,7 @@ dependencies:
118
118
  requirements:
119
119
  - - '='
120
120
  - !ruby/object:Gem::Version
121
- version: 0.9.6
121
+ version: 0.9.8
122
122
  type: :runtime
123
123
  prerelease: false
124
124
  version_requirements: !ruby/object:Gem::Requirement
@@ -126,7 +126,7 @@ dependencies:
126
126
  requirements:
127
127
  - - '='
128
128
  - !ruby/object:Gem::Version
129
- version: 0.9.6
129
+ version: 0.9.8
130
130
  - !ruby/object:Gem::Dependency
131
131
  name: activesupport
132
132
  requirement: !ruby/object:Gem::Requirement
@@ -203,6 +203,7 @@ files:
203
203
  - lib/beetle/client.rb
204
204
  - lib/beetle/commands/configuration_client.rb
205
205
  - lib/beetle/commands/configuration_server.rb
206
+ - lib/beetle/commands/garbage_collect_deduplication_store.rb
206
207
  - lib/beetle/commands.rb
207
208
  - lib/beetle/configuration.rb
208
209
  - lib/beetle/deduplication_store.rb
@@ -276,7 +277,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
276
277
  version: '0'
277
278
  segments:
278
279
  - 0
279
- hash: -36003570872479659
280
+ hash: -3293468122773492105
280
281
  required_rubygems_version: !ruby/object:Gem::Requirement
281
282
  none: false
282
283
  requirements: