rt-tackle 0.8.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 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: []