hutch 0.27.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/hutch/config.rb CHANGED
@@ -49,6 +49,9 @@ module Hutch
49
49
  # RabbitMQ Exchange to use for publishing
50
50
  string_setting :mq_exchange, 'hutch'
51
51
 
52
+ # RabbitMQ Exchange type to use for publishing
53
+ string_setting :mq_exchange_type, 'topic'
54
+
52
55
  # RabbitMQ vhost to use
53
56
  string_setting :mq_vhost, '/'
54
57
 
@@ -142,6 +145,9 @@ module Hutch
142
145
  # Prefix displayed on the consumers tags.
143
146
  string_setting :consumer_tag_prefix, 'hutch'
144
147
 
148
+ # A namespace to help group your queues
149
+ string_setting :namespace, nil
150
+
145
151
  string_setting :group, ''
146
152
 
147
153
  # Set of all setting keys
@@ -190,14 +196,7 @@ module Hutch
190
196
  env_keys_configured.each_with_object({}) {|attr, result|
191
197
  value = ENV[key_for(attr)]
192
198
 
193
- case
194
- when is_bool(attr) || value == 'false'
195
- result[attr] = to_bool(value)
196
- when is_num(attr)
197
- result[attr] = value.to_i
198
- else
199
- result[attr] = value
200
- end
199
+ result[attr] = type_cast(attr, value)
201
200
  }
202
201
  end
203
202
 
@@ -232,7 +231,7 @@ module Hutch
232
231
 
233
232
  def self.set(attr, value)
234
233
  check_attr(attr.to_sym)
235
- user_config[attr.to_sym] = value
234
+ user_config[attr.to_sym] = type_cast(attr, value)
236
235
  end
237
236
 
238
237
  class << self
@@ -269,6 +268,18 @@ module Hutch
269
268
  end
270
269
  end
271
270
 
271
+ def self.type_cast(attr, value)
272
+ case
273
+ when is_bool(attr) || value == 'false'
274
+ to_bool(value)
275
+ when is_num(attr)
276
+ value.to_i
277
+ else
278
+ value
279
+ end
280
+ end
281
+ private_class_method :type_cast
282
+
272
283
  def self.define_methods
273
284
  @config.keys.each do |key|
274
285
  define_singleton_method(key) do
@@ -29,18 +29,47 @@ module Hutch
29
29
  # wants to subscribe to.
30
30
  def consume(*routing_keys)
31
31
  @routing_keys = self.routing_keys.union(routing_keys)
32
+ # these are opt-in
33
+ @queue_mode = nil
34
+ @queue_type = nil
32
35
  end
33
36
 
37
+ attr_reader :queue_mode, :queue_type, :initial_group_size
38
+
34
39
  # Explicitly set the queue name
35
40
  def queue_name(name)
36
41
  @queue_name = name
37
42
  end
38
43
 
39
- # Allow to specify custom arguments that will be passed when creating the queue.
44
+ # Explicitly set the queue mode to 'lazy'
45
+ def lazy_queue
46
+ @queue_mode = 'lazy'
47
+ end
48
+
49
+ # Explicitly set the queue type to 'classic'
50
+ def classic_queue
51
+ @queue_type = 'classic'
52
+ end
53
+
54
+ # Explicitly set the queue type to 'quorum'
55
+ # @param [Hash] options the options params related to quorum queue
56
+ # @option options [Integer] :initial_group_size Initial Replication Factor
57
+ def quorum_queue(options = {})
58
+ @queue_type = 'quorum'
59
+ @initial_group_size = options[:initial_group_size]
60
+ end
61
+
62
+ # Configures an optional argument that will be passed when declaring the queue.
63
+ # Prefer using a policy to this DSL: https://www.rabbitmq.com/parameters.html#policies
40
64
  def arguments(arguments = {})
41
65
  @arguments = arguments
42
66
  end
43
67
 
