basquiat 1.2.0 → 1.3.0.pre.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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +50 -0
  3. data/.gitignore +1 -0
  4. data/.metrics +1 -5
  5. data/.reek +0 -1
  6. data/.rubocop.yml +2 -1
  7. data/.ruby-version +1 -1
  8. data/Guardfile +4 -0
  9. data/README.md +77 -61
  10. data/basquiat.gemspec +5 -4
  11. data/basquiat_docker.sh +1 -1
  12. data/docker/Dockerfile +8 -3
  13. data/docker-compose.yml +3 -3
  14. data/lib/basquiat/adapters/base_adapter.rb +42 -7
  15. data/lib/basquiat/adapters/base_message.rb +14 -9
  16. data/lib/basquiat/adapters/rabbitmq/configuration.rb +31 -16
  17. data/lib/basquiat/adapters/rabbitmq/connection.rb +33 -56
  18. data/lib/basquiat/adapters/rabbitmq/events.rb +1 -1
  19. data/lib/basquiat/adapters/rabbitmq/message.rb +10 -9
  20. data/lib/basquiat/adapters/rabbitmq/requeue_strategies/auto_acknowledge.rb +15 -0
  21. data/lib/basquiat/adapters/rabbitmq/requeue_strategies/base_strategy.rb +8 -4
  22. data/lib/basquiat/adapters/rabbitmq/requeue_strategies/basic_acknowledge.rb +1 -1
  23. data/lib/basquiat/adapters/rabbitmq/requeue_strategies/dead_lettering.rb +16 -15
  24. data/lib/basquiat/adapters/rabbitmq/requeue_strategies/delayed_delivery.rb +80 -16
  25. data/lib/basquiat/adapters/rabbitmq/requeue_strategies.rb +7 -0
  26. data/lib/basquiat/adapters/rabbitmq/session.rb +12 -14
  27. data/lib/basquiat/adapters/rabbitmq_adapter.rb +35 -16
  28. data/lib/basquiat/adapters/test_adapter.rb +2 -5
  29. data/lib/basquiat/errors/strategy_not_registered.rb +1 -9
  30. data/lib/basquiat/interfaces/base.rb +28 -7
  31. data/lib/basquiat/support/configuration.rb +33 -4
  32. data/lib/basquiat/support/hash_refinements.rb +9 -0
  33. data/lib/basquiat/support/json.rb +9 -0
  34. data/lib/basquiat/version.rb +1 -1
  35. data/lib/basquiat.rb +6 -1
  36. data/spec/lib/adapters/base_adapter_spec.rb +1 -1
  37. data/spec/lib/adapters/base_message_spec.rb +0 -6
  38. data/spec/lib/adapters/rabbitmq/configuration_spec.rb +2 -2
  39. data/spec/lib/adapters/rabbitmq/connection_spec.rb +8 -13
  40. data/spec/lib/adapters/rabbitmq/events_spec.rb +8 -1
  41. data/spec/lib/adapters/rabbitmq/requeue_strategies/auto_acknowledge_spec.rb +24 -0
  42. data/spec/lib/adapters/rabbitmq/requeue_strategies/basic_acknowledge_spec.rb +4 -4
  43. data/spec/lib/adapters/rabbitmq/requeue_strategies/dead_lettering_spec.rb +23 -28
  44. data/spec/lib/adapters/rabbitmq/requeue_strategies/delayed_delivery_spec.rb +105 -0
  45. data/spec/lib/adapters/rabbitmq_adapter_spec.rb +21 -17
  46. data/spec/lib/basquiat_spec.rb +0 -6
  47. data/spec/lib/interfaces/base_spec.rb +11 -19
  48. data/spec/lib/support/configuration_spec.rb +0 -8
  49. data/spec/lib/support/hash_refinements_spec.rb +2 -2
  50. data/spec/spec_helper.rb +2 -6
  51. data/spec/support/rabbitmq_queue_matchers.rb +9 -3
  52. metadata +21 -12
@@ -1,7 +1,7 @@
1
1
  require 'set'
2
2
 
3
3
  module Basquiat
4
- # base module extend the classes that will use the event infrastructure
4
+ # Base module used to extend the classes so that they will be able to use the event infrastructure
5
5
  module Base
6
6
  class << self
7
7
  def extended(klass)
