beetle 0.3.0.rc.2 → 0.3.0.rc.5

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -58,7 +58,7 @@ windows and execute the following commands:
58
58
 
59
59
  To set up a redundant messaging system you will need
60
60
  * at least 2 AMQP servers (we use {RabbitMQ}[http://www.rabbitmq.com/])
61
- * at least one {Redis}[http://github.com/antirez/redis] server (better are two in a master/slave setup)
61
+ * at least one {Redis}[http://github.com/antirez/redis] server (better are two in a master/slave setup, see REDIS_AUTO_FAILOVER.rdoc)
62
62
 
63
63
  == Gem Dependencies
64
64
 
data/RELEASE_NOTES.rdoc CHANGED
@@ -1,5 +1,10 @@
1
1
  = Release Notes
2
2
 
3
+ == Version 0.3.0.rc.3
4
+
5
+ * use hiredis as the redis backend, which overcomes lack of proper time-outs in the "generic" redis-rb
6
+ gem for Ruby 1.9
7
+
3
8
  == Version 0.2.9.8
4
9
 
5
10
  * since version 2.0, RabbitMQ supports Basic.reject(:requeue => true). we use it now too,
data/Rakefile CHANGED
@@ -1,6 +1,8 @@
1
1
  require 'rake'
2
2
  require 'rake/testtask'
3
3
  require 'rcov/rcovtask'
4
+ require 'bundler/gem_tasks'
5
+
4
6
  # rake 0.9.2 hack to supress deprecation warnings caused by cucumber
5
7
  include Rake::DSL if RAKEVERSION >= "0.9"
6
8
  require 'cucumber/rake/task'
@@ -30,7 +32,6 @@ namespace :test do
30
32
  end if RUBY_PLATFORM =~ /darwin/
31
33
  end
32
34
 
33
-
34
35
  namespace :beetle do
35
36
  task :test do
36
37
  Beetle::Client.new.test
@@ -108,8 +109,3 @@ Rake::RDocTask.new do |rdoc|
108
109
  rdoc.rdoc_files.include('MIT-LICENSE')
109
110
  rdoc.rdoc_files.include('lib/**/*.rb')
110
111
  end
111
-
112
- desc "build the beetle gem"
113
- task :build do
114
- system("gem build beetle.gemspec")
115
- end
data/beetle.gemspec CHANGED
@@ -1,13 +1,17 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "beetle/version"
4
+
1
5
  Gem::Specification.new do |s|
2
6
  s.name = "beetle"
3
- s.version = "0.3.0.rc.2"
7
+ s.version = Beetle::VERSION
4
8
  s.required_rubygems_version = ">= 1.3.7"
5
- s.authors = ["Stefan Kaes", "Pascal Friederich", "Ali Jelveh", "Sebastian Roebke"]
9
+ s.authors = ["Stefan Kaes", "Pascal Friederich", "Ali Jelveh", "Sebastian Roebke", "Larry Baltz"]
6
10
  s.date = Time.now.strftime('%Y-%m-%d')
7
11
  s.default_executable = "beetle"
8
12
  s.description = "A highly available, reliable messaging infrastructure"
9
13
  s.summary = "High Availability AMQP Messaging with Redundant Queues"
10
- s.email = "developers@xing.com"
14
+ s.email = "opensource@xing.com"
11
15
  s.executables = ["beetle"]
12
16
  s.extra_rdoc_files = Dir['**/*.rdoc'] + %w(MIT-LICENSE)
13
17
  s.files = Dir['{examples,lib}/**/*.rb'] + Dir['{features,script}/**/*'] + %w(beetle.gemspec Rakefile)
@@ -29,9 +33,12 @@ Gem::Specification.new do |s|
29
33
 
30
34
  s.specification_version = 3
31
35
  s.add_runtime_dependency("uuid4r", [">= 0.1.2"])
32
- s.add_runtime_dependency("bunny", ["~> 0.7.1"])
33
- s.add_runtime_dependency("redis", ["= 2.0.4"])
34
- s.add_runtime_dependency("amqp", ["~> 0.6.7"])
36
+ s.add_runtime_dependency("bunny", ["= 0.7.8"])
37
+ s.add_runtime_dependency("redis", ["= 2.2.0"])
38
+ s.add_runtime_dependency("hiredis", ["= 0.3.2"])
39
+ s.add_runtime_dependency("amq-client", ["= 0.8.3"])
40
+ s.add_runtime_dependency("amq-protocol", ["= 0.8.1"])
41
+ s.add_runtime_dependency("amqp", ["= 0.8.0"])
35
42
  s.add_runtime_dependency("activesupport", [">= 2.3.4"])
36
43
  s.add_runtime_dependency("daemons", [">= 1.0.10"])
37
44
  s.add_development_dependency("rake", [">= 0.8.7"])
@@ -0,0 +1,28 @@
1
+ # nonexistent_server.rb
2
+ # this example shows what happens when you try connect to a nonexistent server
3
+ #
4
+ # start it with ruby nonexistent_server.rb
5
+
6
+ require "rubygems"
7
+ require File.expand_path("../lib/beetle", File.dirname(__FILE__))
8
+
9
+ # set Beetle log level to info, less noisy than debug
10
+ Beetle.config.logger.level = Logger::INFO
11
+
12
+ Beetle.config.servers = "unknown.railsexpress.de:5672"
13
+
14
+ # setup client
15
+ client = Beetle::Client.new
16
+ client.register_queue(:test)
17
+ client.register_message(:test)
18
+
19
+ # register our handler to the message, check out the message.rb for more stuff you can get from the message object
20
+ client.register_handler(:test) {|message| puts "got message: #{message.data}"}
21
+
22
+ # start listening
23
+ # this starts the event machine event loop using EM.run
24
+ # the block passed to listen will be yielded as the last step of the setup process
25
+ client.listen do
26
+ EM.add_timer(10) { client.stop_listening }
27
+ end
28
+
@@ -58,7 +58,7 @@ end
58
58
  # this starts the event machine event loop using EM.run
59
59
  # the block passed to listen will be yielded as the last step of the setup process
60
60
  client.listen do
61
- EM.add_timer(0.1) { client.stop_listening }
61
+ EM.add_timer(0.2) { client.stop_listening }
62
62
  end
63
63
 
64
64
  puts "Received #{k} test messages"
data/examples/rpc.rb CHANGED
@@ -3,7 +3,7 @@ require "rubygems"
3
3
  require File.expand_path(File.dirname(__FILE__)+"/../lib/beetle")
4
4
 
5
5
  # suppress debug messages
6
- Beetle.config.logger.level = Logger::DEBUG
6
+ Beetle.config.logger.level = Logger::INFO
7
7
  Beetle.config.servers = "localhost:5672, localhost:5673"
8
8
  # instantiate a client
9
9
 
@@ -28,12 +28,12 @@ if ARGV.include?("--server")
28
28
  trap("INT") { puts "stopped echo server"; client.stop_listening }
29
29
  end
30
30
  else
31
- n = 100
31
+ n = 10000
32
32
  ms = Benchmark.ms do
33
33
  n.times do |i|
34
34
  content = "Hello #{i}"
35
35
  # puts "performing RPC with message content '#{content}'"
36
- status, result = client.rpc(:echo, content)
36
+ status, result = client.rpc(:echo, content, :persistent => false)
37
37
  # puts "status #{status}"
38
38
  # puts "result #{result}"
39
39
  # puts
data/lib/beetle/base.rb CHANGED
@@ -33,6 +33,10 @@ module Beetle
33
33
  @server = s
34
34
  end
35
35
 
36
+ def server_from_settings(settings)
37
+ settings.values_at(:host,:port).join(':')
38
+ end
39
+
36
40
  def each_server
37
41
  @servers.each { |s| set_current_server(s); yield }
38
42
  end
data/lib/beetle/client.rb CHANGED
@@ -289,7 +289,7 @@ module Beetle
289
289
  end
290
290
 
291
291
  def queue_name_for_tracing(queue)
292
- "trace-#{queue}-#{`hostname`.chomp}-#{$$}"
292
+ "trace-#{queue}-#{Beetle.hostname}-#{$$}"
293
293
  end
294
294
  end
295
295
  end
@@ -73,9 +73,10 @@ module Beetle
73
73
 
74
74
  # extracts various values form the AMQP header properties
75
75
  def decode #:nodoc:
76
- amqp_headers = header.properties
76
+ # p header.attributes
77
+ amqp_headers = header.attributes
77
78
  @uuid = amqp_headers[:message_id]
78
- headers = amqp_headers[:headers]
79
+ headers = amqp_headers[:headers].symbolize_keys
79
80
  @format_version = headers[:format_version].to_i
80
81
  @flags = headers[:flags].to_i
81
82
  @expires_at = headers[:expires_at].to_i
@@ -25,7 +25,7 @@ module Beetle
25
25
 
26
26
  # Unique id for this instance (defaults to the fully qualified hostname)
27
27
  def id
28
- @id ||= `hostname -f`.chomp
28
+ @id ||= Beetle.hostname
29
29
  end
30
30
 
31
31
  def initialize #:nodoc:
@@ -1,8 +1,8 @@
1
1
  module Beetle
2
2
  # A RedisConfigurationServer is the supervisor part of beetle's
3
- # redis failover solution
3
+ # redis failover solution.
4
4
  #
5
- # An single instance of RedisConfigurationServer works as a supervisor for
5
+ # A single instance of RedisConfigurationServer works as a supervisor for
6
6
  # several RedisConfigurationClient instances. It is responsible for watching
7
7
  # the redis master and electing and publishing a new master in case of failure.
8
8
  #
@@ -75,6 +75,7 @@ module Beetle
75
75
  end
76
76
  end
77
77
 
78
+ # called by the message dispatcher when a "client_started" message from a RedisConfigurationClient is received
78
79
  def client_started(payload)
79
80
  id = payload["id"]
80
81
  if client_id_valid?(id)
@@ -186,7 +187,7 @@ module Beetle
186
187
  end
187
188
  known_client
188
189
  end
189
-
190
+
190
191
  def client_id_valid?(client_id)
191
192
  @client_ids.include?(client_id)
192
193
  end
@@ -1,5 +1,4 @@
1
1
  require 'amqp'
2
- require 'mq'
3
2
 
4
3
  module Beetle
5
4
  # Manages subscriptions and message processing on the receiver side of things.
@@ -10,9 +9,10 @@ module Beetle
10
9
  super
11
10
  @servers.concat @client.additional_subscription_servers
12
11
  @handlers = {}
13
- @amqp_connections = {}
14
- @mqs = {}
12
+ @connections = {}
13
+ @channels = {}
15
14
  @subscriptions = {}
15
+ @listened_queues = []
16
16
  end
17
17
 
18
18
  # the client calls this method to subscribe to a list of queues.
@@ -24,11 +24,12 @@ module Beetle
24
24
  #
25
25
  # yields before entering the eventmachine loop (if a block was given)
26
26
  def listen_queues(queues) #:nodoc:
27
+ @listened_queues = queues
28
+ @exchanges_for_queues = exchanges_for_queues(queues)
27
29
  EM.run do
28
- exchanges = exchanges_for_queues(queues)
29
- create_exchanges(exchanges)
30
- bind_queues(queues)
31
- subscribe_queues(queues)
30
+ each_server do
31
+ connect_server connection_settings
32
+ end
32
33
  yield if block_given?
33
34
  end
34
35
  end
@@ -47,10 +48,10 @@ module Beetle
47
48
 
48
49
  # closes all AMQP connections and stops the eventmachine loop
49
50
  def stop! #:nodoc:
50
- if @amqp_connections.empty?
51
+ if @connections.empty?
51
52
  EM.stop_event_loop
52
53
  else
53
- server, connection = @amqp_connections.shift
54
+ server, connection = @connections.shift
54
55
  logger.debug "Beetle: closing connection to #{server}"
55
56
  connection.close { stop! }
56
57
  end
@@ -74,30 +75,19 @@ module Beetle
74
75
  end
75
76
 
76
77
  def create_exchanges(exchanges)
77
- each_server do
78
- exchanges.each { |name| exchange(name) }
79
- end
78
+ exchanges.each { |name| exchange(name) }
80
79
  end
81
80
 
82
81
  def bind_queues(queues)
83
- each_server do
84
- queues.each { |name| queue(name) }
85
- end
82
+ queues.each { |name| queue(name) }
86
83
  end
87
84
 
88
85
  def subscribe_queues(queues)
89
- each_server do
90
- queues.each { |name| subscribe(name) if @handlers.include?(name) }
91
- end
86
+ queues.each { |name| subscribe(name) if @handlers.include?(name) }
92
87
  end
93
88
 
94
- # returns the mq object for the given server or returns a new one created with the
95
- # prefetch(1) option. this tells it to just send one message to the receiving buffer
96
- # (instead of filling it). this is necesssary to ensure that one subscriber always just
97
- # handles one single message. we cannot ensure reliability if the buffer is filled with
98
- # messages and crashes.
99
- def mq(server=@server)
100
- @mqs[server] ||= MQ.new(amqp_connection).prefetch(1)
89
+ def channel(server=@server)
90
+ @channels[server]
101
91
  end
102
92
 
103
93
  def subscriptions(server=@server)
@@ -116,12 +106,8 @@ module Beetle
116
106
  callback = create_subscription_callback(queue_name, amqp_queue_name, handler, opts)
117
107
  keys = opts.slice(*SUBSCRIPTION_KEYS).merge(:key => "#", :ack => true)
118
108
  logger.debug "Beetle: subscribing to queue #{amqp_queue_name} with key # on server #{@server}"
119
- begin
120
- queues[queue_name].subscribe(keys, &callback)
121
- subscriptions[queue_name] = [keys, callback]
122
- rescue MQ::Error
123
- error("Beetle: binding multiple handlers for the same queue isn't possible.")
124
- end
109
+ queues[queue_name].subscribe(keys, &callback)
110
+ subscriptions[queue_name] = [keys, callback]
125
111
  end
126
112
 
127
113
  def pause(queue_name)
@@ -147,13 +133,14 @@ module Beetle
147
133
  if result.reject?
148
134
  sleep 1
149
135
  header.reject(:requeue => true)
150
- elsif reply_to = header.properties[:reply_to]
136
+ elsif reply_to = header.attributes[:reply_to]
137
+ # logger.info "Beetle: sending reply to queue #{reply_to}"
151
138
  # require 'ruby-debug'
152
139
  # Debugger.start
153
140
  # debugger
154
141
  status = result == Beetle::RC::OK ? "OK" : "FAILED"
155
- exchange = MQ::Exchange.new(mq(server), :direct, "", :key => reply_to)
156
- exchange.publish(m.handler_result.to_s, :headers => {:status => status})
142
+ exchange = AMQP::Exchange.new(channel(server), :direct, "")
143
+ exchange.publish(m.handler_result.to_s, :routing_key => reply_to, :persistent => false, :headers => {:status => status})
157
144
  end
158
145
  # logger.debug "Beetle: processed message"
159
146
  rescue Exception
@@ -168,27 +155,63 @@ module Beetle
168
155
  end
169
156
 
170
157
  def create_exchange!(name, opts)
171
- mq.__send__(opts[:type], name, opts.slice(*EXCHANGE_CREATION_KEYS))
158
+ channel.__send__(opts[:type], name, opts.slice(*EXCHANGE_CREATION_KEYS))
172
159
  end
173
160
 
174
161
  def bind_queue!(queue_name, creation_keys, exchange_name, binding_keys)
175
- queue = mq.queue(queue_name, creation_keys)
162
+ queue = channel.queue(queue_name, creation_keys)
176
163
  exchange = exchange(exchange_name)
177
164
  queue.bind(exchange, binding_keys)
178
165
  queue
179
166
  end
180
167
 
181
- def amqp_connection(server=@server)
182
- @amqp_connections[server] ||= new_amqp_connection
168
+ def connection_settings
169
+ {
170
+ :host => current_host, :port => current_port, :logging => false,
171
+ :user => Beetle.config.user, :pass => Beetle.config.password, :vhost => Beetle.config.vhost,
172
+ :on_tcp_connection_failure => on_tcp_connection_failure
173
+ }
174
+ end
175
+
176
+ def on_tcp_connection_failure
177
+ Proc.new do |settings|
178
+ logger.warn "Beetle: connection failed: #{server_from_settings(settings)}"
179
+ EM::Timer.new(10) { connect_server(settings) }
180
+ end
183
181
  end
184
182
 
185
- def new_amqp_connection
186
- # FIXME: wtf, how to test that reconnection feature....
187
- con = AMQP.connect(:host => current_host, :port => current_port, :logging => false,
188
- :user => Beetle.config.user, :pass => Beetle.config.password, :vhost => Beetle.config.vhost)
189
- con.instance_variable_set("@on_disconnect", proc{ con.__send__(:reconnect) })
190
- con
183
+ def on_tcp_connection_loss(connection, settings)
184
+ # reconnect in 10 seconds, without enforcement
185
+ logger.warn "Beetle: lost connection: #{server_from_settings(settings)}. reconnecting."
186
+ connection.reconnect(false, 10)
191
187
  end
192
188
 
189
+ def connect_server(settings)
190
+ server = server_from_settings settings
191
+ logger.info "Beetle: connecting to rabbit #{server}"
192
+ AMQP.connect(settings) do |connection|
193
+ connection.on_tcp_connection_loss(&method(:on_tcp_connection_loss))
194
+ @connections[server] = connection
195
+ open_channel_and_subscribe(connection, settings)
196
+ end
197
+ rescue EventMachine::ConnectionError => e
198
+ # something serious went wrong, for example DNS lookup failure
199
+ # in this case, the on_tcp_connection_failure callback is never called automatically
200
+ logger.error "Beetle: connection failed: #{e.class}(#{e})"
201
+ settings[:on_tcp_connection_failure].call(settings)
202
+ end
203
+
204
+ def open_channel_and_subscribe(connection, settings)
205
+ server = server_from_settings settings
206
+ AMQP::Channel.new(connection) do |channel|
207
+ channel.auto_recovery = true
208
+ channel.prefetch(1)
209
+ set_current_server server
210
+ @channels[server] = channel
211
+ create_exchanges(@exchanges_for_queues)
212
+ bind_queues(@listened_queues)
213
+ subscribe_queues(@listened_queues)
214
+ end
215
+ end
193
216
  end
194
217
  end
@@ -0,0 +1,3 @@
1
+ module Beetle
2
+ VERSION = "0.3.0.rc.5"
3
+ end
data/lib/beetle.rb CHANGED
@@ -1,12 +1,27 @@
1
1
  $:.unshift(File.expand_path('..', __FILE__))
2
2
  require 'bunny'
3
3
  require 'uuid4r'
4
+ require 'redis/connection/hiredis' # require *before* redis as specified in the redis-rb gem docs
5
+ require 'redis'
4
6
  require 'active_support'
5
7
  require 'active_support/core_ext'
6
- require 'redis'
7
8
  require 'set'
9
+ require 'socket'
8
10
 
9
11
  module Beetle
12
+ Timer = if RUBY_VERSION < "1.9"
13
+ begin
14
+ require 'system_timer'
15
+ SystemTimer
16
+ rescue Exception => e
17
+ warn "WARNING: It's highly recommended to install the SystemTimer gem: `gem install SystemTimer -v '=1.2.1'` See: http://ph7spot.com/musings/system-timer" if RUBY_VERSION < "1.9"
18
+ require 'timeout'
19
+ Timeout
20
+ end
21
+ else
22
+ require 'timeout'
23
+ Timeout
24
+ end
10
25
 
11
26
  # abstract superclass for Beetle specific exceptions
12
27
  class Error < StandardError; end
@@ -32,6 +47,13 @@ module Beetle
32
47
  # AMQP options for subscribing to queues
33
48
  SUBSCRIPTION_KEYS = [:ack, :key]
34
49
 
50
+ # determine the fully qualified domainname of the host we're running on
51
+ def self.hostname
52
+ name = Socket.gethostname
53
+ parts = name.split('.')
54
+ parts.size > 1 ? name : Socket.gethostbyname(parts.first).first
55
+ end
56
+
35
57
  # use ruby's autoload mechanism for loading beetle classes
36
58
  lib_dir = File.expand_path(File.dirname(__FILE__) + '/beetle/')
37
59
  Dir["#{lib_dir}/*.rb"].each do |libfile|
@@ -56,11 +78,4 @@ module Beetle
56
78
  def self.reraise_expectation_errors! #:nodoc:
57
79
  end
58
80
  end
59
-
60
- Timer = begin
61
- RUBY_VERSION < "1.9" ? SystemTimer : Timeout
62
- rescue NameError
63
- warn "WARNING: It's highly recommended to install the SystemTimer gem: `gem install SystemTimer -v '=1.2.1'` See: http://ph7spot.com/musings/system-timer" if RUBY_VERSION < "1.9"
64
- Timeout
65
- end
66
81
  end
@@ -0,0 +1,30 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+ require 'eventmachine'
3
+ require 'amqp'
4
+
5
+ class AMQPGemBehaviorTest < Test::Unit::TestCase
6
+ test "subscribing twice to the same queue raises a RuntimeError which throws us out of the event loop" do
7
+ begin
8
+ @exception = nil
9
+ EM.run do
10
+ AMQP.start do |connection|
11
+ begin
12
+ EM::Timer.new(1){ connection.close { EM.stop }}
13
+ channel = AMQP::Channel.new(connection)
14
+ channel.on_error { puts "woot"}
15
+ exchange = channel.topic("beetle_tests")
16
+ queue = AMQP::Queue.new(channel)
17
+ queue.bind(exchange, :key => "#")
18
+ queue.subscribe { }
19
+ queue.subscribe { }
20
+ rescue
21
+ # we never get here, because the subscription is deferred
22
+ # the only known way to avoid this is to use the block version of AMQP::Queue.new
23
+ end
24
+ end
25
+ end
26
+ rescue Exception => @exception
27
+ end
28
+ assert @exception
29
+ end
30
+ end
@@ -48,5 +48,9 @@ module Beetle
48
48
  @bs.send(:set_current_server, "xxx:123")
49
49
  assert_equal "xxx:123", @bs.server
50
50
  end
51
+
52
+ test "server_from_settings should create a valid server string from an AMQP settings hash" do
53
+ assert_equal "goofy:123", @bs.send(:server_from_settings, {:host => "goofy", :port => 123})
54
+ end
51
55
  end
52
56
  end
@@ -9,8 +9,8 @@ module Beetle
9
9
  end
10
10
 
11
11
  test "trying to delete a non existent key doesn't throw an error" do
12
- assert !@r.del("hahahaha")
13
12
  assert !@r.exists("hahahaha")
13
+ assert_equal 0, @r.del("hahahaha")
14
14
  end
15
15
 
16
16
  test "msetnx returns 0 or 1" do
@@ -55,11 +55,9 @@ module Beetle
55
55
  end
56
56
  end
57
57
 
58
- class RedisTimeoutTest < Test::Unit::TestCase
59
- test "should use a timer" do
60
- r = Redis.new(:host => "localhost", :port => 6390, :timeout => 1)
61
- r.client.expects(:with_timeout).with(1).raises(Timeout::Error)
62
- assert_equal({}, r.info_with_rescue)
58
+ class HiredisLoadedTest < Test::Unit::TestCase
59
+ test 'should be using hiredis instead of the redis ruby backend' do
60
+ assert defined?(Hiredis)
63
61
  end
64
62
  end
65
63
  end