68
+ # Congfiures queue options that will be passed when declaring the queue.
69
+ def queue_options(options = {})
70
+ @queue_options = options
71
+ end
72
+
44
73
  # Set custom serializer class, override global value
45
74
  def serializer(name)
46
75
  @serializer = name
@@ -58,7 +87,22 @@ module Hutch
58
87
 
59
88
  # Returns consumer custom arguments.
60
89
  def get_arguments
61
- @arguments || {}
90
+ all_arguments = @arguments || {}
91
+
92
+ all_arguments['x-queue-mode'] = @queue_mode if @queue_mode
93
+ all_arguments['x-queue-type'] = @queue_type if @queue_type
94
+ all_arguments['x-quorum-initial-group-size'] = @initial_group_size if @initial_group_size
95
+
96
+ all_arguments
97
+ end
98
+
99
+ def get_options
100
+ default_options = { durable: true }
101
+
102
+ all_options = default_options.merge(@queue_options || {})
103
+ all_options[:arguments] = get_arguments
104
+
105
+ all_options
62
106
  end
63
107
 
64
108
  # Accessor for the consumer's routing key.
@@ -0,0 +1,30 @@
1
+ require "hutch/logging"
2
+ require "bugsnag"
3
+ require "hutch/error_handlers/base"
4
+
5
+ module Hutch
6
+ module ErrorHandlers
7
+ class Bugsnag < Base
8
+ def handle(properties, payload, consumer, ex)
9
+ message_id = properties.message_id
10
+ prefix = "message(#{message_id || "-"}):"
11
+ logger.error "#{prefix} Logging event to Bugsnag"
12
+ logger.error "#{prefix} #{ex.class} - #{ex.message}"
13
+
14
+ ::Bugsnag.notify(ex) do |report|
15
+ report.add_tab(:hutch, {
16
+ payload: payload,
17
+ consumer: consumer
18
+ })
19
+ end
20
+ end
21
+
22
+ def handle_setup_exception(ex)
23
+ logger.error "Logging setup exception to Bugsnag"
24
+ logger.error "#{ex.class} - #{ex.message}"
25
+
26
+ ::Bugsnag.notify(ex)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,31 +1,26 @@
1
1
  require 'hutch/logging'
2
- require 'raven'
2
+ require 'sentry-ruby'
3
3
  require 'hutch/error_handlers/base'
4
4
 
5
5
  module Hutch
6
6
  module ErrorHandlers
7
7
  class Sentry < Base
8
-
9
- def initialize
10
- unless Raven.respond_to?(:capture_exception)
11
- raise "The Hutch Sentry error handler requires Raven >= 0.4.0"
12
- end
13
- end
14
-
15
8
  def handle(properties, payload, consumer, ex)
16
9
  message_id = properties.message_id
17
10
  prefix = "message(#{message_id || '-'}):"
18
11
  logger.error "#{prefix} Logging event to Sentry"
19
12
  logger.error "#{prefix} #{ex.class} - #{ex.message}"
20
- Raven.capture_exception(ex, extra: { payload: payload })
13
+ ::Sentry.configure_scope do |scope|
14
+ scope.set_context("payload", payload)
15
+ end
16
+ ::Sentry.capture_exception(ex)
21
17
  end
22
18
 
23
19
  def handle_setup_exception(ex)
24
20
  logger.error "Logging setup exception to Sentry"
25
21
  logger.error "#{ex.class} - #{ex.message}"
26
- Raven.capture_exception(ex)
22
+ ::Sentry.capture_exception(ex)
27
23
  end
28
-
29
24
  end
30
25
  end
31
26
  end
