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

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
@@ -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