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.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.metrics +5 -0
- data/.reek +3 -0
- data/.rspec +1 -0
- data/.rubocop.yml +2 -1
- data/.ruby-version +1 -1
- data/Guardfile +5 -4
- data/README.md +10 -8
- data/basquiat.gemspec +10 -8
- data/basquiat_docker.sh +35 -0
- data/docker-compose.yml +5 -1
- data/docker/Dockerfile +2 -3
- data/docker/guard_start.sh +3 -0
- data/lib/basquiat.rb +5 -0
- data/lib/basquiat/adapters/base_adapter.rb +21 -11
- data/lib/basquiat/adapters/base_message.rb +29 -0
- data/lib/basquiat/adapters/rabbitmq/configuration.rb +52 -0
- data/lib/basquiat/adapters/rabbitmq/connection.rb +89 -0
- data/lib/basquiat/adapters/rabbitmq/events.rb +49 -0
- data/lib/basquiat/adapters/rabbitmq/message.rb +33 -0
- data/lib/basquiat/adapters/rabbitmq/requeue_strategies.rb +3 -0
- data/lib/basquiat/adapters/rabbitmq/requeue_strategies/base_strategy.rb +33 -0
- data/lib/basquiat/adapters/rabbitmq/requeue_strategies/basic_acknowledge.rb +12 -0
- data/lib/basquiat/adapters/rabbitmq/requeue_strategies/dead_lettering.rb +58 -0
- data/lib/basquiat/adapters/rabbitmq/requeue_strategies/delayed_delivery.rb +27 -0
- data/lib/basquiat/adapters/rabbitmq/session.rb +47 -0
- data/lib/basquiat/adapters/rabbitmq_adapter.rb +39 -95
- data/lib/basquiat/adapters/test_adapter.rb +4 -3
- data/lib/basquiat/errors.rb +2 -0
- data/lib/basquiat/errors/strategy_not_registered.rb +14 -0
- data/lib/basquiat/errors/subclass_responsibility.rb +9 -0
- data/lib/basquiat/interfaces/base.rb +0 -1
- data/lib/basquiat/support/configuration.rb +4 -4
- data/lib/basquiat/support/hash_refinements.rb +2 -1
- data/lib/basquiat/version.rb +1 -1
- data/spec/lib/adapters/base_adapter_spec.rb +24 -6
- data/spec/lib/adapters/base_message_spec.rb +16 -0
- data/spec/lib/adapters/rabbitmq/configuration_spec.rb +47 -0
- data/spec/lib/adapters/rabbitmq/connection_spec.rb +45 -0
- data/spec/lib/adapters/rabbitmq/events_spec.rb +78 -0
- data/spec/lib/adapters/rabbitmq/message_spec.rb +26 -0
- data/spec/lib/adapters/rabbitmq/requeue_strategies/basic_acknowledge_spec.rb +38 -0
- data/spec/lib/adapters/rabbitmq/requeue_strategies/dead_lettering_spec.rb +102 -0
- data/spec/lib/adapters/rabbitmq_adapter_spec.rb +39 -49
- data/spec/lib/adapters/test_adapter_spec.rb +15 -19
- data/spec/lib/support/configuration_spec.rb +1 -1
- data/spec/lib/support/hash_refinements_spec.rb +8 -2
- data/spec/spec_helper.rb +8 -5
- data/spec/support/rabbitmq_queue_matchers.rb +53 -0
- data/spec/support/shared_examples/basquiat_adapter_shared_examples.rb +9 -20
- metadata +65 -6
- data/.travis.yml +0 -3
- 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,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,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
|
-
|
7
|
+
class RabbitMq < Basquiat::Adapters::Base
|
8
|
+
using Basquiat::HashRefinements
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
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
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
end
|
51
|
+
def reset_connection
|
52
|
+
connection.disconnect
|
53
|
+
@connection = nil
|
54
|
+
@session = nil
|
45
55
|
end
|
46
56
|
|
47
|
-
|
48
|
-
current_server_uri
|
49
|
-
end
|
57
|
+
alias_method :disconnect, :reset_connection
|
50
58
|
|
51
|
-
def
|
52
|
-
|
53
|
-
connection.close
|
54
|
-
reset_connection
|
59
|
+
def strategy
|
60
|
+
@strategy ||= @configuration.strategy.new(session)
|
55
61
|
end
|
56
62
|
|
57
|
-
def
|
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 ||=
|
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
|
-
|
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(
|
41
|
+
msg ? procs[event].call(Message.new(msg)) : nil
|
41
42
|
end
|
42
43
|
|
43
44
|
private
|