@@ -0,0 +1,31 @@
1
+ require 'hutch/logging'
2
+ require 'raven'
3
+ require 'hutch/error_handlers/base'
4
+
5
+ module Hutch
6
+ module ErrorHandlers
7
+ class SentryRaven < Base
8
+
9
+ def initialize
10
+ unless Raven.respond_to?(:capture_exception)
11
+ raise "The Hutch Sentry error handler requires Raven >= 0.4.0"
12
+ end
13
+ end
14
+
15
+ def handle(properties, payload, consumer, ex)
16
+ message_id = properties.message_id
17
+ prefix = "message(#{message_id || '-'}):"
18
+ logger.error "#{prefix} Logging event to Sentry"
19
+ logger.error "#{prefix} #{ex.class} - #{ex.message}"
20
+ Raven.capture_exception(ex, extra: { payload: payload })
21
+ end
22
+
23
+ def handle_setup_exception(ex)
24
+ logger.error "Logging setup exception to Sentry"
25
+ logger.error "#{ex.class} - #{ex.message}"
26
+ Raven.capture_exception(ex)
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -2,8 +2,10 @@ module Hutch
2
2
  module ErrorHandlers
3
3
  autoload :Logger, 'hutch/error_handlers/logger'
4
4
  autoload :Sentry, 'hutch/error_handlers/sentry'
5
+ autoload :SentryRaven, 'hutch/error_handlers/sentry_raven'
5
6
  autoload :Honeybadger, 'hutch/error_handlers/honeybadger'
6
7
  autoload :Airbrake, 'hutch/error_handlers/airbrake'
7
8
  autoload :Rollbar, 'hutch/error_handlers/rollbar'
9
+ autoload :Bugsnag, 'hutch/error_handlers/bugsnag'
8
10
  end
9
11
  end
@@ -42,7 +42,7 @@ module Hutch
42
42
  private
43
43
 
44
44
  def log_publication(serializer, payload, routing_key)
