beetle 0.2.6 → 0.2.9

Sign up to get free protection for your applications and to get access to all the features.
data/RELEASE_NOTES.rdoc CHANGED
@@ -1,5 +1,11 @@
1
1
  = Release Notes
2
2
 
3
+ == Version 0.2.9
4
+
5
+ * Beetle::Client now raises an exception when it fails to publish a message to at least 1 RabbitMQ server
6
+ * Subscribers are now stopped cleanly to avoid 'closed abruptly' messages in the RabbitMQ server log
7
+ * Added send and receive timeouts on the socket and use system_timer for ruby side timeouts
8
+
3
9
  == Version 0.2.6
4
10
 
5
11
  * Set dependency on ActiveSupport to 2.3.x since it ain't compatible to version 3.x yet
data/beetle.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "beetle"
3
- s.version = "0.2.6"
3
+ s.version = "0.2.9"
4
4
 
5
5
  s.required_rubygems_version = ">= 1.3.1"
6
6
  s.authors = ["Stefan Kaes", "Pascal Friederich", "Ali Jelveh", "Sebastian Roebke"]
@@ -31,7 +31,7 @@ Gem::Specification.new do |s|
31
31
  s.specification_version = 3
32
32
  s.add_runtime_dependency("uuid4r", [">= 0.1.1"])
33
33
  s.add_runtime_dependency("bunny", [">= 0.6.0"])
34
- s.add_runtime_dependency("redis", [">= 2.0.4"])
34
+ s.add_runtime_dependency("redis", ["= 2.0.4"])
35
35
  s.add_runtime_dependency("amqp", [">= 0.6.7"])
36
36
  s.add_runtime_dependency("activesupport", ["~> 2.3.4"])
37
37
  s.add_runtime_dependency("daemons", [">= 1.0.10"])
data/examples/attempts.rb CHANGED
@@ -14,15 +14,15 @@ require File.expand_path("../lib/beetle", File.dirname(__FILE__))
14
14
  Beetle.config.logger.level = Logger::INFO
15
15
 
16
16
  # setup client
17
- client = Beetle::Client.new
18
- client.register_queue(:test)
19
- client.register_message(:test)
17
+ $client = Beetle::Client.new
18
+ $client.register_queue(:test)
19
+ $client.register_message(:test)
20
20
 
21
21
  # purge the test queue
22
- client.purge(:test)
22
+ $client.purge(:test)
23
23
 
24
24
  # empty the dedup store
25
- client.deduplication_store.flushdb
25
+ $client.deduplication_store.flushdb
26
26
 
27
27
  # we're starting with 0 exceptions and expect our handler to process the message until the exception count has reached 10
28
28
  $exceptions = 0
@@ -32,35 +32,35 @@ $max_exceptions = 10
32
32
  # in this example we've not only overwritten the process method but also the
33
33
  # error and failure methods of the handler baseclass
34
34
  class Handler < Beetle::Handler
35
-
35
+
36
36
  # called when the handler receives the message - fail everytime
37
37
  def process
38
38
  raise "failed #{$exceptions += 1} times"
39
39
  end
40
-
40
+
41
41
  # called when handler process raised an exception
42
42
  def error(exception)
43
43
  logger.info "execution failed: #{exception}"
44
44
  end
45
-
45
+
46
46
  # called when the handler has finally failed
47
47
  # we're stopping the event loop so this script stops after that
48
48
  def failure(result)
49
49
  super
50
- EM.stop_event_loop
50
+ $client.stop_listening
51
51
  end
52
52
  end
53
53
 
54
54
  # register our handler to the message, configure it to our max_exceptions limit, we configure a delay of 0 to have it not wait before retrying
55
- client.register_handler(:test, Handler, :exceptions => $max_exceptions, :delay => 0)
55
+ $client.register_handler(:test, Handler, :exceptions => $max_exceptions, :delay => 0)
56
56
 
57
57
  # publish a our test message
58
- client.publish(:test, "snafu")
58
+ $client.publish(:test, "snafu")
59
59
 
60
60
  # and start our listening loop...
61
- client.listen
61
+ $client.listen
62
62
 
63
63
  # error handling, if everything went right this shouldn't happen.
64
64
  if $exceptions != $max_exceptions + 1
65
65
  raise "something is fishy. Failed #{$exceptions} times"