@@ -14,41 +14,62 @@ module Basquiat
14
14
  end
15
15
 
16
16
  def reload_adapter_from_configuration
17
- @adapter = Kernel.const_get(Basquiat.configuration.default_adapter).new
18
- @adapter.adapter_options Basquiat.configuration.adapter_options
17
+ @adapter = Kernel.const_get Basquiat.configuration.default_adapter
18
+ adapter_options Basquiat.configuration.adapter_options
19
19
  end
20
20
 
21
- def event_adapter=(adapter)
22
- @adapter = adapter.new
21
+ # @!attribute [rw] adapter
22
+ # Initializes and return a instance of the default adapter specified on Basquiat.configuration.default_adapter
23
+ # @return [Basquiat::Adapter] the adapter instance for the current class
24
+ # @deprecated event_adapter is deprecated and will be removed eventually. Please use {#adapter}.
25
+ def adapter=(adapter_klass)
26
+ @adapter = adapter_klass.new
23
27
  end
28
+ alias_method :event_adapter=, :adapter=
24
29
 
25
30
  def adapter
26
31
  @adapter ||= Kernel.const_get(Basquiat.configuration.default_adapter).new
27
32
  end
28
33
 
34
+ # @param opts [Hash] The adapter specific options. Defaults to Basquiat.configuration.adapter_options
29
35
  def adapter_options(opts = Basquiat.configuration.adapter_options)
30
36
  adapter.adapter_options(opts)
31
37
  end
32
38
 
39
+ # Publishes the message of type event to the queue. Note that the message will be converted to a JSON
40
+ # @param event [String] the event name
41
+ # @param message [#to_json] Message to be JSONfied and sent to the Message Queue
33
42
  def publish(event, message)
34
43
  adapter.publish(event, message)
35
44
  end
36
45
 
46
+ # Subscribe the event with the proc passed.
47
+ # @param event_name [String] the event name
48
+ # @param proc [Symbol, #call] the proc to be executed when the event is consumed.
49
+ # You can pass anything that answers to call or a symbol.
50
+ # If a symbol is passed it will try to look for a public class method of the same name.
37
51
  def subscribe_to(event_name, proc)
38
52
  proc = make_callable(proc)
39
53
  adapter.subscribe_to(event_name, proc)
40
54
  end
41
55
 
56
+ # Utility method to force a disconnect from the message queue.
57
+ # @note The adapter should reconnect automatically.
42
58
  def disconnect
43
59
  adapter.disconnect
44
60
  end
45
61
 
62
+ # Utility method to check connection status
63
+ # @return [truthy, falsey]
46
64
  def connected?
47
65
  adapter.connected?
48
66
  end
49
67
 
50
- def listen(block: true)
51
- adapter.listen(block: block)
68
+ # Starts the consumer loop
69
+ # @param block [Boolean] If it should block the thread. The relevance of this is dictated by the adapter.
70
+ # Defaults to true.
71
+ def listen(block: true, rescue_proc: -> {})
72
+ adapter.listen(block: block, rescue_proc: rescue_proc)
52
73
  end
53
74
 
54
75
  private
@@ -1,6 +1,6 @@
1
- require 'basquiat/support/hash_refinements'
2
1
  require 'naught'
3
2
  require 'erb'
3
+ require 'basquiat/support/hash_refinements'
4
4
 
5
5
  module Basquiat
6
6
  require 'logger'
@@ -9,8 +9,32 @@ module Basquiat
9
9
  class Configuration
10
10
  using HashRefinements
11
11
 
12
+ def initialize
13
+ @yaml = {}
14
+ @rescue_proc = lambda do |exception, message|
15
+ logger.error do
16
+ { exception: exception, stack_trace: exception.stack_trace, message: message }.to_json
17
+ end
18
+ fail exception
19
+ end
20
+ end
21
+
22
+ # @!attribute queue_name
23
+ # @return [String] the queue name. Defaults to 'basquiat.queue'
24
+ # @!attribute exchange_name
25
+ # @return [String] the exchange name. Defaults to 'basquiat.exchange'
26
+ # @!attribute logger
27
+ # @return [Logger] return the application logger. Defaults to {DefaultLogger}.
28
+ # @!attribute environment
29
+ # @return [Symbol] return the set environment or the value of the 'BASQUIAT_ENV' environment variable
30
+ # or :development
12
31
  attr_writer :queue_name, :exchange_name, :logger, :environment
13
32
 
33
+ # @!attribute rescue_proc
34
+ # @return [#call] return the callable to be executed when some exception is thrown. The callable receives the
35
+ # exception and message
36
+ attr_accessor :rescue_proc
37
+
14
38
  def queue_name
15
39
  @queue_name || 'basquiat.queue'
16
40
  end
@@ -27,19 +51,24 @@ module Basquiat
27
51
  (@environment || ENV['BASQUIAT_ENV'] || 'development').to_sym
28
52
  end
29
53
 
54
+ # Loads a YAML file with the configuration options
55
+ # @param path [String] the path of the configuration file
30
56
  def config_file=(path)
31
57
  load_yaml(path)
32
58
  setup_basic_options
33
59
  end
34
60
 
61
+ # @return [Hash] return the configured adapter options. Defaults to an empty {::Hash}
35
62
  def adapter_options
36
63
  config.fetch(:adapter_options) { Hash.new }
37
64
  end
38
65
 
66
+ # @return [String] return the configured default adapter. Defaults to {Adapters::Test}
39
67
  def default_adapter
40
- config.fetch(:default_adapter) { Basquiat::Adapter::Test }
68
+ config.fetch(:default_adapter) { 'Basquiat::Adapters::Test' }
41
69
  end
42
70
 
71
+ # Used by the railtie. Forces the reconfiguration of all extended classes
43
72
  def reload_classes
44
73
  Basquiat::Base.descendants.each(&:reload_adapter_from_configuration)
45
74
  end
@@ -47,7 +76,7 @@ module Basquiat
47
76
  private
48
77
 
49
78
  def config
50
- @yaml.fetch(environment)
79
+ @yaml.fetch(environment, {})
51
80
  end
52
81
 
53
82
  def load_yaml(path)
@@ -55,7 +84,7 @@ module Basquiat
55
84
  end
56
85
 
57
86
  def setup_basic_options
58
- @queue_name ||= config.fetch(:queue_name) { 'basquiat.exchange' }
87
+ @queue_name ||= config.fetch(:queue_name) { 'basquiat.exchange' }
59
88
  @exchange_name ||= config.fetch(:exchange_name) { 'basquiat.queue' }
60
89
  end
61
90
  end
@@ -1,5 +1,14 @@
1
1
  module Basquiat
2
2
  module HashRefinements
3
+ # @!method deep_merge
4
+ # Merges self with other_hash recursively
5
+ # @param other_hash [Hash] hash to be merged into self
6
+ # @return [self]
7
+
8
+ # @!method symbolize_keys
9
+ # Symbolize all the keys in a given hash. Works with nested hashes
10
+ # @return [Hash] return other hash with the symbolized keys
11
+
3
12
  refine Hash do
4
13
  def deep_merge(other_hash)
5
14
  other_hash.each_pair do |key, value|
@@ -1,9 +1,18 @@
1
1
  module Basquiat
2
+ # A simple MultiJson wrapper to protect against eventual API changes.
2
3
  module Json
4
+ # Serializes an Object into a JSON
5
+ # @see MultiJson.dump
6
+ # @param object [Object] object to be serialized
7
+ # @return [String] JSON representation of the object
3
8
  def self.encode(object)
4
9
  MultiJson.dump(object)
5
10
  end
6
11
 
12
+ # De-serializes a JSON into a Hash
13
+ # @see MultiJson.load
14
+ # @param object [Object] object to be de-serialized
15
+ # @return [Hash] Hash representing the JSON. The keys are symbolized by default
7
16
  def self.decode(object)
8
17
  MultiJson.load(object, symbolize_keys: true)
9
18
  rescue MultiJson::ParseError
@@ -1,4 +1,4 @@
1
1
  # Version file
2
2
  module Basquiat
3
- VERSION = '1.2.0'
3
+ VERSION = '1.3.0-1'
4
4
  end
data/lib/basquiat.rb CHANGED
@@ -8,21 +8,26 @@ require 'basquiat/adapters'
8
8
  require 'basquiat/version'
9
9
  require 'basquiat/interfaces/base'
10
10
 
11
- # Overall namespace And config class
11
+ # Overall namespace config class
12
12
  module Basquiat
13
13
  class << self
14
+ # resets the gems configuration. Useful for testing and not much else
14
15
  def reset
15
16
  @configuration = Configuration.new
16
17
  end
17
18
 
19
+ # @return [Configuration] returns or initializes the Configuration object
18
20
  def configuration
19
21
  @configuration ||= Configuration.new
20
22
  end
21
23
 
24
+ # used to configure the gem using a block
25
+ # @yieldparam [Configuration] configuration the current {Configuration} instance
22
26
  def configure
23
27
  yield configuration
24
28
  end
25
29
 
30
+ # @return [Logger] shorthand for configuration.logger
26
31
  def logger
27
32
  configuration.logger
28
33
  end
@@ -11,7 +11,7 @@ describe Basquiat::Adapters::Base do
11
11
  end
12
12
 
13
13
  it 'raise error when using an unregistered strategy' do
14
- # expect(adapter.use_strategy(:not_here)).to raise_error StrategyNotRegistered
14
+ expect { adapter.class.strategy(:not_here) }.to raise_error Basquiat::Errors::StrategyNotRegistered
15
15
  end
16
16
 
17
17
  it 'register a requeue strategy' do
@@ -7,10 +7,4 @@ describe Basquiat::Adapters::BaseMessage do
7
7
  expect(message.fetch(:data)).to eq('everything is AWESOME!')
8
8
  expect { message.fetch(:error) }.to raise_error KeyError
9
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
10
  end
@@ -19,11 +19,11 @@ describe Basquiat::Adapters::RabbitMq::Configuration do
19
19
  end
20
20
 
21
21
  it '#connection_options' do
22
- expect(config.connection_options.keys).to contain_exactly(:servers, :auth, :failover)
22
+ expect(config.connection_options.keys).to contain_exactly(:hosts, :auth, :port)
23
23
  end
24
24
 
25
25
  it '#session_options' do
26
- expect(config.session_options.keys).to contain_exactly(:exchange, :queue, :publisher)
26
+ expect(config.session_options.keys).to contain_exactly(:exchange, :queue, :publisher, :consumer)
27
27
  end
28
28
 
29
29
  context 'Strategies' do
@@ -4,17 +4,12 @@ require 'basquiat/adapters/rabbitmq_adapter'
4
4
  describe Basquiat::Adapters::RabbitMq::Connection do
5
5
  subject(:connection) { Basquiat::Adapters::RabbitMq::Connection }
6
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') }
7
+ let(:hosts) do
8
+ [ENV.fetch('BASQUIAT_RABBITMQ_1_PORT_5672_TCP_ADDR') { 'localhost' }]
14
9
  end
15
10
 
16
11
  it '#connected?' do
17
- conn = connection.new(servers: servers)
12
+ conn = connection.new(hosts: hosts)
18
13
  expect(conn.connected?).to be_falsey
19
14
  conn.start
20
15
  expect(conn.connected?).to_not be_truthy
@@ -23,22 +18,22 @@ describe Basquiat::Adapters::RabbitMq::Connection do
23
18
 
24
19
  context 'failover' do
25
20
  let(:failover) do
26
- { default_timeout: 0.2, max_retries: 2, threaded: false }
21
+ { default_timeout: 0.2, max_retries: 2, connection_timeout: 0.3 }
27
22
  end
28
23
 
29
- before(:each) { servers.unshift(host: 'localhost', port: 1234) }
24
+ before(:each) { hosts.unshift('172.168.0.124') }
30
25
 
31
26
  it 'tries a reconnection after a few seconds' do
32
- conn = connection.new(servers: [host: 'localhost', port: 1234],
27
+ conn = connection.new(hosts: ['172.168.0.124'],
33
28
  failover: { default_timeout: 0.2, max_retries: 1 })
34
29
  expect { conn.start }.to raise_error(Bunny::TCPConnectionFailed)
35
30
  conn.close
36
31
  end
37
32
 
38
33
  it 'uses another server after all retries on a single one' do
39
- conn = connection.new(servers: servers, failover: failover)
34
+ conn = connection.new(hosts: hosts, failover: failover)
40
35
  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 }}"
