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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: beb7fcc50f104dade384ab9e3090d1edc68c8c6e
4
- data.tar.gz: 87865b2ef1bea50c964d75cc79302a7aa29b9157
3
+ metadata.gz: b51e7b8dd21855f3c3a88d9da4be12c54a715d69
4
+ data.tar.gz: b4efab4ce923c03a8891099568192400ca085db4
5
5
  SHA512:
6
- metadata.gz: c6daaaaaab58645d5a22450790a7f44e30304ac68329bec4369d2024f790e5d39a0f6812e26a981ffb3d1bb4c356f14606523d0ecf4ccbd347965beb1ce4fead
7
- data.tar.gz: 8760c4719ebb9c953fbb8e6aa30a65de5e4332a7a956fb32c1c51906f822952883096769e1982e5154f534d8e80092b4153e415afb00c1561e3e30ed2568bda4
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
- ### Subscribe to an exchange
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
+ ![Tackle consumer](docs/consumer.png)
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
@@ -1,3 +1,3 @@
1
1
  module Tackle
2
- VERSION = "0.8.1"
2
+ VERSION = "0.9"
3
3
  end
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.8.1
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-05 00:00:00.000000000 Z
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