66
- end
66
+ end
data/lib/beetle.rb CHANGED
@@ -1,3 +1,4 @@
1
+ $:.unshift(File.expand_path('..', __FILE__))
1
2
  require 'amqp'
2
3
  require 'mq'
3
4
  require 'bunny'
@@ -17,6 +18,8 @@ module Beetle
17
18
  class UnknownQueue < Error; end
18
19
  # raised when no redis master server can be found
19
20
  class NoRedisMaster < Error; end
21
+ # raise when no message could be sent by the publisher
22
+ class NoMessageSent < Error; end
20
23
 
21
24
  # AMQP options for exchange creation
22
25
  EXCHANGE_CREATION_KEYS = [:auto_delete, :durable, :internal, :nowait, :passive]
@@ -56,3 +59,5 @@ module Beetle
56
59
 
57
60
  Timer = RUBY_VERSION < "1.9" ? SystemTimer : Timeout
58
61
  end
62
+
63
+ require 'ext/qrack/client'
data/lib/beetle/client.rb CHANGED
@@ -24,6 +24,9 @@ module Beetle
24
24
  # the AMQP servers available for publishing
25
25
  attr_reader :servers
26
26
 
27
+ # additional AMQP servers available for subscribing. useful for migration scenarios.
28
+ attr_reader :additional_subscription_servers
29
+
27
30
  # an options hash for the configured exchanges
28
31
  attr_reader :exchanges
29
32
 
@@ -46,6 +49,7 @@ module Beetle
46
49
  def initialize(config = Beetle.config)
47
50
  @config = config
48
51
  @servers = config.servers.split(/ *, */)
52
+ @additional_subscription_servers = config.additional_subscription_servers.split(/ *, */)
49
53
  @exchanges = {}
50
54
  @queues = {}
51
55
  @messages = {}
@@ -38,6 +38,8 @@ module Beetle
38
38
 
39
39
  # list of amqp servers to use (defaults to <tt>"localhost:5672"</tt>)
40
40
  attr_accessor :servers
41
+ # list of additional amqp servers to use for subscribers (defaults to <tt>""</tt>)
42
+ attr_accessor :additional_subscription_servers
41
43
  # the virtual host to use on the AMQP servers (defaults to <tt>"/"</tt>)
42
44
  attr_accessor :vhost
43
45
  # the AMQP user to use when connecting to the AMQP servers (defaults to <tt>"guest"</tt>)
@@ -63,6 +65,7 @@ module Beetle
63
65
  self.redis_configuration_client_ids = ""
64
66
 
65
67
  self.servers = "localhost:5672"
68
+ self.additional_subscription_servers = ""
66
69
  self.vhost = "/"
67
70
  self.user = "guest"
68
71
  self.password = "guest"
@@ -77,13 +80,14 @@ module Beetle
77
80
  end
78
81
 
79
82
  def logger
80
- @logger ||= begin
81
- l = Logger.new(log_file)
82
- l.formatter = Logger::Formatter.new
83
- l.level = Logger::INFO
84
- l.datetime_format = "%Y-%m-%d %H:%M:%S"
85
- l
86
- end
83
+ @logger ||=
84
+ begin
85
+ l = Logger.new(log_file)
86
+ l.formatter = Logger::Formatter.new
87
+ l.level = Logger::INFO
88
+ l.datetime_format = "%Y-%m-%d %H:%M:%S"
89
+ l
90
+ end
87
91
  end
88
92
 
89
93
  private
@@ -93,6 +97,7 @@ module Beetle
93
97
  send("#{key}=", value)
94
98
  end
95
99
  rescue Exception
100
+ Beetle::reraise_expectation_errors!
96
101
  logger.error "Error loading beetle config file '#{config_file}': #{$!}"
97
102
  raise
98
103
  end
@@ -80,6 +80,7 @@ module Beetle
80
80
  @flags = headers[:flags].to_i
81
81
  @expires_at = headers[:expires_at].to_i
82
82
  rescue Exception => @exception
83
+ Beetle::reraise_expectation_errors!
83
84
  logger.error "Could not decode message. #{self.inspect}"
84
85
  end
85
86
 
@@ -50,6 +50,7 @@ module Beetle
50
50
  tries -= 1
51
51
  retry if tries > 0
52
52
  logger.error "Beetle: message could not be delivered: #{message_name}"
53
+ raise NoMessageSent.new
53
54
  end
54
55
  published
55
56
  end
