basquiat 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.metrics +5 -0
  4. data/.reek +3 -0
  5. data/.rspec +1 -0
  6. data/.rubocop.yml +2 -1
  7. data/.ruby-version +1 -1
  8. data/Guardfile +5 -4
  9. data/README.md +10 -8
  10. data/basquiat.gemspec +10 -8
  11. data/basquiat_docker.sh +35 -0
  12. data/docker-compose.yml +5 -1
  13. data/docker/Dockerfile +2 -3
  14. data/docker/guard_start.sh +3 -0
  15. data/lib/basquiat.rb +5 -0
  16. data/lib/basquiat/adapters/base_adapter.rb +21 -11
  17. data/lib/basquiat/adapters/base_message.rb +29 -0
  18. data/lib/basquiat/adapters/rabbitmq/configuration.rb +52 -0
  19. data/lib/basquiat/adapters/rabbitmq/connection.rb +89 -0
  20. data/lib/basquiat/adapters/rabbitmq/events.rb +49 -0
  21. data/lib/basquiat/adapters/rabbitmq/message.rb +33 -0
  22. data/lib/basquiat/adapters/rabbitmq/requeue_strategies.rb +3 -0
  23. data/lib/basquiat/adapters/rabbitmq/requeue_strategies/base_strategy.rb +33 -0
  24. data/lib/basquiat/adapters/rabbitmq/requeue_strategies/basic_acknowledge.rb +12 -0
  25. data/lib/basquiat/adapters/rabbitmq/requeue_strategies/dead_lettering.rb +58 -0
  26. data/lib/basquiat/adapters/rabbitmq/requeue_strategies/delayed_delivery.rb +27 -0
  27. data/lib/basquiat/adapters/rabbitmq/session.rb +47 -0
  28. data/lib/basquiat/adapters/rabbitmq_adapter.rb +39 -95
  29. data/lib/basquiat/adapters/test_adapter.rb +4 -3
  30. data/lib/basquiat/errors.rb +2 -0
  31. data/lib/basquiat/errors/strategy_not_registered.rb +14 -0
  32. data/lib/basquiat/errors/subclass_responsibility.rb +9 -0
  33. data/lib/basquiat/interfaces/base.rb +0 -1
  34. data/lib/basquiat/support/configuration.rb +4 -4
  35. data/lib/basquiat/support/hash_refinements.rb +2 -1
  36. data/lib/basquiat/version.rb +1 -1
  37. data/spec/lib/adapters/base_adapter_spec.rb +24 -6
  38. data/spec/lib/adapters/base_message_spec.rb +16 -0
  39. data/spec/lib/adapters/rabbitmq/configuration_spec.rb +47 -0
  40. data/spec/lib/adapters/rabbitmq/connection_spec.rb +45 -0
  41. data/spec/lib/adapters/rabbitmq/events_spec.rb +78 -0
  42. data/spec/lib/adapters/rabbitmq/message_spec.rb +26 -0
  43. data/spec/lib/adapters/rabbitmq/requeue_strategies/basic_acknowledge_spec.rb +38 -0
  44. data/spec/lib/adapters/rabbitmq/requeue_strategies/dead_lettering_spec.rb +102 -0
  45. data/spec/lib/adapters/rabbitmq_adapter_spec.rb +39 -49
  46. data/spec/lib/adapters/test_adapter_spec.rb +15 -19
  47. data/spec/lib/support/configuration_spec.rb +1 -1
  48. data/spec/lib/support/hash_refinements_spec.rb +8 -2
  49. data/spec/spec_helper.rb +8 -5
  50. data/spec/support/rabbitmq_queue_matchers.rb +53 -0
  51. data/spec/support/shared_examples/basquiat_adapter_shared_examples.rb +9 -20
  52. metadata +65 -6
  53. data/.travis.yml +0 -3
  54. data/docker/basquiat_start.sh +0 -9
@@ -0,0 +1,2 @@
1
+ require 'basquiat/errors/subclass_responsibility'
2
+ require 'basquiat/errors/strategy_not_registered'
@@ -0,0 +1,14 @@
1
+ module Basquiat
2
+ module Errors
3
+ class StrategyNotRegistered < StandardError
4
+ def initialize(symbol)
5
+ super()
6
+ @symbol = symbol
7
+ end
8
+
9
+ def message
10
+ "No matching requeue strategy registered as :#{@symbol}"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ module Basquiat
2
+ module Errors
3
+ class SubclassResponsibility < NoMethodError
4
+ def message
5
+ 'This method should be implemented by a subclass tailored to the adapter'
6
+ end
7
+ end
8
+ end
9
+ end
@@ -18,7 +18,6 @@ module Basquiat
18
18
  @adapter.adapter_options Basquiat.configuration.adapter_options
