rt-tackle 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c97d2ff7d67880695b2d97d2ecad9b07abafaca6
4
+ data.tar.gz: 33cb64ad00cc670d12d675103e2f0a81f1c4ca9b
5
+ SHA512:
6
+ metadata.gz: 536a9691867f1a5d25f1cc35785f99c777e4172f826bd65e03d42432647301de49ecdb6d02c7000560e3284859141b5e11c5249337ac164f24bb9b640a878f7f
7
+ data.tar.gz: 3b6f70028efc46b0ce4fda79a0305354ebc749a6a580f7bcf117e2d0e09487629841dad15281b41b18c16233ea3026943a86b9d0489d2ecf0dff907d84b55583
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ vendor/bundle
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tackle.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # Tackle
2
+
3
+ [![Build Status](https://semaphoreci.com/api/v1/projects/b39e2ae2-2516-4fd7-9e2c-f5be1a043ff5/732979/badge.svg)](https://semaphoreci.com/renderedtext/tackle)
4
+
5
+ Tackles the problem of processing asynchronous jobs in reliable manner
6
+ by relying on RabbitMQ.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem "rt-tackle", :require => "tackle"
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ### Subscribe consumer:
19
+
20
+ ```ruby
21
+ require "tackle"
22
+
23
+ options = {
24
+ :url => "amqp://localhost", # optional
25
+ :exchange => "test-exchange", # required
26
+ :routing_key => "test-messages", # required
27
+ :queue => "test-queue", # required
28
+ :retry_limit => 8, # optional
29
+ :retry_delay => 30, # optional
30
+ :logger => Logger.new(STDOUT) # optional
31
+ }
32
+
33
+ Tackle.subscribe(options) do |message|
34
+ # Do something with message
35
+ end
36
+ ```
37
+
38
+ ### Publish message:
39
+
40
+ ```ruby
41
+
42
+ options = {
43
+ :url => "amqp://localhost", # optional
44
+ :exchange => "test-exchange", # required
45
+ :routing_key => "test-messages", # required
46
+ :logger => Logger.new(STDOUT) # optional
47
+ }
48
+
49
+ Tackle.publish("Hello, world!", options)
50
+ ```
51
+
52
+ ## Development
53
+
54
+
55
+ After checking out the repo, run `bin/setup` to install dependencies. Then,
56
+ run `rake rspec` to run the tests. You can also run `bin/console` for an
57
+ interactive prompt that will allow you to experiment.
58
+
59
+ To install this gem onto your local machine, run `bundle exec rake install`.
60
+ To release a new version, update the version number in `version.rb`, and
61
+ then run `bundle exec rake release`, which will create a git tag for the
62
+ version, push git commits and tags, and push the `.gem` file
63
+ to [rubygems.org](https://rubygems.org).
64
+
65
+ ## Contributing
66
+
67
+ Bug reports and pull requests are welcome on GitHub at
68
+ https://github.com/renderedtext/tackle.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "tackle"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,36 @@
1
+ require "tackle/tackle_logger"
2
+
3
+ module Tackle
4
+
5
+ class DelayedRetry
6
+ include Tackle::TackleLogger
7
+
8
+ def initialize(dead_letter_queue, properties, payload, retry_limit, logger)
9
+ @dead_letter_queue = dead_letter_queue
10
+ @properties = properties
11
+ @payload = payload
12
+ @retry_limit = retry_limit
13
+ @logger = logger
14
+ end
15
+
16
+ def schedule_retry
17
+ if retry_count < @retry_limit
18
+ tackle_log("Adding message to retry queue for retry #{retry_count + 1}/#{@retry_limit}")
19
+ @dead_letter_queue.publish(@payload, :headers => {:retry_count => retry_count + 1})
20
+ else
21
+ tackle_log("Reached #{retry_count} retries. Discarding message.")
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def retry_count
28
+ if @properties.headers && @properties.headers["retry_count"]
29
+ @properties.headers["retry_count"]
30
+ else
31
+ 0
32
+ end
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,50 @@
1
+ module Tackle
2
+ class Publisher
3
+ include Tackle::TackleLogger
4
+
5
+ def initialize(exchange_name, routing_key, url, logger)
6
+ @exchange_name = exchange_name
7
+ @routing_key = routing_key
8
+ @url = url
9
+ @logger = logger
10
+ end
11
+
12
+ def publish(message)
13
+ tackle_log("Publishing message started exchange='#{@exchange_name}' routing_key='#{@routing_key}'")
14
+
15
+ with_rabbit_connection do |conn|
16
+ channel = conn.create_channel
17
+ tackle_log("Created a communication channel")
18
+
19
+ exchange = channel.direct(@exchange_name, :durable => true)
20
+ tackle_log("Declared the exchange")
21
+
22
+ exchange.publish(message, :routing_key => @routing_key, :persistent => true)
23
+ end
24
+
25
+ tackle_log("Publishing message finished exchange='#{@exchange_name}' routing_key='#{@routing_key}'")
26
+ end
27
+
28
+ private
29
+
30
+ def with_rabbit_connection
31
+ tackle_log("Establishing rabbit connection")
32
+
33
+ conn = Bunny.new(@url)
34
+ conn.start
35
+
36
+ yield(conn)
37
+
38
+ tackle_log("Established rabbit connection")
39
+ rescue StandardError => ex
40
+ tackle_log("An exception occured while sending the message exception='#{ex.class.name}' message='#{ex.message}'")
41
+
42
+ raise ex
43
+ ensure
44
+ tackle_log("Clossing rabbit connection")
45
+
46
+ conn.close if conn
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,87 @@
1
+ require "bunny"
2
+ require "tackle/tackle_logger"
3
+
4
+ module Tackle
5
+
6
+ class Rabbit
7
+ include Tackle::TackleLogger
8
+
9
+ attr_reader :channel, :dead_letter_queue, :queue, :exchange
10
+
11
+ def initialize(exchange_name, routing_key, queue_name, amqp_url, retry_delay, logger)
12
+ @exchange_name = exchange_name
13
+ @routing_key = routing_key
14
+ @queue_name = queue_name
15
+ @amqp_url = amqp_url
16
+ @retry_delay = retry_delay
17
+ @logger = logger
18
+ end
19
+
20
+ def connect
21
+ @conn = Bunny.new(@amqp_url)
22
+ @conn.start
23
+ tackle_log("Connected to RabbitMQ")
24
+
25
+ @channel = @conn.create_channel
26
+ @channel.prefetch(1)
27
+
28
+ tackle_log("Connected to channel")
29
+ connect_queue
30
+ connect_dead_letter_queue
31
+ rescue StandardError => ex
32
+ tackle_log("An exception occured while connecting to the server message='#{ex.message}'")
33
+
34
+ raise ex
35
+ end
36
+
37
+ def on_uncaught_exception(blk)
38
+ @channel.on_uncaught_exception(&blk)
39
+ end
40
+
41
+ def close
42
+ @channel.close
43
+ tackle_log("Closed channel")
44
+ @conn.close
45
+ tackle_log("Closed connection to RabbitMQ")
46
+ end
47
+
48
+ def dead_letter_exchange_name
49
+ "#{@exchange_name}.dead_letter_exchange"
50
+ end
51
+
52
+ def dead_letter_queue_name
53
+ "#{@queue_name}_dead_letters"
54
+ end
55
+
56
+ private
57
+
58
+ def connect_queue
59
+ @exchange = @channel.direct(@exchange_name, :durable => true)
60
+ tackle_log("Connected to exchange '#{@exchange_name}'")
61
+
62
+ @queue = @channel.queue(@queue_name, :durable => true).bind(@exchange, :routing_key => @routing_key)
63
+
64
+ tackle_log("Connected to queue '#{@queue_name}' with routing key '#{@routing_key}'")
65
+ end
66
+
67
+ def connect_dead_letter_queue
68
+ tackle_log("Connected to dead letter exchange '#{dead_letter_exchange_name}'")
69
+
70
+ dead_letter_exchange = @channel.fanout(dead_letter_exchange_name)
71
+
72
+ queue_options = {
73
+ :durable => true,
74
+ :arguments => {
75
+ "x-dead-letter-exchange" => @exchange_name,
76
+ "x-dead-letter-routing-key" => @routing_key,
77
+ "x-message-ttl" => @retry_delay
78
+ }
79
+ }
80
+
81
+ @dead_letter_queue = @channel.queue(dead_letter_queue_name, queue_options).bind(dead_letter_exchange, :routing_key => @routing_key)
82
+
83
+ tackle_log("Connected to dead letter queue '#{dead_letter_queue_name}'")
84
+ end
85
+
86
+ end
87
+ end
@@ -0,0 +1,13 @@
1
+ module Tackle
2
+
3
+ module TackleLogger
4
+
5
+ def tackle_log(message)
6
+ pid = Process.pid
7
+
8
+ whole_message = "tackle - pid=#{pid} message=#{message}"
9
+
10
+ @logger.info(whole_message)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module Tackle
2
+ VERSION = "0.8.0"
3
+ end
@@ -0,0 +1,86 @@
1
+ require "tackle/rabbit"
2
+ require "tackle/delayed_retry"
3
+
4
+ module Tackle
5
+ class Worker
6
+ include Tackle::TackleLogger
7
+
8
+ attr_reader :rabbit
9
+
10
+ # Initializes now worker
11
+ #
12
+ # @param [String] exchange_name Name of the exchange queue is connected to.
13
+ # @param [String] routing_key Routing key for binding queue to exchange
14
+ # @param [String] queue_name Name of the queue worker is processing.
15
+ # @param [Hash] options Worker options for RabbitMQ connection, retries and logger.
16
+ #
17
+ # @option options [String] :url AMQP connection url. Defaults to 'localhost'
18
+ # @option options [Integer] :retry_limit Number of times message processing should be retried in case of an exception.
19
+ # @option options [Integer] :retry_delay Delay between processing retries. Dafaults to 30 seconds. Cannot be changed without deleting or renameing a queue.
20
+ # @option options [Logger] :logger Logger instance. Defaults to standard output.
21
+ #
22
+ # @api public
23
+ def initialize(exchange_name, routing_key, queue_name, options = {})
24
+ @queue_name = queue_name
25
+ @amqp_url = options[:url] || "amqp://localhost:5672"
26
+ @retry_limit = options[:retry_limit] || 8
27
+ @retry_delay = (options[:retry_delay] || 30) * 1000 #ms
28
+ @logger = options[:logger] || Logger.new(STDOUT)
29
+
30
+ @rabbit = Tackle::Rabbit.new(exchange_name,
31
+ routing_key,
32
+ @queue_name,
33
+ @amqp_url,
34
+ @retry_delay,
35
+ @logger)
36
+
37
+ @rabbit.connect
38
+ @rabbit.on_uncaught_exception(options[:on_uncaught_exception]) if options[:on_uncaught_exception]
39
+ end
40
+
41
+ # Subscribes for message deliveries
42
+ #
43
+ # @param [Block] Accepts a block that accepts message
44
+ #
45
+ # @api public
46
+ def subscribe(&block)
47
+ tackle_log("Subscribing to queue '#{@queue_name}'...")
48
+ rabbit.queue.subscribe(:manual_ack => true,
49
+ :block => true) do |delivery_info, properties, payload|
50
+
51
+ tackle_log("Received message. Processing...")
52
+ process_message(delivery_info, properties, payload, block)
53
+ tackle_log("Done with processing message.")
54
+
55
+ end
56
+ rescue Interrupt => _
57
+ rabbit.close
58
+ rescue StandardError => ex
59
+ tackle_log("An exception occured message='#{ex.message}'")
60
+
61
+ raise ex
62
+ end
63
+
64
+ def process_message(delivery_info, properties, payload, block)
65
+ begin
66
+ tackle_log("Calling message processor...")
67
+ block.call(payload)
68
+ @rabbit.channel.ack(delivery_info.delivery_tag)
69
+ tackle_log("Successfully processed message")
70
+ rescue Exception => ex
71
+ tackle_log("Failed to process message. Received exception '#{ex}'")
72
+ try_again = Tackle::DelayedRetry.new(@rabbit.dead_letter_queue,
73
+ properties,
74
+ payload,
75
+ @retry_limit,
76
+ @logger)
77
+ try_again.schedule_retry
78
+ tackle_log("Sending negative acknowledgement to source queue...")
79
+ @rabbit.channel.nack(delivery_info.delivery_tag)
80
+ tackle_log("Negative acknowledgement sent")
81
+
82
+ raise ex
83
+ end
84
+ end
85
+ end
86
+ end
data/lib/tackle.rb ADDED
@@ -0,0 +1,45 @@
1
+ require "tackle/version"
2
+
3
+ module Tackle
4
+ require "tackle/worker"
5
+ require "tackle/publisher"
6
+
7
+ def self.subscribe(options = {}, &block)
8
+ # required
9
+ exchange_name = options.fetch(:exchange)
10
+ routing_key = options.fetch(:routing_key)
11
+ queue_name = options.fetch(:queue)
12
+
13
+ # optional
14
+ amqp_url = options[:url]
15
+ retry_limit = options[:retry_limit]
16
+ retry_delay = options[:retry_delay]
17
+ logger = options[:logger]
18
+ on_uncaught_exception = options[:on_uncaught_exception]
19
+
20
+ worker = Tackle::Worker.new(exchange_name,
21
+ routing_key,
22
+ queue_name,
23
+ :url => amqp_url,
24
+ :retry_limit => retry_limit,
25
+ :retry_delay => retry_delay,
26
+ :logger => logger,
27
+ :on_uncaught_exception => on_uncaught_exception)
28
+
29
+ worker.subscribe(&block)
30
+ end
31
+
32
+ def self.publish(message, options = {})
33
+ # required
34
+ exchange_name = options.fetch(:exchange)
35
+ routing_key = options.fetch(:routing_key)
36
+
37
+ # optional
38
+ amqp_url = options[:url] || "amqp://localhost:5672"
39
+ logger = options[:logger] || Logger.new(STDOUT)
40
+
41
+ publisher = Tackle::Publisher.new(exchange_name, routing_key, amqp_url, logger)
42
+
43
+ publisher.publish(message)
44
+ end
45
+ end
data/reset_rabbit.sh ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+
3
+ sudo rabbitmqctl delete_vhost '/'
4
+ sudo rabbitmqctl stop_app
5
+ sudo rabbitmqctl reset
6
+ sudo rabbitmqctl start_app
data/tackle.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'tackle/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "rt-tackle"
8
+ spec.version = Tackle::VERSION
9
+ spec.licenses = ['MIT']
10
+ spec.authors = ["Rendered Text"]
11
+ spec.email = ["devops@renderedtext.com"]
12
+
13
+ spec.summary = %q{RabbitMQ based single-thread worker}
14
+ spec.description = %q{RabbitMQ based single-thread worker}
15
+ spec.homepage = "https://semaphoreci.com"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "bunny"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.10"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec"
27
+ spec.add_development_dependency "byebug"
28
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rt-tackle
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0
5
+ platform: ruby
6
+ authors:
7
+ - Rendered Text
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-09-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bunny
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.10'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.10'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: byebug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: RabbitMQ based single-thread worker
84
+ email:
85
+ - devops@renderedtext.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".rspec"
92
+ - Gemfile
93
+ - README.md
94
+ - Rakefile
95
+ - bin/console
96
+ - bin/setup
97
+ - lib/tackle.rb
98
+ - lib/tackle/delayed_retry.rb
99
+ - lib/tackle/publisher.rb
100
+ - lib/tackle/rabbit.rb
101
+ - lib/tackle/tackle_logger.rb
102
+ - lib/tackle/version.rb
103
+ - lib/tackle/worker.rb
104
+ - reset_rabbit.sh
105
+ - tackle.gemspec
106
+ homepage: https://semaphoreci.com
107
+ licenses:
108
+ - MIT
109
+ metadata: {}
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubyforge_project:
126
+ rubygems_version: 2.4.8
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: RabbitMQ based single-thread worker
130
+ test_files: []