@@ -79,9 +80,11 @@ module Beetle
79
80
  case published.size
80
81
  when 0
81
82
  logger.error "Beetle: message could not be delivered: #{message_name}"
83
+ raise NoMessageSent.new
82
84
  when 1
83
85
  logger.warn "Beetle: failed to send message redundantly"
84
86
  end
87
+
85
88
  published.size
86
89
  end
87
90
 
@@ -5,6 +5,7 @@ module Beetle
5
5
  # create a new subscriber instance
6
6
  def initialize(client, options = {}) #:nodoc:
7
7
  super
8
+ @servers.concat @client.additional_subscription_servers
8
9
  @handlers = {}
9
10
  @amqp_connections = {}
10
11
  @mqs = {}
@@ -30,9 +31,14 @@ module Beetle
30
31
  end
31
32
  end
32
33
 
33
- # stops the eventmachine loop
34
+ # closes all AMQP connections and stops the eventmachine loop
34
35
  def stop! #:nodoc:
35
- EM.stop_event_loop
36
+ if @amqp_connections.empty?
37
+ EM.stop_event_loop
38
+ else
39
+ server, connection = @amqp_connections.shift
40
+ connection.close { stop! }
41
+ end
36
42
  end
37
43
 
38
44
  # register handler for the given queues (see Client#register_handler)
@@ -0,0 +1,27 @@
1
+ require 'qrack/client'
2
+
3
+
4
+ module Qrack
5
+ class Client
6
+ # overwrite the timeout method so that SystemTimer is used
7
+ # instead the standard timeout.rb: http://ph7spot.com/musings/system-timer
8
+ delegate :timeout, :to => Beetle::Timer
9
+
10
+ def socket_with_reliable_timeout
11
+ socket_without_reliable_timeout
12
+
13
+ secs = Integer(CONNECT_TIMEOUT)
14
+ usecs = Integer((CONNECT_TIMEOUT - secs) * 1_000_000)
15
+ optval = [secs, usecs].pack("l_2")
16
+
17
+ begin
18
+ @socket.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
19
+ @socket.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
20
+ rescue Errno::ENOPROTOOPT
21
+ end
22
+ @socket
23
+ end
24
+ alias_method_chain :socket, :reliable_timeout
25
+
26
+ end
27
+ end
@@ -11,6 +11,10 @@ module Beetle
11
11
  assert_equal ["localhost:5672"], @client.servers
12
12
  end
13
13
 
14
+ test "should have no additional subscription servers" do
15
+ assert_equal [], @client.additional_subscription_servers
16
+ end
17
+
14
18
  test "should have no exchanges" do
15
19
  assert @client.exchanges.empty?
16
20
  end
@@ -12,7 +12,7 @@ module Beetle
12
12
  config.config_file = "some/path/to/a/file"
13
13
  assert_equal new_value, config.gc_threshold
14
14
  end
15
-
15
+
16
16
  test "should log to STDOUT if no log_file given" do
17
17
  config = Configuration.new
18
18
  Logger.expects(:new).with(STDOUT).returns(stub_everything)
@@ -27,4 +27,4 @@ module Beetle
27
27
  config.logger
28
28
  end
29
29
  end
30
- end
30
+ end
@@ -95,6 +95,8 @@ module Beetle
95
95
  redis1.expects(:get).with("foo:x").raises("disconnected").in_sequence(s)
96
96
  @store.expects(:redis).returns(redis2).in_sequence(s)
97
97
  redis2.expects(:get).with("foo:x").returns("42").in_sequence(s)
98
+ @store.logger.expects(:info)
99
+ @store.logger.expects(:error)
98
100
  assert_equal("42", @store.get("foo", "x"))
99
101
  end
100
102
 
@@ -103,6 +105,8 @@ module Beetle
103
105
  @store.stubs(:redis).returns(redis1)
104
106
  redis1.stubs(:get).with("foo:x").raises("disconnected")
105
107
  @store.stubs(:sleep)
108
+ @store.logger.stubs(:info)
109
+ @store.logger.stubs(:error)
106
110
  assert_raises(NoRedisMaster) { @store.get("foo", "x") }
107
111
  end
108
112
  end