19
19
  end
20
20
 
21
-
22
21
  def event_adapter=(adapter)
23
22
  @adapter = adapter.new
24
23
  end
@@ -3,6 +3,7 @@ require 'naught'
3
3
  require 'erb'
4
4
 
5
5
  module Basquiat
6
+ require 'logger'
6
7
  DefaultLogger = Naught.build { |config| config.mimic Logger }
7
8
 
8
9
  class Configuration
@@ -40,18 +41,17 @@ module Basquiat
40
41
  end
41
42
 
42
43
  def reload_classes
43
- Basquiat::Base.descendants.each do |klass|
44
- klass.reload_adapter_from_configuration
45
- end
44
+ Basquiat::Base.descendants.each(&:reload_adapter_from_configuration)
46
45
  end
47
46
 
48
47
  private
48
+
49
49
  def config
50
50
  @yaml.fetch(environment)
51
51
  end
52
52
 
53
53
  def load_yaml(path)
54
- @yaml = YAML.load(ERB.new(IO.readlines(path).join).result).symbolize_keys
54
+ @yaml = YAML.load(ERB.new(IO.readlines(path).join).result).symbolize_keys
55
55
  end
56
56
 
57
57
  def setup_basic_options
@@ -10,12 +10,13 @@ module Basquiat
10
10
  self[key] = value
11
11
  end
12
12
  end
13
+ self
13
14
  end
14
15
 
15
16
  def symbolize_keys
16
17
  each_with_object({}) do |(key, value), new_hash|
17
18
  new_key = key.to_sym rescue key
18
- new_value = (value.is_a? Hash) ? value.symbolize_keys : value
19
+ new_value = (value.is_a? Hash) ? value.symbolize_keys : value
19
20
  new_hash[new_key] = new_value
20
21
  end
21
22
  end
@@ -1,4 +1,4 @@
1
1
  # Version file
2
2
  module Basquiat
3
- VERSION = '1.1.1'
3
+ VERSION = '1.2.0'
4
4
  end
@@ -1,11 +1,29 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  # Sample class used for testing
4
- class SampleAdapter
5
- include Basquiat::Adapters::Base
6
- end
7
-
8
4
  describe Basquiat::Adapters::Base do
9
- subject { SampleAdapter.new }
10
- it_behaves_like 'a Basquiat::Adapter'
5
+ subject(:adapter) { Basquiat::Adapters::Base.new }
6
+
7
+ [:disconnect, :subscribe_to, :publish].each do |meth|
8
+ it "raise a SubclassResponsibility error if #{meth} isn't implemented" do
9
+ expect { adapter.public_send(meth) }.to raise_error Basquiat::Errors::SubclassResponsibility
10
+ end
11
+ end
12
+
13
+ it 'raise error when using an unregistered strategy' do
14
+ # expect(adapter.use_strategy(:not_here)).to raise_error StrategyNotRegistered
15
+ end
16
+
17
+ it 'register a requeue strategy' do
18
+ class CoolStuff
19
+ end
20
+ adapter.class.register_strategy :cool_stuff, CoolStuff
21
+ expect(adapter.strategies).to have_key :cool_stuff
22
+ end
23
+
24
+ it 'merges the options with the default ones' do
25
+ opts = adapter.instance_variable_get(:@options)
26
+ adapter.adapter_options(nice_option: '127.0.0.2')
27
+ expect(opts[:nice_option]).to eq('127.0.0.2')
28
+ end
11
29
  end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe Basquiat::Adapters::BaseMessage do
