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