@@ -0,0 +1,26 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+
4
+ class QrackClientExtTest < Test::Unit::TestCase
5
+ def setup
6
+ Qrack::Client.any_instance.stubs(:create_channel).returns(nil)
7
+ @client = Qrack::Client.new
8
+ end
9
+
10
+
11
+ test "should use system-timer for reliable timeouts" do
12
+ Beetle::Timer.expects(:timeout)
13
+ @client.send :timeout, 1, 1 do
14
+ end
15
+ end
16
+
17
+ test "should set send/receive timeouts on the socket" do
18
+ socket_mock = mock("socket")
19
+ @client.instance_variable_set(:@socket, socket_mock)
20
+ @client.stubs(:socket_without_reliable_timeout)
21
+
22
+ socket_mock.expects(:setsockopt).with(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, anything)
23
+ socket_mock.expects(:setsockopt).with(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, anything)
24
+ @client.send(:socket)
25
+ end
26
+ end
@@ -525,7 +525,7 @@ module Beetle
525
525
  test "processing a message catches internal exceptions risen by process_internal and returns an internal error" do
526
526
  header = header_with_params({})
527
527
  message = Message.new("somequeue", header, 'foo', :store => @store)
528
- message.expects(:process_internal).raises(Exception.new)
528
+ message.expects(:process_internal).raises(Exception.new("this is expected"))
529
529
  handler = Handler.new
530
530
  handler.expects(:process_exception).never
531
531
  handler.expects(:process_failure).never
@@ -76,13 +76,16 @@ module Beetle
76
76
  end
77
77
 
78
78
  test "publishing should fail over to the next server" do
79
- failover = sequence('failover')
80
- @pub.expects(:select_next_server).in_sequence(failover)
81
- e = mock("exchange")
82
- @pub.expects(:exchange).with("mama-exchange").returns(e).in_sequence(failover)
83
- e.expects(:publish).raises(Bunny::ConnectionError).in_sequence(failover)
84
- @pub.expects(:stop!).in_sequence(failover)
85
- @pub.expects(:mark_server_dead).in_sequence(failover)
79
+ @pub.servers << "localhost:3333"
80
+ raising_exchange = mock("raising exchange")
81
+ nice_exchange = mock("nice exchange")
82
+ @pub.stubs(:exchange).with("mama-exchange").returns(raising_exchange).then.returns(nice_exchange)
83
+
84
+ raising_exchange.expects(:publish).raises(Bunny::ConnectionError)
85
+ nice_exchange.expects(:publish)
86
+ @pub.expects(:set_current_server).twice
87
+ @pub.expects(:stop!).once
88
+ @pub.expects(:mark_server_dead).once
86
89
  @pub.publish_with_failover("mama-exchange", "mama", @data, @opts)
87
90
  end
88
91
 
@@ -114,7 +117,7 @@ module Beetle
114
117
  assert_equal 1, @pub.publish_with_redundancy("mama-exchange", "mama", @data, @opts)
115
118
  end
116
119
 
117
- test "redundant publishing should return 0 if the message was published to no server" do
120
+ test "redundant publishing should raise an exception if the message was published to no server" do
118
121
  redundant = sequence("redundant")
119
122
  @pub.servers = ["someserver", "someotherserver"]
120
123
  @pub.server = "someserver"
@@ -125,7 +128,9 @@ module Beetle
125
128
  @pub.expects(:exchange).with("mama-exchange").returns(e).in_sequence(redundant)
126
129
  e.expects(:publish).raises(Bunny::ConnectionError).in_sequence(redundant)
127
130
 
128
- assert_equal 0, @pub.publish_with_redundancy("mama-exchange", "mama", @data, @opts)
131
+ assert_raises Beetle::NoMessageSent do
132
+ @pub.publish_with_redundancy("mama-exchange", "mama", @data, @opts)
133
+ end
129
134
  end
130
135
 
131
136
  test "redundant publishing should fallback to failover publishing if less than one server is available" do
@@ -65,13 +65,17 @@ module Beetle
65
65
 
66
66
  test "should clear redis master file if redis from master file is slave" do
67
67
  @client.stubs(:redis_master_from_master_file).returns(stub(:master? => false))
68
+ Beetle::Client.any_instance.stubs(:publish)
68
69
  @client.expects(:clear_redis_master_file)
70
+ @client.expects(:client_started!)
69
71
  @client.start
70
72
  end
71
73
 
72
74
  test "should clear redis master file if redis from master file is not available" do
73
75
  @client.stubs(:redis_master_from_master_file).returns(nil)
