beetle 0.2.6 → 0.2.9

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