hutch 0.27.0 → 1.1.1

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