76
+ Beetle::Client.any_instance.stubs(:publish)
74
77
  @client.expects(:clear_redis_master_file)
78
+ @client.expects(:client_started!)
75
79
  @client.start
76
80
  end
77
81
 
@@ -51,6 +51,19 @@ module Beetle
51
51
 
52
52
  end
53
53
 
54
+ class AdditionalSubscriptionServersTest < Test::Unit::TestCase
55
+ def setup
56
+ @config = Configuration.new
57
+ @config.additional_subscription_servers = "localhost:1234"
58
+ @client = Client.new(@config)
59
+ @sub = @client.send(:subscriber)
60
+ end
61
+
62
+ test "subscribers server list should contain addtional subcription hosts" do
63
+ assert_equal ["localhost:5672", "localhost:1234"], @sub.servers
64
+ end
65
+ end
66
+
54
67
  class SubscriberQueueManagementTest < Test::Unit::TestCase
55
68
  def setup
56
69
  @client = Client.new
@@ -143,7 +156,7 @@ module Beetle
143
156
 
144
157
  test "exceptions raised from message processing should be ignored" do
145
158
  header = header_with_params({})
146
- Message.any_instance.expects(:process).raises(Exception.new)
159
+ Message.any_instance.expects(:process).raises(Exception.new("don't worry"))
147
160
  assert_nothing_raised { @callback.call(header, 'foo') }
148
161
  end
149
162
 
@@ -214,12 +227,12 @@ module Beetle
214
227
  proc = lambda do |m|
215
228
  block_called = true
216
229
  assert_equal header, m.header
217
- assert_equal "data", m.data
230
+ assert_equal "foo", m.data
218
231
  assert_equal server, m.server
219
232
  end
220
233
  @sub.register_handler("some_queue", &proc)
221
234
  q = mock("QUEUE")
222
- q.expects(:subscribe).with({:ack => true, :key => "#"}).yields(header, 'foo')
235
+ q.expects(:subscribe).with({:ack => true, :key => "#"}).yields(header, "foo")
223
236
  @sub.expects(:queues).returns({"some_queue" => q})
224
237
  @sub.send(:subscribe, "some_queue")
225
238
  assert block_called
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: beetle
3
3
  version: !ruby/object:Gem::Version
4
- hash: 27
4
+ hash: 5
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 2
9
- - 6
10
- version: 0.2.6
9
+ - 9
10
+ version: 0.2.9
11
11
  platform: ruby
12
12
  authors:
13
13
  - Stefan Kaes
@@ -18,7 +18,7 @@ autorequire:
18
18
  bindir: bin
19
19
  cert_chain: []
20
20
 
21
- date: 2010-09-08 00:00:00 +02:00
21
+ date: 2010-10-27 00:00:00 +02:00
22
22
  default_executable: beetle
23
23
  dependencies:
24
24
  - !ruby/object:Gem::Dependency
@@ -59,7 +59,7 @@ dependencies:
59
59
  requirement: &id003 !ruby/object:Gem::Requirement
60
60
  none: false
61
61
  requirements:
62
- - - ">="
62
+ - - "="
63
63
  - !ruby/object:Gem::Version
64
64
  hash: 7
65
65
  segments:
@@ -217,6 +217,7 @@ files:
217
217
  - lib/beetle/redis_server_info.rb
218
218
  - lib/beetle/subscriber.rb
219
219
  - lib/beetle.rb
220
+ - lib/ext/qrack/client.rb
220
221
  - features/README.rdoc
221
222
  - features/redis_auto_failover.feature
222
223
  - features/step_definitions/redis_auto_failover_steps.rb
@@ -240,6 +241,7 @@ files:
240
241
  - test/beetle/client_test.rb
241
242
  - test/beetle/configuration_test.rb
242
243
  - test/beetle/deduplication_store_test.rb
244
+ - test/beetle/ext_test.rb
243
245
  - test/beetle/handler_test.rb
244
246
  - test/beetle/message_test.rb
245
247
  - test/beetle/publisher_test.rb
@@ -293,6 +295,7 @@ test_files:
293
295
  - test/beetle/client_test.rb
294
296
  - test/beetle/configuration_test.rb
295
297
  - test/beetle/deduplication_store_test.rb
298
+ - test/beetle/ext_test.rb
296
299
  - test/beetle/handler_test.rb
297
300
  - test/beetle/message_test.rb
298
301
  - test/beetle/publisher_test.rb