rt-tackle 0.8.1 → 0.9
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/README.md +84 -1
- data/docs/consumer.png +0 -0
- data/lib/tackle.rb +9 -0
- data/lib/tackle/consumer.rb +71 -0
- data/lib/tackle/consumer/connection.rb +43 -0
- data/lib/tackle/consumer/dead_queue.rb +27 -0
- data/lib/tackle/consumer/delay_queue.rb +42 -0
- data/lib/tackle/consumer/exchange.rb +34 -0
- data/lib/tackle/consumer/main_queue.rb +42 -0
- data/lib/tackle/consumer/message.rb +52 -0
- data/lib/tackle/consumer/params.rb +31 -0
- data/lib/tackle/consumer/queue.rb +26 -0
- data/lib/tackle/version.rb +1 -1
- metadata +12 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b51e7b8dd21855f3c3a88d9da4be12c54a715d69
|
4
|
+
data.tar.gz: b4efab4ce923c03a8891099568192400ca085db4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 37f17e2b31c9b00c927dd3611ddc24b95b82e348f8ae33534b29df4bdc275507cc63b04df991ba26b42ae13f58f366e00e4771c06d8b3cc684b92e5d86623c69
|
7
|
+
data.tar.gz: 3e6743290efe3377fe2842c9930ffe86b5322e3ac7247d24860560cb5efaed2914936bdfb32c603ca888c94107a989e4d673d61def947ee95b94c3112711dc92
|
data/README.md
CHANGED
@@ -44,7 +44,90 @@ options = {
|
|
44
44
|
Tackle.publish("Hello World!", options)
|
45
45
|
```
|
46
46
|
|
47
|
-
###
|
47
|
+
### Consume messages
|
48
|
+
|
49
|
+
Tackle enables you to connect to an AMQP exchange and consume messages from it.
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
require "tackle"
|
53
|
+
|
54
|
+
options = {
|
55
|
+
:url => "amqp://localhost",
|
56
|
+
:exchange => "users",
|
57
|
+
:routing_key => "signed-up",
|
58
|
+
:service => "user-mailer",
|
59
|
+
:exception_handler => lambda { |ex, consumer| puts ex.message }
|
60
|
+
}
|
61
|
+
|
62
|
+
Tackle.consume(options) do |message|
|
63
|
+
puts message
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
The above code snippet creates the following AMQP resources:
|
68
|
+
|
69
|
+
1. A dedicated exchange for your service, in this example `user-mailer.signed-up`
|
70
|
+
2. Connects your dedicated `user-mailer.signed-up` exchange to the remote
|
71
|
+
exchange from which you want to consume messages, in this example `users`
|
72
|
+
exchange
|
73
|
+
3. Creates an AMQP queue `user-mailer.signed-up` and connects it to your local
|
74
|
+
exchange
|
75
|
+
4. Creates a delay queue `user-mailer.signed-up.delay`. If your service raises
|
76
|
+
an exception while processing an incoming message, tackle will put it in this
|
77
|
+
this queue, wait for a while, and then republish to the
|
78
|
+
`user-mailer.signed-up` exchange.
|
79
|
+
5. Creates a dead queue `user-mailer.signed-up.dead`. After several retries
|
80
|
+
where your service can't consume the message, tackle will store them in a
|
81
|
+
dedicated dead queue. You can consume this messages manually.
|
82
|
+
|
83
|
+

|
84
|
+
|
85
|
+
You can pass additional configuration to tackle in order to control the number
|
86
|
+
of retries, and the delay between each retry.
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
require "tackle"
|
90
|
+
|
91
|
+
options = {
|
92
|
+
:url => "amqp://localhost",
|
93
|
+
:exchange => "users",
|
94
|
+
:routing_key => "signed-up"
|
95
|
+
:service => "user-mailer",
|
96
|
+
:retry_limit => 8,
|
97
|
+
:retry_delay => 30,
|
98
|
+
:exception_handler => lambda { |ex, consumer| puts ex.message }
|
99
|
+
}
|
100
|
+
|
101
|
+
Tackle.consume(options) do |message|
|
102
|
+
puts message
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
By default, tackle logs helpful information to the `STDOUT`. To redirect these
|
107
|
+
messages to a file, pass a dedicated logger to tackle.
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
require "tackle"
|
111
|
+
|
112
|
+
options = {
|
113
|
+
:url => "amqp://localhost",
|
114
|
+
:exchange => "users",
|
115
|
+
:routing_key => "signed-up"
|
116
|
+
:service => "user-mailer",
|
117
|
+
:retry_limit => 8,
|
118
|
+
:retry_delay => 30,
|
119
|
+
:logger => Logger.new("consumer.log"),
|
120
|
+
:exception_handler => lambda { |ex, consumer| puts ex.message }
|
121
|
+
}
|
122
|
+
|
123
|
+
Tackle.consume(options) do |message|
|
124
|
+
puts message
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
### [DEPRECATED] Subscribe to an exchange
|
129
|
+
|
130
|
+
**Deprecation notice:** For newer projects please use `Tackle.consume`.
|
48
131
|
|
49
132
|
To consume messages from an exchange, do the following:
|
50
133
|
|
data/docs/consumer.png
ADDED
Binary file
|
data/lib/tackle.rb
CHANGED
@@ -3,7 +3,16 @@ require "tackle/version"
|
|
3
3
|
module Tackle
|
4
4
|
require "tackle/worker"
|
5
5
|
require "tackle/publisher"
|
6
|
+
require "tackle/consumer"
|
6
7
|
|
8
|
+
def self.consume(params = {}, &block)
|
9
|
+
params = Tackle::Consumer::Params.new(params)
|
10
|
+
consumer = Tackle::Consumer.new(params)
|
11
|
+
|
12
|
+
consumer.subscribe(&block)
|
13
|
+
end
|
14
|
+
|
15
|
+
# deprecated
|
7
16
|
def self.subscribe(options = {}, &block)
|
8
17
|
# required
|
9
18
|
exchange_name = options.fetch(:exchange)
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Tackle
|
2
|
+
require_relative "consumer/params"
|
3
|
+
require_relative "consumer/connection"
|
4
|
+
require_relative "consumer/message"
|
5
|
+
require_relative "consumer/exchange"
|
6
|
+
|
7
|
+
require_relative "consumer/queue"
|
8
|
+
require_relative "consumer/main_queue"
|
9
|
+
require_relative "consumer/delay_queue"
|
10
|
+
require_relative "consumer/dead_queue"
|
11
|
+
|
12
|
+
class Consumer
|
13
|
+
|
14
|
+
def initialize(params)
|
15
|
+
@params = params
|
16
|
+
@logger = @params.logger
|
17
|
+
|
18
|
+
setup_rabbit_connections
|
19
|
+
end
|
20
|
+
|
21
|
+
def setup_rabbit_connections
|
22
|
+
@connection = Connection.new(@params.amqp_url, @params.exception_handler, @logger)
|
23
|
+
|
24
|
+
@exchange = Exchange.new(@params.service, @params.routing_key, @connection, @logger)
|
25
|
+
@main_queue = MainQueue.new(@exchange, @connection, @logger)
|
26
|
+
@delay_queue = DelayQueue.new(@params.retry_delay, @exchange, @connection, @logger)
|
27
|
+
@dead_queue = DeadQueue.new(@exchange, @connection, @logger)
|
28
|
+
|
29
|
+
@exchange.bind_to_exchange(@params.exchange)
|
30
|
+
end
|
31
|
+
|
32
|
+
def subscribe(&block)
|
33
|
+
@logger.info "Subscribing to the main queue '#{@main_queue.name}'"
|
34
|
+
|
35
|
+
@main_queue.subscribe { |message| process_message(message, &block) }
|
36
|
+
rescue Interrupt => _
|
37
|
+
@connection.close
|
38
|
+
rescue StandardError => ex
|
39
|
+
@logger.error("An exception occured message='#{ex.message}'")
|
40
|
+
|
41
|
+
raise ex
|
42
|
+
end
|
43
|
+
|
44
|
+
def process_message(message, &block)
|
45
|
+
message.log_info "Calling message processor"
|
46
|
+
|
47
|
+
block.call(message.payload)
|
48
|
+
|
49
|
+
message.ack
|
50
|
+
rescue Exception => ex
|
51
|
+
message.log_error "Failed to process message. Received exception '#{ex}'"
|
52
|
+
|
53
|
+
redeliver_message(message)
|
54
|
+
|
55
|
+
message.nack
|
56
|
+
|
57
|
+
raise ex
|
58
|
+
end
|
59
|
+
|
60
|
+
def redeliver_message(message)
|
61
|
+
message.log_error "Retry count #{message.retry_count}/#{@params.retry_limit}"
|
62
|
+
|
63
|
+
if message.retry_count < @params.retry_limit
|
64
|
+
@delay_queue.publish(message)
|
65
|
+
else
|
66
|
+
@dead_queue.publish(message)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Tackle
|
2
|
+
class Consumer
|
3
|
+
class Connection
|
4
|
+
attr_reader :channel
|
5
|
+
|
6
|
+
def initialize(amqp_url, exception_handler, logger)
|
7
|
+
@amqp_url = amqp_url
|
8
|
+
@exception_handler = exception_handler
|
9
|
+
@logger = logger
|
10
|
+
|
11
|
+
connect
|
12
|
+
end
|
13
|
+
|
14
|
+
def connect
|
15
|
+
@logger.info("Connecting to RabbitMQ")
|
16
|
+
|
17
|
+
@connection = Bunny.new(@amqp_url)
|
18
|
+
@connection.start
|
19
|
+
|
20
|
+
@logger.info("Connected to RabbitMQ")
|
21
|
+
|
22
|
+
@channel = @connection.create_channel
|
23
|
+
@channel.prefetch(1)
|
24
|
+
@channel.on_uncaught_exception(&@exception_handler)
|
25
|
+
|
26
|
+
@logger.info("Connected to channel")
|
27
|
+
rescue StandardError => ex
|
28
|
+
@logger.error("Error while connecting to RabbitMQ message='#{ex}'")
|
29
|
+
|
30
|
+
raise ex
|
31
|
+
end
|
32
|
+
|
33
|
+
def close
|
34
|
+
@channel.close
|
35
|
+
@logger.info("Closed channel")
|
36
|
+
|
37
|
+
@connection.close
|
38
|
+
@logger.info("Closed connection to RabbitMQ")
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Tackle
|
2
|
+
class Consumer
|
3
|
+
class DeadQueue < Tackle::Consumer::Queue
|
4
|
+
|
5
|
+
def initialize(exchange, connection, logger)
|
6
|
+
name = "#{exchange.name}.dead"
|
7
|
+
|
8
|
+
options = { :durable => true }
|
9
|
+
|
10
|
+
super(name, options, connection, logger)
|
11
|
+
end
|
12
|
+
|
13
|
+
def publish(message)
|
14
|
+
message.log_error "Pushing message to '#{name}'"
|
15
|
+
|
16
|
+
@amqp_queue.publish(message.payload)
|
17
|
+
|
18
|
+
message.log_error "Message pushed to '#{name}'"
|
19
|
+
rescue StandardError => ex
|
20
|
+
message.log_error "Error while pushing message exception='#{ex}'"
|
21
|
+
|
22
|
+
raise ex
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Tackle
|
2
|
+
class Consumer
|
3
|
+
class DelayQueue < Tackle::Consumer::Queue
|
4
|
+
|
5
|
+
def initialize(retry_delay, exchange, connection, logger)
|
6
|
+
@retry_delay = retry_delay
|
7
|
+
|
8
|
+
name = "#{exchange.name}.delay.#{retry_delay}"
|
9
|
+
|
10
|
+
options = {
|
11
|
+
:durable => true,
|
12
|
+
:arguments => {
|
13
|
+
"x-dead-letter-exchange" => exchange.name,
|
14
|
+
"x-dead-letter-routing-key" => exchange.routing_key,
|
15
|
+
"x-message-ttl" => retry_delay * 1000 # miliseconds
|
16
|
+
}
|
17
|
+
}
|
18
|
+
|
19
|
+
super(name, options, connection, logger)
|
20
|
+
end
|
21
|
+
|
22
|
+
def publish(message)
|
23
|
+
message.log_error "Pushing message to delay queue delay='#{@retry_delay}'"
|
24
|
+
|
25
|
+
headers = {
|
26
|
+
:headers => {
|
27
|
+
:retry_count => message.retry_count + 1
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
@amqp_queue.publish(message.payload, headers)
|
32
|
+
|
33
|
+
message.log_error "Message pushed to delay queue"
|
34
|
+
rescue StandardError => ex
|
35
|
+
message.log_error "Error while pushing message to delay queue exception='#{ex}'"
|
36
|
+
|
37
|
+
raise ex
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Tackle
|
2
|
+
class Consumer
|
3
|
+
class Exchange
|
4
|
+
|
5
|
+
attr_reader :routing_key
|
6
|
+
|
7
|
+
def initialize(service_name, routing_key, connection, logger)
|
8
|
+
@service_name = service_name
|
9
|
+
@routing_key = routing_key
|
10
|
+
@connection = connection
|
11
|
+
@logger = logger
|
12
|
+
|
13
|
+
@logger.info("Creating local exchange '#{name}'")
|
14
|
+
@amqp_exchange = @connection.channel.direct(name, :durable => true)
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
"#{@service_name}.#{@routing_key}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def bind_to_exchange(remote_exchange_name)
|
22
|
+
@logger.info("Creating remote exchange '#{remote_exchange_name}'")
|
23
|
+
@connection.channel.direct(remote_exchange_name, :durable => true)
|
24
|
+
|
25
|
+
@logger.info("Binding exchange '#{name}' to exchange '#{remote_exchange_name}'")
|
26
|
+
@amqp_exchange.bind(remote_exchange_name, :routing_key => routing_key)
|
27
|
+
rescue Exception => ex
|
28
|
+
@logger.error "Binding to remote exchange failed #{ex}"
|
29
|
+
raise ex
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Tackle
|
2
|
+
class Consumer
|
3
|
+
class MainQueue < Tackle::Consumer::Queue
|
4
|
+
|
5
|
+
def initialize(exchange, connection, logger)
|
6
|
+
@exchange = exchange
|
7
|
+
|
8
|
+
name = @exchange.name
|
9
|
+
options = { :durable => true }
|
10
|
+
|
11
|
+
super(name, options, connection, logger)
|
12
|
+
|
13
|
+
bind_to_exchange
|
14
|
+
end
|
15
|
+
|
16
|
+
def bind_to_exchange
|
17
|
+
@logger.info("Binding queue '#{name}' to exchange '#{@exchange.name}' with routing_key '#{@exchange.routing_key}'")
|
18
|
+
|
19
|
+
@amqp_queue.bind(@exchange, :routing_key => @exchange.routing_key)
|
20
|
+
rescue Exception => ex
|
21
|
+
@logger.error "Failed to bind queue to exchange '#{ex}'"
|
22
|
+
raise ex
|
23
|
+
end
|
24
|
+
|
25
|
+
def subscribe(&block)
|
26
|
+
options = { :manual_ack => true, :block => true }
|
27
|
+
|
28
|
+
@amqp_queue.subscribe(options) do |delivery_info, properties, payload|
|
29
|
+
message = Message.new(@connection,
|
30
|
+
@logger,
|
31
|
+
delivery_info,
|
32
|
+
properties,
|
33
|
+
payload)
|
34
|
+
|
35
|
+
block.call(message)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Tackle
|
2
|
+
class Consumer
|
3
|
+
class Message
|
4
|
+
|
5
|
+
attr_reader :payload
|
6
|
+
attr_reader :properties
|
7
|
+
attr_reader :payload
|
8
|
+
|
9
|
+
def initialize(connection, logger, delivery_info, properties, payload)
|
10
|
+
@connection = connection
|
11
|
+
@logger = logger
|
12
|
+
|
13
|
+
@delivery_info = delivery_info
|
14
|
+
@properties = properties
|
15
|
+
@payload = payload
|
16
|
+
end
|
17
|
+
|
18
|
+
def ack
|
19
|
+
log_info "Sending positive acknowledgement to source queue"
|
20
|
+
@connection.channel.ack(delivery_tag)
|
21
|
+
log_info "Positive acknowledgement sent"
|
22
|
+
end
|
23
|
+
|
24
|
+
def nack
|
25
|
+
log_error "Sending negative acknowledgement to source queue"
|
26
|
+
@connection.channel.nack(delivery_tag)
|
27
|
+
log_error "Negative acknowledgement sent"
|
28
|
+
end
|
29
|
+
|
30
|
+
def retry_count
|
31
|
+
if @properties.headers && @properties.headers["retry_count"]
|
32
|
+
@properties.headers["retry_count"]
|
33
|
+
else
|
34
|
+
0
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def delivery_tag
|
39
|
+
@delivery_info.delivery_tag
|
40
|
+
end
|
41
|
+
|
42
|
+
def log_info(message)
|
43
|
+
@logger.info("[delivery_tag=#{delivery_tag}] #{message}")
|
44
|
+
end
|
45
|
+
|
46
|
+
def log_error(message)
|
47
|
+
@logger.error("[delivery_tag=#{delivery_tag}] #{message}")
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Tackle
|
2
|
+
class Consumer
|
3
|
+
class Params
|
4
|
+
|
5
|
+
attr_reader :amqp_url
|
6
|
+
attr_reader :exchange
|
7
|
+
attr_reader :routing_key
|
8
|
+
attr_reader :service
|
9
|
+
attr_reader :retry_limit
|
10
|
+
attr_reader :retry_delay
|
11
|
+
attr_reader :logger
|
12
|
+
attr_reader :exception_handler
|
13
|
+
|
14
|
+
def initialize(params = {})
|
15
|
+
# required
|
16
|
+
@amqp_url = params.fetch(:url)
|
17
|
+
@exchange = params.fetch(:exchange)
|
18
|
+
@routing_key = params.fetch(:routing_key)
|
19
|
+
@service = params.fetch(:service)
|
20
|
+
|
21
|
+
# optional
|
22
|
+
@retry_limit = params[:retry_limit] || 8
|
23
|
+
@retry_delay = params[:retry_delay] || 30
|
24
|
+
@logger = params[:logger] || Logger.new(STDOUT)
|
25
|
+
|
26
|
+
@exception_handler = params[:exception_handler]
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Tackle
|
2
|
+
class Consumer
|
3
|
+
class Queue
|
4
|
+
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
def initialize(name, options, connection, logger)
|
8
|
+
@name = name
|
9
|
+
@connection = connection
|
10
|
+
@logger = logger
|
11
|
+
@options = options
|
12
|
+
|
13
|
+
@amqp_queue = create_amqp_queue
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_amqp_queue
|
17
|
+
@logger.info("Creating queue '#{@name}'")
|
18
|
+
@connection.channel.queue(@name, @options)
|
19
|
+
rescue Exception => ex
|
20
|
+
@logger.error "Failed to create queue '#{ex}'"
|
21
|
+
raise ex
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/tackle/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rt-tackle
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: '0.9'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rendered Text
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-09-
|
11
|
+
date: 2016-09-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bunny
|
@@ -94,7 +94,17 @@ files:
|
|
94
94
|
- Rakefile
|
95
95
|
- bin/console
|
96
96
|
- bin/setup
|
97
|
+
- docs/consumer.png
|
97
98
|
- lib/tackle.rb
|
99
|
+
- lib/tackle/consumer.rb
|
100
|
+
- lib/tackle/consumer/connection.rb
|
101
|
+
- lib/tackle/consumer/dead_queue.rb
|
102
|
+
- lib/tackle/consumer/delay_queue.rb
|
103
|
+
- lib/tackle/consumer/exchange.rb
|
104
|
+
- lib/tackle/consumer/main_queue.rb
|
105
|
+
- lib/tackle/consumer/message.rb
|
106
|
+
- lib/tackle/consumer/params.rb
|
107
|
+
- lib/tackle/consumer/queue.rb
|
98
108
|
- lib/tackle/delayed_retry.rb
|
99
109
|
- lib/tackle/publisher.rb
|
100
110
|
- lib/tackle/rabbit.rb
|