basquiat 1.1.1 → 1.2.0

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