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

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.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: