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,49 @@
1
+ require 'set'
2
+
3
+ module Basquiat
4
+ module Adapters
5
+ class RabbitMq
6
+ class Events
7
+ attr_reader :keys
8
+
9
+ def initialize
10
+ @keys = []
11
+ @exact = {}
12
+ @patterns = {}
13
+ end
14
+
15
+ def []=(key, value)
16
+ if key =~ /\*|\#/
17
+ set_pattern_key(key, value)
18
+ else
19
+ @exact[key] = value
20
+ end
21
+ @keys.push key
22
+ end
23
+
24
+ def [](key)
25
+ @exact.fetch(key) { simple_pattern_match(key) }
26
+ rescue KeyError
27
+ raise KeyError, "No event handler found for #{key}"
28
+ end
29
+
30
+ # event.for.the.win, event.for.everyone, event.for.*
31
+ private
32
+
33
+ def set_pattern_key(key, value)
34
+ key = if key =~ /\*/
35
+ /^#{key.gsub('*', '[^.]+')}$/
36
+ else
37
+ /^#{key.gsub(/\#/, '.*')}$/
38
+ end
39
+ @patterns[key] = value
40
+ end
41
+
42
+ def simple_pattern_match(key)
43
+ match = @patterns.keys.detect(nil) { |pattern| key =~ pattern }
44
+ @patterns.fetch match
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,33 @@
1
+ module Basquiat
2
+ module Adapters
3
+ class RabbitMq
4
+ class Message < Basquiat::Adapters::BaseMessage
5
+ attr_reader :delivery_info, :props
6
+ alias_method :di, :delivery_info
7
+
8
+ def initialize(message, delivery_info = {}, props = {})
9
+ super(message)
10
+ @delivery_info = delivery_info
11
+ @props = props
12
+ @action = :ack
13
+ end
14
+
15
+ def routing_key
16
+ delivery_info.routing_key
17
+ end
18
+
19
+ def delivery_tag
20
+ delivery_info.delivery_tag
21
+ end
22
+
23
+ def ack
24
+ @action = :ack
25
+ end
26
+
27
+ def unack
28
+ @action = :unack
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ require 'basquiat/adapters/rabbitmq/requeue_strategies/base_strategy'
2
+ require 'basquiat/adapters/rabbitmq/requeue_strategies/basic_acknowledge'
3
+ require 'basquiat/adapters/rabbitmq/requeue_strategies/dead_lettering'
@@ -0,0 +1,33 @@
1
+ module Basquiat
2
+ module Adapters
3
+ class RabbitMq
4
+ class BaseStrategy
5
+ class << self
6
+ def session_options
7
+ {}
8
+ end
9
+
10
+ def setup(options = {})
11
+ @options = options
12
+ end
13
+ end
14
+
15
+ def initialize(session)
16
+ @session = session
17
+ end
18
+
19
+ def run(_message)
20
+ fail Basquiat::Errors::SubclassResponsibility
21
+ end
22
+
23
+ def ack(delivery_tag)
24
+ @session.channel.ack(delivery_tag)
25
+ end
26
+
27
+ def unack(delivery_tag)
28
+ @session.channel.nack(delivery_tag, false)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,12 @@
1
+ module Basquiat
2
+ module Adapters
3
+ class RabbitMq
4
+ class BasicAcknowledge < BaseStrategy
5
+ def run(message)
6
+ yield
7
+ send(message.action, message.di.delivery_tag)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,58 @@
1
+ module Basquiat
2
+ module Adapters
3
+ class RabbitMq
4
+ class DeadLettering < BaseStrategy
5
+ class << self
6
+ attr_reader :options
7
+
8
+ def setup(opts)
9
+ @options = {
10
+ session: { queue: {
11
+ options: { 'x-dead-letter-exchange' => opts.fetch(:exchange, 'basquiat.dlx') }
12
+ } },
13
+ dlx: { ttl: opts.fetch(:ttl, 1_000) } }
14
+ end
15
+
16
+ def session_options
17
+ options.fetch :session
18
+ rescue KeyError
19
+ raise 'You have to setup the strategy first'
20
+ end
21
+ end
22
+
23
+ def initialize(session)
24
+ super
25
+ setup_dead_lettering
26
+ end
27
+
28
+ def run(message)
29
+ catch :skip_processing do
30
+ check_incoming_messages(message)
31
+ yield
32
+ end
33
+ public_send(message.action, message.delivery_tag)
34
+ end
35
+
36
+ private
37
+
38
+ def check_incoming_messages(message)
39
+ message.props.headers and
40
+ message.props.headers['x-death'][1]['queue'] != @session.queue.name and
41
+ throw(:skip_processing)
42
+ end
43
+
44
+ def options
45
+ self.class.options
46
+ end
47
+
48
+ def setup_dead_lettering
49
+ dlx = @session.channel.topic('basquiat.dlx')
50
+ queue = @session.channel.queue('basquiat.dlq',
51
+ arguments: { 'x-dead-letter-exchange' => @session.exchange.name,
52
+ 'x-message-ttl' => options[:dlx][:ttl] })
53
+ queue.bind(dlx, routing_key: '*.#')
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,27 @@
1
+ module Basquiat
2
+ module Adapters
3
+ class RabbitMq
4
+ module Strategies
5
+ class DelayedDeliveryWIP
6
+ def initialize(channel, message)
7
+ @channel = channel
8
+ @message = message
9
+ end
10
+
11
+ # Criar um exchange
12
+ # Criar o queue (ou redeclara-lo)
13
+ # O queue tem que ter um dlx para o exchange padrão
14
+ # Publicar a mensagem no exchange com um ttl igual ao anterior **2
15
+ # dar um unack caso o tempo estoure o maximo.
16
+ def message_handler
17
+ delay = message[:headers][0][:expiration]**2
18
+ exchange = channel.topic('basquiat.dd')
19
+ queue = channel.queue('delay', ttl: delay * 2)
20
+ queue.bind(exchange, 'original_queue.delay.message_name')
21
+ exchange.publish('original_queue.delay.message_name', message, ttl: delay, dlx: default_exchange)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,47 @@
1
+ module Basquiat
2
+ module Adapters
3
+ class RabbitMq
4
+ class Session
5
+ def initialize(connection, session_options = {})
6
+ @connection = connection
7
+ @options = session_options
8
+ end
9
+
10
+ def bind_queue(routing_key)
11
+ queue.bind(exchange, routing_key: routing_key)
12
+ end
13
+
14
+ def publish(routing_key, message, props = {})
15
+ channel.confirm_select if @options[:publisher][:confirm]
16
+ exchange.publish(Basquiat::Json.encode(message),
17
+ { routing_key: routing_key,
18
+ timestamp: Time.now.to_i }.merge(props))
19
+ end
20
+
21
+ def subscribe(lock, &_block)
22
+ queue.subscribe(block: lock, manual_ack: true) do |di, props, msg|
23
+ message = Basquiat::Adapters::RabbitMq::Message.new(msg, di, props)
24
+ yield message
25
+ end
26
+ end
27
+
28
+ def channel
29
+ @connection.start unless @connection.connected?
30
+ @channel ||= @connection.create_channel
31
+ end
32
+
33
+ def queue
34
+ @queue ||= channel.queue(@options[:queue][:name],
35
+ durable: true,
36
+ arguments: (@options[:queue][:options] || {}))
37
+ end
38
+
39
+ def exchange
40
+ @exchange ||= channel.topic(@options[:exchange][:name],
41
+ durable: true,
42
+ arguments: (@options[:exchange][:options] || {}))
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,129 +1,73 @@
1
1
  require 'bunny'
2
+ require 'delegate'
2
3
 
3
4
  module Basquiat
4
5
  module Adapters
5
6
  # The RabbitMQ adapter for Basquiat
6
- class RabbitMq
7
- include Basquiat::Adapters::Base
7
+ class RabbitMq < Basquiat::Adapters::Base
8
+ using Basquiat::HashRefinements
8
9
 
9
- def default_options
10
- { failover: { default_timeout: 5, max_retries: 5 },
11
- servers: [{ host: 'localhost', port: 5672 }],
12
- queue: { name: Basquiat.configuration.queue_name, options: { durable: true } },
13
- exchange: { name: Basquiat.configuration.exchange_name, options: { durable: true } },
14
- publisher: { confirm: true, persistent: false },
15
- auth: { user: 'guest', password: 'guest' } }
10
+
11
+ # Avoid superclass mismatch errors
12
+ require 'basquiat/adapters/rabbitmq/events'
13
+ require 'basquiat/adapters/rabbitmq/message'
14
+ require 'basquiat/adapters/rabbitmq/configuration'
15
+ require 'basquiat/adapters/rabbitmq/connection'
16
+ require 'basquiat/adapters/rabbitmq/session'
17
+ require 'basquiat/adapters/rabbitmq/requeue_strategies'
18
+
19
+ def initialize
20
+ super
21
+ @procs = Events.new
22
+ end
23
+
24
+ def base_options
25
+ @configuration ||= Configuration.new
26
+ @configuration.merge_user_options(Basquiat.configuration.adapter_options)
16
27
  end
17
28
 
18
29
  def subscribe_to(event_name, proc)
19
30
  procs[event_name] = proc
20
31
  end
21
32
 
22
- def publish(event, message, persistent: options[:publisher][:persistent])
23
- with_network_failure_handler do
24
- channel.confirm_select if options[:publisher][:confirm]
25
- exchange.publish(Basquiat::Json.encode(message), routing_key: event)
33
+ def publish(event, message, persistent: options[:publisher][:persistent], props: {})
34
+ connection.with_network_failure_handler do
35
+ session.publish(event, message, props)
26
36
  disconnect unless persistent
27
37
  end
28
38
  end
29
39
 
30
40
  def listen(block: true)
31
- with_network_failure_handler do
32
- procs.keys.each { |key| bind_queue(key) }
33
- queue.subscribe(block: block) do |di, _, msg|
34
- message = Basquiat::Json.decode(msg)
35
- procs[di.routing_key].call(message)
41
+ connection.with_network_failure_handler do
42
+ procs.keys.each { |key| session.bind_queue(key) }
43
+ session.subscribe(block) do |message|
44
+ strategy.run(message) do
45
+ procs[message.routing_key].call(message)
46
+ end
36
47
  end
37
48
  end
38
49
  end
39
50
 
40
- def connect
41
- with_network_failure_handler do
42
- connection.start
43
- current_server[:retries] = 0
44
- end
51
+ def reset_connection
52
+ connection.disconnect
53
+ @connection = nil
54
+ @session = nil
45
55
  end
46
56
 
47
- def connection_uri
48
- current_server_uri
49
- end
57
+ alias_method :disconnect, :reset_connection
50
58
 
51
- def disconnect
52
- connection.close_all_channels
53
- connection.close
54
- reset_connection
59
+ def strategy
60
+ @strategy ||= @configuration.strategy.new(session)
55
61
  end
56
62
 
57
- def connected?
58
- @connection
63
+ def session
64
+ @session ||= Session.new(connection, @configuration.session_options)
59
65
  end
60
66
 
61
67
  private
62
- def with_network_failure_handler
63
- yield if block_given?
64
- rescue Bunny::ConnectionForced, Bunny::TCPConnectionFailed, Bunny::NetworkFailure => error
65
- if current_server.fetch(:retries, 0) <= failover_opts[:max_retries]
66
- handle_network_failures
67
- retry
68
- else
69
- raise(error)
70
- end
71
- end
72
-
73
- def failover_opts
74
- options[:failover]
75
- end
76
-
77
- def bind_queue(event_name)
78
- queue.bind(exchange, routing_key: event_name)
79
- end
80
-
81
- def reset_connection
82
- @connection, @channel, @exchange, @queue = nil, nil, nil, nil
83
- end
84
-
85
- def rotate_servers
86
- return unless options[:servers].any? { |server| server.fetch(:retries, 0) < failover_opts[:max_retries] }
87
- options[:servers].rotate!
88
- end
89
-
90
- def handle_network_failures
91
- logger.warn "[WARN] Handling connection to #{current_server_uri}"
92
- retries = current_server.fetch(:retries, 0)
93
- current_server[:retries] = retries + 1
94
- if retries < failover_opts[:max_retries]
95
- logger.warn("[WARN] Connection failed retrying in #{failover_opts[:default_timeout]} seconds")
96
- sleep(failover_opts[:default_timeout])
97
- else
98
- rotate_servers
99
- end
100
- reset_connection
101
- end
102
68
 
103
69
  def connection
104
- @connection ||= Bunny.new(current_server_uri)
105
- end
106
-
107
- def channel
108
- connect
109
- @channel ||= connection.create_channel
110
- end
111
-
112
- def queue
113
- @queue ||= channel.queue(options[:queue][:name], options[:queue][:options])
114
- end
115
-
116
- def exchange
117
- @exchange ||= channel.topic(options[:exchange][:name], options[:exchange][:options])
118
- end
119
-
120
- def current_server
121
- options[:servers].first
122
- end
123
-
124
- def current_server_uri
125
- auth = current_server[:auth] || options[:auth]
126
- "amqp://#{auth[:user]}:#{auth[:password]}@#{current_server[:host]}:#{current_server[:port]}"
70
+ @connection ||= Connection.new(@configuration.connection_options)
127
71
  end
128
72
  end
129
73
  end
@@ -1,8 +1,9 @@
1
1
  module Basquiat
2
2
  module Adapters
3
3
  # An adapter to be used in testing
4
- class Test
5
- include Basquiat::Adapters::Base
4
+ class Test < Basquiat::Adapters::Base
5
+ class Message < BaseMessage
6
+ end
6
7
 
7
8
  class << self
8
9
  def events
@@ -37,7 +38,7 @@ module Basquiat
37
38
  def listen(*)
38
39
  event = subscribed_event
39
40
  msg = self.class.events[event].shift
40
- msg ? procs[event].call(Basquiat::Json.decode(msg)) : nil
41
+ msg ? procs[event].call(Message.new(msg)) : nil
41
42
  end
42
43
 
43
44
  private