45
- logger.info {
45
+ logger.debug {
46
46
  spec =
47
47
  if serializer.binary?
48
48
  "#{payload.bytesize} bytes message"
@@ -6,7 +6,7 @@ module Hutch
6
6
  class JSON
7
7
 
8
8
  def self.encode(payload)
9
- ::JSON.dump(payload)
9
+ ::MultiJson.dump(payload)
10
10
  end
11
11
 
12
12
  def self.decode(payload)
@@ -0,0 +1,17 @@
1
+ require 'ddtrace'
2
+
3
+ module Hutch
4
+ module Tracers
5
+ class Datadog
6
+ def initialize(klass)
7
+ @klass = klass
8
+ end
9
+
10
+ def handle(message)
11
+ ::Datadog.tracer.trace(@klass.class.name, service: 'hutch', span_type: 'rabbitmq') do
12
+ @klass.process(message)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
data/lib/hutch/tracers.rb CHANGED
@@ -2,5 +2,6 @@ module Hutch
2
2
  module Tracers
3
3
  autoload :NullTracer, 'hutch/tracers/null_tracer'
4
4
  autoload :NewRelic, 'hutch/tracers/newrelic'
5
+ autoload :Datadog, 'hutch/tracers/datadog'
5
6
  end
6
7
  end
data/lib/hutch/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Hutch
2
- VERSION = '0.27.0'.freeze
2
+ VERSION = '1.1.1'.freeze
3
3
  end
data/lib/hutch/worker.rb CHANGED
@@ -47,7 +47,7 @@ module Hutch
47
47
  def setup_queue(consumer)
48
48
  logger.info "setting up queue: #{consumer.get_queue_name}"
49
49
 
50
- queue = @broker.queue(consumer.get_queue_name, consumer.get_arguments)
50
+ queue = @broker.queue(consumer.get_queue_name, consumer.get_options)
51
51
  @broker.bind_queue(queue, consumer.routing_keys)
52
52
 
53
53
  queue.subscribe(consumer_tag: unique_consumer_tag, manual_ack: true) do |*args|
@@ -262,7 +262,7 @@ describe Hutch::Broker do
262
262
  args.first == ''
263
263
  args.last == arguments
264
264
  end
265
- broker.queue('test', arguments)
265
+ broker.queue('test', arguments: arguments)
266
266
  end
267
267
  end
268
268
 
@@ -49,6 +49,44 @@ describe Hutch::Config do
49
49
  context 'sets value in user config hash' do
50
50
  it { is_expected.to eq(new_value) }
51
51
  end
52
+
53
+ context 'type casting' do
54
+ context 'number attributes' do
55
+ before { Hutch::Config.set(:heartbeat, new_value) }
56
+ subject(:value) { Hutch::Config.user_config[:heartbeat] }
57
+
58
+ let(:new_value) { "0" }
59
+
60
+
61
+ specify 'casts values to integers' do
62
+ expect(value).to eq 0
63
+ end
64
+ end
65
+ end
66
+
67
+ context 'boolean attributes' do
68
+ before { Hutch::Config.set(:autoload_rails, new_value) }
69
+ subject(:value) { Hutch::Config.user_config[:autoload_rails] }
70
+
71
+ let(:new_value) { "t" }
72
+
73
+
74
+ specify 'casts values to booleans' do
75
+ expect(value).to eq true
76
+ end
77
+ end
78
+
79
+ context 'string attributes' do
80
+ before { Hutch::Config.set(:mq_exchange_type, new_value) }
81
+ subject(:value) { Hutch::Config.user_config[:mq_exchange_type] }
82
+
83
+ let(:new_value) { 1 }
84
+
85
+
86
+ specify 'does not perform any typecasting' do
87
+ expect(value).to eq new_value
88
+ end
89
+ end
52
90
  end
53
91
 
54
92
  context 'for invalid attributes' do
@@ -28,6 +28,32 @@ describe Hutch::Consumer do
28
28
  ComplexConsumer
29
29
  end
30
30
 
31
+ let(:consumer_using_quorum_queue) do
32
+ unless defined? ConsumerUsingQuorumQueue
33
+ class ConsumerUsingQuorumQueue
34
+ include Hutch::Consumer
35
+ consume 'hutch.test1'
36
+ arguments foo: :bar
37
+
38
+ quorum_queue
39
+ end
40
+ end
41
+ ConsumerUsingQuorumQueue
42
+ end
43
+
44
+ let(:consumer_using_classic_queue) do
45
+ unless defined? ConsumerUsingLazyQueue
46
+ class ConsumerUsingLazyQueue
47
+ include Hutch::Consumer
48
+ consume 'hutch.test1'
49
+ arguments foo: :bar
50
+ lazy_queue
51
+ classic_queue
52
+ end
53
+ end
54
+ ConsumerUsingLazyQueue
55
+ end
56
+
31
57
  describe 'module inclusion' do
32
58
  it 'registers the class as a consumer' do
33
59
  expect(Hutch).to receive(:register_consumer) do |klass|
@@ -71,6 +97,43 @@ describe Hutch::Consumer do
71
97
  end
72
98
  end
73
99
 
100
+ describe 'default queue mode' do
101
+ it 'does not specify any mode by default' do
102
+ expect(simple_consumer.queue_mode).to eq(nil)
103
+ expect(simple_consumer.queue_type).to eq(nil)
104
+ end
105
+ end
106
+
107
+ describe '.lazy_queue' do
108
+ context 'when queue mode has been set explicitly to lazy' do
109
+ it 'sets queue mode to lazy' do
110
+ expect(consumer_using_classic_queue.queue_mode).to eq('lazy')
111
+ end
112
+ end
113
+ end
114
+
115
+ describe '.classic_queue' do
116
+ context 'when queue type has been set explicitly to classic' do
117
+ it 'sets queue type to classic' do
118
+ expect(consumer_using_classic_queue.queue_type).to eq('classic')
119
+ end
120
+ end
121
+ end
122
+
123
+ describe '.quorum_queue' do
124
+ context 'when queue type has been set explicitly to quorum' do
125
+ it 'sets queue type to quorum' do
126
+ expect(consumer_using_quorum_queue.queue_type).to eq('quorum')
127
+ end
128
+
129
+ it 'accepts initial group size as an option' do
130
+ consumer = simple_consumer
131
+ expect { consumer.quorum_queue(initial_group_size: 3) }
132
+ .to change { consumer.initial_group_size }.to(3)
133
+ end
134
+ end
135
+ end
136
+
74
137
  describe '.arguments' do
75
138
  let(:args) { { foo: :bar} }
76
139
 
@@ -82,15 +145,30 @@ describe Hutch::Consumer do
82
145
  end
83
146
 
84
147
  describe '.get_arguments' do
85
-
86
148
  context 'when defined' do
87
- it { expect(complex_consumer.get_arguments).to eq(foo: :bar) }
149
+ it { expect(complex_consumer.get_arguments).to include(foo: :bar) }
88
150
  end
89
151
 
90
- context 'when not defined' do
91
- it { expect(simple_consumer.get_arguments).to eq({}) }
152
+ context 'when queue is lazy' do
153
+ it 'has the x-queue-mode argument set to lazy' do
154
+ expect(consumer_using_classic_queue.get_arguments['x-queue-mode'])
155
+ .to eq('lazy')
156
+ end
92
157
  end
93
158
 
159
+ context "when queue's type is quorum" do
160
+ let(:arguments) { consumer_using_quorum_queue.get_arguments }
161
+ it 'has the x-queue-type argument set to quorum' do
162
+ expect(arguments['x-queue-type']).to eq('quorum')
163
+ expect(arguments).to_not have_key('x-quorum-initial-group-size')
164
+ end
165
+
166
+ it 'has the x-quorum-initial-group-size argument set to quorum' do
167
+ consumer_using_quorum_queue.quorum_queue(initial_group_size: 5)
168
+ expect(arguments['x-queue-type']).to eq('quorum')
169
+ expect(arguments['x-quorum-initial-group-size']).to eq(5)
170
+ end
171
+ end
94
172
  end
95
173
 
96
174
  describe '.get_queue_name' do
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ describe Hutch::ErrorHandlers::Bugsnag do
6
+ let(:error_handler) { described_class.new }
7
+
8
+ before do
9
+ Bugsnag.configure do |bugsnag|
10
+ bugsnag.api_key = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
11
+ end
12
+ end
13
+
14
+ describe "#handle" do
15
+ let(:error) do
16
+ begin
17
+ raise "Stuff went wrong"
18
+ rescue RuntimeError => err
19
+ err
20
+ end
21
+ end
22
+
23
+ it "logs the error to Bugsnag" do
24
+ message_id = "1"
25
+ properties = OpenStruct.new(message_id: message_id)
26
+ payload = "{}"
27
+ consumer = double
28
+ ex = error
29
+ message = {
30
+ payload: payload,
31
+ consumer: consumer
32
+ }
33
+
34
+ expect(::Bugsnag).to receive(:notify).with(ex).and_call_original
35
+ expect_any_instance_of(::Bugsnag::Report).to receive(:add_tab).with(:hutch, message)
36
+ error_handler.handle(properties, payload, consumer, ex)
37
+ end
38
+ end
39
+
40
+ describe "#handle_setup_exception" do
41
+ let(:error) do
42
+ begin
43
+ raise "Stuff went wrong"
44
+ rescue RuntimeError => err
45
+ err
46
+ end
47
+ end
48
+
49
+ it "logs the error to Bugsnag" do
50
+ ex = error
51
+ expect(::Bugsnag).to receive(:notify).with(ex)
52
+ error_handler.handle_setup_exception(ex)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hutch::ErrorHandlers::SentryRaven do
4
+ let(:error_handler) { Hutch::ErrorHandlers::SentryRaven.new }
5
+
6
+ describe '#handle' do
7
+ let(:properties) { OpenStruct.new(message_id: "1") }
8
+ let(:payload) { "{}" }
9
+ let(:error) do
10
+ begin
11
+ raise "Stuff went wrong"
12
+ rescue RuntimeError => err
13
+ err
14
+ end
15
+ end
16
+
17
+ it "logs the error to Sentry" do
18
+ expect(Raven).to receive(:capture_exception).with(error, extra: { payload: payload })
19
+ error_handler.handle(properties, payload, double, error)
20
+ end
21
+ end
22
+
23
+ describe '#handle_setup_exception' do
24
+ let(:error) do
25
+ begin
26
+ raise "Stuff went wrong during setup"
27
+ rescue RuntimeError => err
28
+ err
29
+ end
30
+ end
31
+
32
+ it "logs the error to Sentry" do
33
+ expect(Raven).to receive(:capture_exception).with(error)
34
+ error_handler.handle_setup_exception(error)
35
+ end
36
+ end
37
+ end
@@ -15,7 +15,8 @@ describe Hutch::ErrorHandlers::Sentry do
15
15
  end
16
16
 
17
17
  it "logs the error to Sentry" do
18
- expect(Raven).to receive(:capture_exception).with(error, extra: { payload: payload })
18
+ expect(::Sentry).to receive(:capture_exception).with(error).and_call_original
19
+
19
20
  error_handler.handle(properties, payload, double, error)
20
21
  end
21
22
  end
@@ -30,7 +31,8 @@ describe Hutch::ErrorHandlers::Sentry do
30
31
  end
31
32
 
32
33
  it "logs the error to Sentry" do
33
- expect(Raven).to receive(:capture_exception).with(error)
34
+ expect(::Sentry).to receive(:capture_exception).with(error).and_call_original
35
+
34
36
  error_handler.handle_setup_exception(error)
35
37
  end
36
38
  end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Hutch::Tracers::Datadog do
4
+ describe "#handle" do
5
+ subject(:handle) { tracer.handle(message) }
6
+
7
+ let(:tracer) { described_class.new(klass) }
8
+ let(:klass) do
9
+ Class.new do
10
+ attr_reader :message
11
+
12
+ def initialize
13
+ @message = nil
14
+ end
15
+
16
+ def class
17
+ OpenStruct.new(name: 'ClassName')
18
+ end
19
+
20
+ def process(message)
21
+ @message = message
22
+ end
23
+ end.new
24
+ end
25
+ let(:message) { double(:message) }
26
+
27
+ before do
28
+ allow(Datadog.tracer).to receive(:trace).and_call_original
29
+ end
30
+
31
+ it 'uses Datadog tracer' do
32
+ handle
33
+
34
+ expect(Datadog.tracer).to have_received(:trace).with('ClassName',
35
+ hash_including(service: 'hutch', span_type: 'rabbitmq'))
36
+ end
37
+
38
+ it 'processes the message' do
39
+ expect {
40
+ handle
41
+ }.to change { klass.message }.from(nil).to(message)
42
+ end
43
+ end
44
+ end
@@ -4,7 +4,7 @@ require 'hutch/worker'
4
4
  describe Hutch::Worker do
5
5
  let(:consumer) { double('Consumer', routing_keys: %w( a b c ),
6
6
  get_queue_name: 'consumer', get_arguments: {},
7
- get_serializer: nil) }
7
+ get_options: {}, get_serializer: nil) }
8
8
  let(:consumers) { [consumer, double('Consumer')] }
9
9
  let(:broker) { Hutch::Broker.new }
10
10
  let(:setup_procs) { Array.new(2) { Proc.new {} } }
@@ -35,7 +35,7 @@ describe Hutch::Worker do
35
35
  before { allow(broker).to receive_messages(queue: queue, bind_queue: nil) }
36
36
 
37
37
  it 'creates a queue' do
38
- expect(broker).to receive(:queue).with(consumer.get_queue_name, consumer.get_arguments).and_return(queue)
38
+ expect(broker).to receive(:queue).with(consumer.get_queue_name, consumer.get_options).and_return(queue)
39
39
  worker.setup_queue(consumer)
40
40
  end
41
41