36
+ expect(conn.host).to match "#{ENV.fetch('BASQUIAT_RABBITMQ_1_PORT_5672_TCP_ADDR') { 'localhost' }}"
42
37
  conn.close
43
38
  end
44
39
  end
@@ -18,8 +18,15 @@ describe Basquiat::Adapters::RabbitMq::Events do
18
18
 
19
19
  context 'wildcard keys' do
20
20
  let(:proc) { -> { 'Hello from the lambda! o/' } }
21
+
21
22
  describe '*' do
22
23
  let(:words) { %w(awesome lame dumb cool) }
24
+
25
+ it 'event.* does not match event_some_word' do
26
+ events['event.*'] = proc
27
+ expect { events['event_some_word'] }.to raise_error KeyError
28
+ end
29
+
23
30
  context 'matches any ONE word' do
24
31
  it 'at the end' do
25
32
  events['some.event.*'] = proc
@@ -70,7 +77,7 @@ describe Basquiat::Adapters::RabbitMq::Events do
70
77
  %w(some.cool.event cool.event).each do |event|
71
78
  expect(events[event]).to eq(proc)
72
79
  end
73
- expect { events['event']}.to raise_error KeyError
80
+ expect { events['event'] }.to raise_error KeyError
74
81
  end
75
82
  end
76
83
  end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+ require 'basquiat/adapters/rabbitmq_adapter'
3
+
4
+ describe Basquiat::Adapters::RabbitMq::AutoAcknowledge do
5
+ let(:adapter) { Basquiat::Adapters::RabbitMq.new }
6
+ let(:base_options) do
7
+ { connection: { hosts: [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
+ requeue: { enabled: false } }
11
+ end
12
+
13
+ before(:each) do
14
+ adapter.adapter_options(base_options)
15
+ end
16
+
17
+ after(:each) { remove_queues_and_exchanges(adapter) }
18
+
19
+ it 'set manual_ack to false' do
20
+ # Setup the strategy
21
+ adapter.strategy
22
+ expect(adapter.send(:options)[:consumer][:manual_ack]).to be_falsey
23
+ end
24
+ end
@@ -4,8 +4,8 @@ require 'basquiat/adapters/rabbitmq_adapter'
4
4
  describe 'Requeue Strategies' do
5
5
  let(:adapter) { Basquiat::Adapters::RabbitMq.new }
6
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 } }],
7
+ { connection: { hosts: [ENV.fetch('BASQUIAT_RABBITMQ_1_PORT_5672_TCP_ADDR') { 'localhost' }],
8
+ port: ENV.fetch('BASQUIAT_RABBITMQ_1_PORT_5672_TCP_PORT') { 5672 } },
9
9
  publisher: { persistent: true } }
10
10
  end
11
11
 
@@ -18,7 +18,7 @@ describe 'Requeue Strategies' do
18
18
  adapter.listen(block: false)
19
19
 
20
20
  adapter.publish('some.event', data: 'stupid message')
21
- sleep 0.7 # Wait for the listening thread.
21
+ sleep 0.5 # Wait for the listening thread.
22
22
 
23
23
  expect(adapter.session.queue.message_count).to eq(0)
24
24
  expect(adapter.session.queue).to_not have_unacked_messages
@@ -29,7 +29,7 @@ describe 'Requeue Strategies' do
29
29
  adapter.listen(block: false)
30
30
 
31
31
  adapter.publish('some.event', data: 'stupid message')
32
- sleep 0.7 # Wait for the listening thread.
32
+ sleep 0.5 # Wait for the listening thread.
33
33
 
34
34
  expect(adapter.session.queue.message_count).to eq(0)
35
35
  expect(adapter.session.queue).to_not have_unacked_messages
@@ -4,28 +4,26 @@ require 'basquiat/adapters/rabbitmq_adapter'
4
4
  describe Basquiat::Adapters::RabbitMq::DeadLettering do
5
5
  let(:adapter) { Basquiat::Adapters::RabbitMq.new }
6
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 } }],
7
+ { connection: { hosts: [ENV.fetch('BASQUIAT_RABBITMQ_1_PORT_5672_TCP_ADDR') { 'localhost' }],
8
+ port: ENV.fetch('BASQUIAT_RABBITMQ_1_PORT_5672_TCP_PORT') { 5672 } },
9
9
  publisher: { persistent: true } }
10
10
  end
11
11
 
12
12
  before(:each) do
13
13
  adapter.adapter_options(base_options)
14
- adapter.class.register_strategy :dlx, Basquiat::Adapters::RabbitMq::DeadLettering
14
+ adapter.adapter_options(requeue: { enabled: true, strategy: 'dead_lettering' })
15
15
  end
16
16
 
17
17
  after(:each) { remove_queues_and_exchanges(adapter) }
18
18
 
19
19
  it 'creates the dead letter exchange' do
20
- adapter.adapter_options(requeue: { enabled: true, strategy: 'dlx' })
21
20
  adapter.strategy # initialize the strategy
22
21
  channel = adapter.session.channel
23
- expect(channel.exchanges.keys).to contain_exactly('my.test_exchange', 'basquiat.dlx')
22
+ expect(channel.exchanges.keys).to contain_exactly('basquiat.exchange', 'basquiat.dlx')
24
23
  end
25
24
 
26
25
  it 'creates and binds a dead letter queue' do
27
26
  # Initialize the strategy since we won't be listening to anything
28
- adapter.adapter_options(requeue: { enabled: true, strategy: 'dlx' })
29
27
  session = adapter.session
30
28
  adapter.strategy
31
29
 
@@ -33,7 +31,7 @@ describe Basquiat::Adapters::RabbitMq::DeadLettering do
33
31
  channel = session.channel
34
32
  expect(channel.queues.keys).to include('basquiat.dlq')
35
33
  expect(channel.queues['basquiat.dlq'].arguments)
36
- .to match(hash_including('x-dead-letter-exchange' => session.exchange.name, 'x-message-ttl' => 1000))
34
+ .to match(hash_including('x-dead-letter-exchange' => session.exchange.name, 'x-message-ttl' => 1000))
37
35
  expect(session.queue.arguments).to match(hash_including('x-dead-letter-exchange' => 'basquiat.dlx'))
38
36
 
39
37
  expect do
@@ -42,9 +40,8 @@ describe Basquiat::Adapters::RabbitMq::DeadLettering do
42
40
  end.to change { channel.queues['basquiat.dlq'].message_count }.by(1)
43
41
  end
44
42
 
45
- context 'if it was the queue that unacked the message then' do
43
+ context 'nacked the message from' do
46
44
  before(:each) do
47
- adapter.adapter_options(requeue: { enabled: true, strategy: 'dlx' })
48
45
  session = adapter.session
49
46
  adapter.strategy # initialize strategy
50
47
 
@@ -56,12 +53,13 @@ describe Basquiat::Adapters::RabbitMq::DeadLettering do
56
53
  end
57
54
  end
58
55
 
59
- it 'process the message' do
56
+ it 'this queue then process the message' do
60
57
  sample = 0
61
- adapter.subscribe_to('sample.message', ->(msg) do
62
- sample += 1
63
- sample == 3 ? msg.ack : msg.unack
64
- end)
58
+ adapter.subscribe_to('sample.message',
59
+ lambda do |msg|
60
+ sample += 1
61
+ sample == 3 ? msg.ack : msg.nack
62
+ end)
65
63
 
66
64
  adapter.listen(block: false)
67
65
  adapter.publish('sample.message', key: 'message')
@@ -70,24 +68,21 @@ describe Basquiat::Adapters::RabbitMq::DeadLettering do
70
68
  expect(sample).to eq(3)
71
69
  end
72
70
 
73
- it 'else drop the message' do
71
+ it 'another queue then drop the message' do
74
72
  ack_count = 0
75
73
  sample = 0
76
74
 
77
75
  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)
76
+ other.adapter_options(base_options.merge(queue:
77
+ { name: 'other_queue' },
78
+ requeue: { enabled: true, strategy: 'dead_lettering', ttl: 5 }))
79
+ other.subscribe_to('sample.message', ->(_msg) { ack_count += 1 })
80
+
81
+ adapter.subscribe_to('sample.message',
82
+ lambda do |msg|
83
+ sample += 1
84
+ sample == 3 ? msg.ack : msg.nack
85
+ end)
91
86
 
92
87
  other.listen(block: false)
93
88
  adapter.listen(block: false)
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+ require 'basquiat/adapters/rabbitmq_adapter'
3
+
4
+ describe Basquiat::Adapters::RabbitMq::DelayedDelivery do
5
+ let(:adapter) { Basquiat::Adapters::RabbitMq.new }
6
+ let(:base_options) do
7
+ { connection: { hosts: [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 :ddl, Basquiat::Adapters::RabbitMq::DelayedDelivery
15
+ adapter.adapter_options(requeue: { enabled: true, strategy: 'ddl' })
16
+ adapter.strategy # initialize the strategy
17
+ end
18
+
19
+ after(:each) { remove_queues_and_exchanges(adapter) }
20
+
21
+ it 'creates the dead letter exchange' do
22
+ channel = adapter.session.channel
23
+ expect(channel.exchanges.keys).to contain_exactly('basquiat.exchange', 'basquiat.dlx')
24
+ end
25
+
26
+ it 'creates the timeout queues' do
27
+ channel = adapter.session.channel
28
+ expect(channel.queues.keys).to contain_exactly('basquiat.ddlq_1', 'basquiat.ddlq_2', 'basquiat.ddlq_4',
29
+ 'basquiat.ddlq_8', 'basquiat.ddlq_16', 'basquiat.queue',
30
+ 'basquiat.ddlq_rejected')
31
+ end
32
+
33
+ it 'set the message ttl and dead letter exchange for the delayed queues' do
34
+ session = adapter.session
35
+ channel = session.channel
36
+ expect(channel.queues['basquiat.ddlq_1'].arguments)
37
+ .to match(hash_including('x-dead-letter-exchange' => session.exchange.name, 'x-message-ttl' => 1_000))
38
+
39
+ expect(channel.queues['basquiat.ddlq_8'].arguments['x-message-ttl']).to eq(8_000)
40
+ end
41
+
42
+ it 'binds the delayed queues' do
43
+ session = adapter.session
44
+ channel = session.channel
45
+ expect do
46
+ channel.exchanges['basquiat.dlx'].publish({ data: 'some message' }.to_json, routing_key: '1000.some.event')
47
+ sleep 0.1
48
+ end.to change { channel.queues['basquiat.ddlq_1'].message_count }.by(1)
49
+ end
50
+
51
+ it 'associates the event *.queue_name.event.name with event.name' do
52
+ message = ''
53
+ session = adapter.session
54
+ adapter.subscribe_to('some.event', ->(msg) { message = msg[:data].upcase })
55
+
56
+ adapter.listen(block: false)
57
+ session.publish('1000.basquiat.queue.some.event', data: 'some message')
58
+ sleep 0.5
59
+
60
+ expect(message).to eq('SOME MESSAGE')
61
+ end
62
+
63
+ context 'when a message is requeued' do
64
+ it 'is republished with the appropriate routing key' do
65
+ session = adapter.session
66
+ adapter.subscribe_to('some.event', ->(msg) { msg.requeue })
67
+ adapter.listen(block: false)
68
+
69
+ expect do
70
+ session.publish('some.event', data: 'some message')
71
+ sleep 0.3
72
+ end.to change { session.channel.queues['basquiat.ddlq_1'].message_count }.by(1)
73
+ end
74
+
75
+ it 'goes to the reject queue if a requeue would exceed the maximum timeout' do
76
+ session = adapter.session
77
+ adapter.subscribe_to('some.event', ->(msg) { msg.requeue })
78
+ adapter.listen(block: false)
79
+
80
+ expect do
81
+ session.publish('32000.basquiat.queue.some.event', data: 'some message')
82
+ sleep 0.3
83
+ end.to change { session.channel.queues['basquiat.ddlq_rejected'].message_count }.by(1)
84
+ end
85
+
86
+ it 'after it expires it is reprocessed by the right queue' do
87
+ analysed = 0
88
+ session = adapter.session
89
+ adapter.subscribe_to('some.event',
90
+ lambda do |msg|
91
+ if analysed == 1
92
+ msg.ack
93
+ else
94
+ analysed += 1
95
+ msg.requeue
96
+ end
97
+ end)
98
+ adapter.listen(block: false)
99
+ session.publish('some.event', data: 'some message')
100
+ sleep 1.3
101
+ expect(analysed).to eq(1)
102
+ expect(session.queue).to_not have_unacked_messages
103
+ end
104
+ end
105
+ end