4
+ subject(:message) { Basquiat::Adapters::BaseMessage.new({ data: 'everything is AWESOME!' }.to_json) }
5
+
6
+ it 'delegates calls to the JSON' do
7
+ expect(message.fetch(:data)).to eq('everything is AWESOME!')
8
+ expect { message.fetch(:error) }.to raise_error KeyError
9
+ end
10
+
11
+ [:ack?, :unack, :requeue, :delay_redelivery].each do |meth|
12
+ it "raise a SubclassResponsibility error if #{meth} isn't implemented" do
13
+ expect { message.public_send(meth) }.to raise_error Basquiat::Errors::SubclassResponsibility
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+ require 'basquiat/adapters/rabbitmq_adapter'
3
+
4
+ class AwesomeStrategy < Basquiat::Adapters::RabbitMq::BaseStrategy
5
+ def self.session_options
6
+ { exchange: { options: { some_setting: 'awesomesauce' } } }
7
+ end
8
+ end
9
+
10
+ describe Basquiat::Adapters::RabbitMq::Configuration do
11
+ subject(:config) { Basquiat::Adapters::RabbitMq::Configuration.new }
12
+
13
+ # used by the Adapter::Base class
14
+ describe '#merge_user_options', focus: true do
15
+ it 'merges the user supplied options with the default ones' do
16
+ config.merge_user_options(queue: { name: 'config.test.queue' })
17
+ expect(config.base_options[:queue][:name]).to eq('config.test.queue')
18
+ end
19
+ end
20
+
21
+ it '#connection_options' do
22
+ expect(config.connection_options.keys).to contain_exactly(:servers, :auth, :failover)
23
+ end
24
+
25
+ it '#session_options' do
26
+ expect(config.session_options.keys).to contain_exactly(:exchange, :queue, :publisher)
27
+ end
28
+
29
+ context 'Strategies' do
30
+ it 'merges the strategy options with the session ones' do
31
+ Basquiat::Adapters::RabbitMq.register_strategy(:awesome, AwesomeStrategy)
32
+ config.merge_user_options(requeue: { enabled: true, strategy: 'awesome' })
33
+ expect(config.session_options[:exchange][:options]).to have_key(:some_setting)
34
+ end
35
+
36
+ it 'raises an error if trying to use a non-registered strategy' do
37
+ config.merge_user_options(requeue: { enabled: true, strategy: 'perfect' })
38
+ expect { config.strategy }.to raise_error Basquiat::Errors::StrategyNotRegistered
39
+ end
40
+
41
+ it 'deals with the requeue strategy options', focus: true do
42
+ Basquiat::Adapters::RabbitMq.register_strategy :dlx, Basquiat::Adapters::RabbitMq::DeadLettering
43
+ config.merge_user_options(requeue: { enabled: true, strategy: 'dlx', options: { exchange: 'dlx.topic' } })
44
+ expect(config.session_options[:queue][:options]).to include('x-dead-letter-exchange' => 'dlx.topic')
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+ require 'basquiat/adapters/rabbitmq_adapter'
3
+
4
+ describe Basquiat::Adapters::RabbitMq::Connection do
5
+ subject(:connection) { Basquiat::Adapters::RabbitMq::Connection }
6
+
7
+ let(:servers) do
8
+ [{ host: ENV.fetch('BASQUIAT_RABBITMQ_1_PORT_5672_TCP_ADDR') { 'localhost' },
9
+ port: ENV.fetch('BASQUIAT_RABBITMQ_1_PORT_5672_TCP_PORT') { 5672 } }]
10
+ end
11
+
12
+ before(:each) do
13
+ Basquiat.configure { |c| c.logger = Logger.new('log/basquiat.log') }
14
+ end
15
+
16
+ it '#connected?' do
17
+ conn = connection.new(servers: servers)
18
+ expect(conn.connected?).to be_falsey
19
+ conn.start
20
+ expect(conn.connected?).to_not be_truthy
21
+ conn.disconnect
22
+ end
23
+
24
+ context 'failover' do
25
+ let(:failover) do
26
+ { default_timeout: 0.2, max_retries: 2, threaded: false }
27
+ end
28
+
29
+ before(:each) { servers.unshift(host: 'localhost', port: 1234) }
30
+
31
+ it 'tries a reconnection after a few seconds' do
32
+ conn = connection.new(servers: [host: 'localhost', port: 1234],
33
+ failover: { default_timeout: 0.2, max_retries: 1 })
34
+ expect { conn.start }.to raise_error(Bunny::TCPConnectionFailed)
35
+ conn.close
36
+ end
37
+
38
+ it 'uses another server after all retries on a single one' do
39
+ conn = connection.new(servers: servers, failover: failover)
40
+ expect { conn.start }.to_not raise_error
41
+ expect(conn.current_server_uri).to match "#{ENV.fetch('BASQUIAT_RABBITMQ_1_PORT_5672_TCP_PORT') { 5672 }}"
42
+ conn.close
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,78 @@
1
+ require 'spec_helper'
2
+ require 'basquiat/adapters/rabbitmq_adapter'
3
+
4
+ describe Basquiat::Adapters::RabbitMq::Events do
5
+ subject(:events) { Basquiat::Adapters::RabbitMq::Events.new }
6
+
7
+ context 'basic functionality' do
8
+ it 'raises a KeyError when no matching keys are found' do
9
+ expect { events['event'] }.to raise_error KeyError
10
+ end
11
+
12
+ it 'stores the key and value of the proc' do
13
+ proc = -> { 'equal awesome lambda' }
14
+ events['some.awesome.event'] = proc
15
+ expect(events['some.awesome.event']).to eq proc
16
+ end
17
+ end
18
+
19
+ context 'wildcard keys' do
20
+ let(:proc) { -> { 'Hello from the lambda! o/' } }
21
+ describe '*' do
22
+ let(:words) { %w(awesome lame dumb cool) }
23
+ context 'matches any ONE word' do
24
+ it 'at the end' do
25
+ events['some.event.*'] = proc
26
+ words.each do |word|
27
+ expect(events["some.event.#{word}"]).to eq proc
28
+ end
29
+ end
30
+
31
+ it 'in the middle of the name' do
32
+ events['some.*.event'] = proc
33
+ words.each do |word|
34
+ expect(events["some.#{word}.event"]).to eq proc
35
+ end
36
+ end
37
+
38
+ it 'at the start' do
39
+ events['*.some.event'] = proc
40
+ words.each do |word|
41
+ expect(events["#{word}.some.event"]).to eq proc
42
+ end
43
+ end
44
+
45
+ it 'in more than one place' do
46
+ events['some.*.bob.*'] = proc
47
+ words.each do |word|
48
+ expect(events["some.#{word}.bob.#{word}"]).to eq(proc)
49
+ end
50
+ end
51
+ end
52
+ context 'does not match more than ONE word' do
53
+ it 'some.* does not match some.event.dude' do
54
+ events['some.*'] = -> {}
55
+ expect { events['some.event.dude'] }.to raise_error KeyError
56
+ end
57
+ end
58
+ end
59
+ describe '#' do
60
+ context 'matches any number of words' do
61
+ it '# matches all events' do
62
+ events['#'] = proc
63
+ %w(some.cool.event event cool.event).each do |event|
64
+ expect(events[event]).to eq(proc)
65
+ end
66
+ end
67
+
68
+ it 'matches specific events' do
69
+ events['#.event'] = proc
70
+ %w(some.cool.event cool.event).each do |event|
71
+ expect(events[event]).to eq(proc)
72
+ end
73
+ expect { events['event']}.to raise_error KeyError
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+ require 'basquiat/adapters/rabbitmq_adapter'
3
+
4
+ describe Basquiat::Adapters::RabbitMq::Message do
5
+ let(:json) do
6
+ { key: 'value', date: Date.new.iso8601 }.to_json
7
+ end
8
+ subject(:message) { Basquiat::Adapters::RabbitMq::Message.new(json) }
9
+
10
+ it 'delegates all calls to message hash' do
11
+ expect(message[:key]).to eq('value')
12
+ end
13
+
14
+ it 'can be JSONified' do
15
+ expect(MultiJson.dump(message)).to eq(json)
16
+ end
17
+
18
+ it 'exposes the delivery information' do
19
+ expect { message.di }.to_not raise_error
20
+ expect { message.delivery_info }.to_not raise_error
21
+ end
22
+
23
+ it 'exposes the properties of the message' do
24
+ expect { message.props }.to_not raise_error
25
+ end
26
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+ require 'basquiat/adapters/rabbitmq_adapter'
3
+
4
+ describe 'Requeue Strategies' do
5
+ let(:adapter) { Basquiat::Adapters::RabbitMq.new }
6
+ let(:base_options) do
7
+ { servers: [{ host: ENV.fetch('BASQUIAT_RABBITMQ_1_PORT_5672_TCP_ADDR') { 'localhost' },
8
+ port: ENV.fetch('BASQUIAT_RABBITMQ_1_PORT_5672_TCP_PORT') { 5672 } }],
9
+ publisher: { persistent: true } }
10
+ end
11
+
12
+ before(:each) { adapter.adapter_options(base_options) }
13
+ after(:each) { remove_queues_and_exchanges(adapter) }
14
+
15
+ describe 'BasickAcknowledge (aka the default)' do
16
+ it 'acks a message by default' do
17
+ adapter.subscribe_to('some.event', ->(_) { 'Everything is AWESOME!' })
18
+ adapter.listen(block: false)
19
+
20
+ adapter.publish('some.event', data: 'stupid message')
21
+ sleep 0.7 # Wait for the listening thread.
22
+
23
+ expect(adapter.session.queue.message_count).to eq(0)
24
+ expect(adapter.session.queue).to_not have_unacked_messages
25
+ end
26
+
27
+ it 'support declared acks' do
28
+ adapter.subscribe_to('some.event', ->(msg) { msg.ack })
29
+ adapter.listen(block: false)
30
+
31
+ adapter.publish('some.event', data: 'stupid message')
32
+ sleep 0.7 # Wait for the listening thread.
33
+
34
+ expect(adapter.session.queue.message_count).to eq(0)
35
+ expect(adapter.session.queue).to_not have_unacked_messages
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,102 @@
1
+ require 'spec_helper'
2
+ require 'basquiat/adapters/rabbitmq_adapter'
3
+
4
+ describe Basquiat::Adapters::RabbitMq::DeadLettering do
5
+ let(:adapter) { Basquiat::Adapters::RabbitMq.new }
6
+ let(:base_options) do
7
+ { servers: [{ host: ENV.fetch('BASQUIAT_RABBITMQ_1_PORT_5672_TCP_ADDR') { 'localhost' },
8
+ port: ENV.fetch('BASQUIAT_RABBITMQ_1_PORT_5672_TCP_PORT') { 5672 } }],
9
+ publisher: { persistent: true } }
10
+ end
11
+
12
+ before(:each) do
13
+ adapter.adapter_options(base_options)
14
+ adapter.class.register_strategy :dlx, Basquiat::Adapters::RabbitMq::DeadLettering
15
+ end
16
+
17
+ after(:each) { remove_queues_and_exchanges(adapter) }
18
+
19
+ it 'creates the dead letter exchange' do
20
+ adapter.adapter_options(requeue: { enabled: true, strategy: 'dlx' })
21
+ adapter.strategy # initialize the strategy
22
+ channel = adapter.session.channel
23
+ expect(channel.exchanges.keys).to contain_exactly('my.test_exchange', 'basquiat.dlx')
24
+ end
25
+
26
+ it 'creates and binds a dead letter queue' do
27
+ # Initialize the strategy since we won't be listening to anything
28
+ adapter.adapter_options(requeue: { enabled: true, strategy: 'dlx' })
29
+ session = adapter.session
30
+ adapter.strategy
31
+
32
+ # Grabs the Bunny::Channel from the session for checks
33
+ channel = session.channel
34
+ expect(channel.queues.keys).to include('basquiat.dlq')
35
+ expect(channel.queues['basquiat.dlq'].arguments)
36
+ .to match(hash_including('x-dead-letter-exchange' => session.exchange.name, 'x-message-ttl' => 1000))
37
+ expect(session.queue.arguments).to match(hash_including('x-dead-letter-exchange' => 'basquiat.dlx'))
38
+
39
+ expect do
40
+ channel.exchanges['basquiat.dlx'].publish('some message', routing_key: 'some.event')
41
+ sleep 0.1
42
+ end.to change { channel.queues['basquiat.dlq'].message_count }.by(1)
43
+ end
44
+
45
+ context 'if it was the queue that unacked the message then' do
46
+ before(:each) do
47
+ adapter.adapter_options(requeue: { enabled: true, strategy: 'dlx' })
48
+ session = adapter.session
49
+ adapter.strategy # initialize strategy
50
+
51
+ queue = session.channel.queue('sample_queue', arguments: { 'x-dead-letter-exchange' => 'basquiat.dlx' })
52
+ queue.bind(session.exchange, routing_key: 'sample.message')
53
+
54
+ queue.subscribe(manual_ack: true, block: false) do |di, _, _|
55
+ adapter.session.channel.ack(di.delivery_tag)
56
+ end
57
+ end
58
+
59
+ it 'process the message' do
60
+ sample = 0
61
+ adapter.subscribe_to('sample.message', ->(msg) do
62
+ sample += 1
63
+ sample == 3 ? msg.ack : msg.unack
64
+ end)
65
+
66
+ adapter.listen(block: false)
67
+ adapter.publish('sample.message', key: 'message')
68
+
69
+ sleep 3
70
+ expect(sample).to eq(3)
71
+ end
72
+
73
+ it 'else drop the message' do
74
+ ack_count = 0
75
+ sample = 0
76
+
77
+ other = Basquiat::Adapters::RabbitMq.new
78
+ other.adapter_options(base_options.merge(queue: { name: 'other_queue' }, requeue: { enabled: true, strategy: 'dlx' }))
79
+ other.subscribe_to('sample.message', ->(msg) do
80
+ ack_count += 1
81
+ end)
82
+
83
+ adapter.subscribe_to('sample.message', ->(msg) do
84
+ if sample == 3
85
+ msg.ack
86
+ else
87
+ sample += 1;
88
+ msg.unack
89
+ end
90
+ end)
91
+
92
+ other.listen(block: false)
93
+ adapter.listen(block: false)
94
+ adapter.publish('sample.message', key: 'message')
95
+
96
+ sleep 3
97
+ remove_queues_and_exchanges(other)
98
+ expect(ack_count).to eq(1)
99
+ expect(sample).to eq(3)
100
+ end
101
+ end
102
+ end