rt-tackle 0.8.1 → 0.9

Sign up to get free protection for your applications and to get access to all the features.
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