advanced-sneakers-activejob 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/.rspec +2 -0
- data/.rubocop.yml +35 -0
- data/.travis.yml +21 -0
- data/CHANGELOG.md +1 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +156 -0
- data/Rakefile +8 -0
- data/advanced-sneakers-activejob.gemspec +38 -0
- data/lib/active_job/queue_adapters/advanced_sneakers_adapter.rb +67 -0
- data/lib/advanced/sneakers/activejob.rb +3 -0
- data/lib/advanced_sneakers_activejob.rb +48 -0
- data/lib/advanced_sneakers_activejob/active_job_patch.rb +34 -0
- data/lib/advanced_sneakers_activejob/configuration.rb +34 -0
- data/lib/advanced_sneakers_activejob/content_type.rb +11 -0
- data/lib/advanced_sneakers_activejob/errors.rb +5 -0
- data/lib/advanced_sneakers_activejob/exponential_backoff.rb +24 -0
- data/lib/advanced_sneakers_activejob/handler.rb +80 -0
- data/lib/advanced_sneakers_activejob/publisher.rb +210 -0
- data/lib/advanced_sneakers_activejob/railtie.rb +23 -0
- data/lib/advanced_sneakers_activejob/tasks.rb +21 -0
- data/lib/advanced_sneakers_activejob/version.rb +5 -0
- data/lib/advanced_sneakers_activejob/workers_registry.rb +47 -0
- metadata +184 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b1a41d60fe8a0f6041331ada53384c54f3316a3f9c08fd94cf21949fd5467073
|
4
|
+
data.tar.gz: 16f1bd88c90e7edde21a1231420f0415c8e07d466226b51270ae4085f6d3b215
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c68cc556832c0433e137a399e43011b7e9e884ca50c12beed290b1cc065d84c31ba5e9fe620527c1b6adfb6eca49b251d4e5c4322b6dd34186194283b7ffcee2
|
7
|
+
data.tar.gz: 5b18d980cf4ee656803dfafb82213fb9a1edb91ba283228a375d963cc8ea5cc5fc93a790472f6d10d37731df6be86b36595fda34f07bf7521564ea8c046f9338
|
data/.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
/.bundle/
|
2
|
+
/.yardoc
|
3
|
+
/_yardoc/
|
4
|
+
/coverage/
|
5
|
+
/doc/
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/tmp/
|
9
|
+
/Gemfile.lock
|
10
|
+
/.ruby-gemset
|
11
|
+
/.ruby-version
|
12
|
+
|
13
|
+
# rspec failure tracking
|
14
|
+
.rspec_status
|
15
|
+
|
16
|
+
# sneakers pid file
|
17
|
+
sneakers.pid
|
18
|
+
|
19
|
+
# log files
|
20
|
+
spec/apps/log/*
|
21
|
+
|
22
|
+
# temp files
|
23
|
+
spec/apps/tmp/*
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
Layout/LineLength:
|
2
|
+
Max: 140
|
3
|
+
Exclude:
|
4
|
+
- spec/**/*
|
5
|
+
|
6
|
+
Metrics/ClassLength:
|
7
|
+
Enabled: false
|
8
|
+
|
9
|
+
Metrics/BlockLength:
|
10
|
+
Exclude:
|
11
|
+
- advanced-sneakers-activejob.gemspec
|
12
|
+
- spec/**/*
|
13
|
+
|
14
|
+
Metrics/MethodLength:
|
15
|
+
Exclude:
|
16
|
+
- spec/**/*
|
17
|
+
|
18
|
+
Security/MarshalLoad:
|
19
|
+
Exclude:
|
20
|
+
- spec/**/*
|
21
|
+
|
22
|
+
Security/Eval:
|
23
|
+
Exclude:
|
24
|
+
- spec/apps/**/*
|
25
|
+
|
26
|
+
Metrics/AbcSize:
|
27
|
+
Exclude:
|
28
|
+
- spec/**/*
|
29
|
+
|
30
|
+
Lint/SuppressedException:
|
31
|
+
Exclude:
|
32
|
+
- spec/**/*
|
33
|
+
|
34
|
+
Style/Documentation:
|
35
|
+
Enabled: false
|
data/.travis.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
---
|
2
|
+
dist: xenial
|
3
|
+
language: ruby
|
4
|
+
cache: bundler
|
5
|
+
rvm:
|
6
|
+
- 2.4.9
|
7
|
+
- 2.5.7
|
8
|
+
- 2.6.5
|
9
|
+
env:
|
10
|
+
- RAILS_VERSION="~> 4.2.11"
|
11
|
+
- RAILS_VERSION="~> 5.2.4"
|
12
|
+
- RAILS_VERSION="~> 6.0.2"
|
13
|
+
jobs:
|
14
|
+
exclude:
|
15
|
+
- rvm: 2.4.9
|
16
|
+
env: RAILS_VERSION="~> 6.0.2"
|
17
|
+
|
18
|
+
before_install: gem install bundler -v 1.17.3
|
19
|
+
before_script:
|
20
|
+
- ".ci/install_rabbitmq"
|
21
|
+
bundler_args: --jobs 3 --retry 3
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
## Original Release: 0.1.0
|
data/Gemfile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source 'https://rubygems.org'
|
4
|
+
|
5
|
+
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
6
|
+
|
7
|
+
# Specify your gem's dependencies in advanced-sneakers-activejob.gemspec
|
8
|
+
gemspec
|
9
|
+
|
10
|
+
gem 'rails', ENV.fetch('RAILS_VERSION', '~> 4.2.11')
|
11
|
+
gem 'sneakers', ENV.fetch('SNEAKERS_VERSION', '~> 2.11')
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Rustam Sharshenov
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
# `:advanced_sneakers` adapter for ActiveJob [![Build Status](https://travis-ci.com/veeqo/advanced-sneakers-activejob.svg?branch=master)](https://travis-ci.com/veeqo/advanced-sneakers-activejob)
|
2
|
+
|
3
|
+
Drop-in replacement for `:sneakers` adapter of ActiveJob. Extra features:
|
4
|
+
|
5
|
+
1. Tries to [handle unrouted messages](#unrouted-messages)
|
6
|
+
2. Respects `queue_as` of ActiveJob and defines consumer class per RabbitMQ queue
|
7
|
+
3. Supports [custom routing keys](#custom-routing-keys)
|
8
|
+
4. Allows to run ActiveJob consumers [separately](#how-to-separate-activejob-consumers) from native Sneakers consumers
|
9
|
+
5. Support for [`delayed jobs`](https://edgeguides.rubyonrails.org/active_job_basics.html#enqueue-the-job) `GuestsCleanupJob.set(wait: 1.week).perform_later(guest)`
|
10
|
+
6. [Exponential backoff\*](#exponential-backoff)
|
11
|
+
7. Exposes [`#delivery_info` & `#headers`](#amqp-metadata) AMQP metadata to job
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'advanced-sneakers-activejob'
|
19
|
+
```
|
20
|
+
|
21
|
+
And then execute:
|
22
|
+
|
23
|
+
$ bundle
|
24
|
+
|
25
|
+
Or install it yourself as:
|
26
|
+
|
27
|
+
$ gem install advanced-sneakers-activejob
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
[Configure ActiveJob adapter](https://edgeguides.rubyonrails.org/active_job_basics.html#setting-the-backend)
|
32
|
+
```ruby
|
33
|
+
config.active_job.queue_adapter = :advanced_sneakers
|
34
|
+
```
|
35
|
+
|
36
|
+
Run worker
|
37
|
+
```sh
|
38
|
+
rake sneakers:active_job
|
39
|
+
```
|
40
|
+
|
41
|
+
## Unrouted messages
|
42
|
+
|
43
|
+
If message is published before routing has been configured (e.g. by consumer), it might be lost. To mitigate this problem the adapter uses [:mandatory](http://rubybunny.info/articles/exchanges.html#publishing_messages_as_mandatory) option for publishing messages. RabbitMQ returns unrouted messages back and the publisher is able to handle them:
|
44
|
+
|
45
|
+
1. Create queue
|
46
|
+
2. Create binding
|
47
|
+
3. Re-publish message
|
48
|
+
|
49
|
+
There is a setting `handle_unrouted_messages` in [configuration](#configuration) to disable this behavior. If it is disabled, publisher will only log unrouted messages.
|
50
|
+
|
51
|
+
Take into accout that **this process is asynchronous**. It means that in case of network failures or process exit unrouted messages could be lost. The adapter tries to postpone application exit up to 30 seconds in case if there are unrouted messages, but it does not provide any guarantees.
|
52
|
+
|
53
|
+
**Delayed messages are not handled!** If job is delayed `GuestsCleanupJob.set(wait: 1.week).perform_later(guest)` and there is no proper routing defined at the moment of job execution, it would be lost.
|
54
|
+
|
55
|
+
## Custom routing keys
|
56
|
+
|
57
|
+
Advanced sneakers adapter supports customizable [routing keys](https://www.rabbitmq.com/tutorials/tutorial-four-ruby.html).
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
class MyJob < ActiveJob::Base
|
61
|
+
|
62
|
+
queue_as :some_name
|
63
|
+
|
64
|
+
def perform(params)
|
65
|
+
# ProcessData.new(params).call
|
66
|
+
end
|
67
|
+
|
68
|
+
def routing_key
|
69
|
+
# we have instance of job here (including #arguments)
|
70
|
+
'my.custom.routing.key'
|
71
|
+
end
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
Take into accout that **custom routing key is used for publishing only**.
|
76
|
+
|
77
|
+
## How to separate ActiveJob consumers
|
78
|
+
|
79
|
+
Sneakers comes with `rake sneakers:run` task, which would run all consumers (including ActiveJob ones). If you need to run native sneakers consumers apart from ActiveJob consumers:
|
80
|
+
1. Set `activejob_workers_strategy` to `:exclude` in [configuration](#configuration)
|
81
|
+
2. Run `rake sneakers:run` task to run native Sneakers consumers
|
82
|
+
3. Run `rake sneakers:active_job` task to run ActiveJob consumers
|
83
|
+
|
84
|
+
Tip: if you want to see how consumers are grouped, exec `Sneakers::Worker::Classes` in rails console.
|
85
|
+
|
86
|
+
## Exponential backoff\*
|
87
|
+
|
88
|
+
The adapter enforces `AdvancedSneakersActiveJob::Handler` for ActiveJob consumers. This handler applies [exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) if failure is not handled by ActiveJob [`rescue_from`/`retry_on`/`discard_on`](https://edgeguides.rubyonrails.org/active_job_basics.html#retrying-or-discarding-failed-jobs).
|
89
|
+
Error name is tracked in `x-last-error-name`, error full message is tracked in `x-last-error-details` gzipped & encoded by Base64. To decode error details:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
ActiveSupport::Gzip.decompress(Base64.decode64(data_from_header))
|
93
|
+
```
|
94
|
+
|
95
|
+
\* For RabbitMQ queues amount optimization exponential backoff is not calculated by formula, but predifined. You can customize `retry_delay_proc` in [configuration](#configuration)
|
96
|
+
|
97
|
+
## AMQP metadata
|
98
|
+
|
99
|
+
Each message in AMQP comes with `delivery_info` and `headers`. `:advanced_sneakers` adapter provides them on job level.
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
class SomeComplexJob < ActiveJob::Base
|
103
|
+
before :perform do |job|
|
104
|
+
# metadata is available in callbacks
|
105
|
+
logger.debug({delivery_info: job.delivery_info, headers: job.headers})
|
106
|
+
end
|
107
|
+
|
108
|
+
def perform(msg)
|
109
|
+
# metadata is available here as well
|
110
|
+
logger.debug({delivery_info: delivery_info, headers: headers})
|
111
|
+
end
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
## Configuration
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
AdvancedSneakersActiveJob.configure do |config|
|
119
|
+
# Should AdvancedSneakersActiveJob try to handle unrouted messages?
|
120
|
+
# There are still no guarantees that unrouted message is not lost in case of network failure or process exit.
|
121
|
+
# Delayed unrouted messages are not handled.
|
122
|
+
config.handle_unrouted_messages = true
|
123
|
+
|
124
|
+
# Should Sneakers build-in runner (e.g. `rake sneakers:run`) run ActiveJob consumers?
|
125
|
+
# :include - yes
|
126
|
+
# :exclude - no
|
127
|
+
# :only - Sneakers runner will run _only_ ActiveJob consumers
|
128
|
+
#
|
129
|
+
# This setting might be helpful if you want to run ActiveJob consumers apart from native Sneakers consumers.
|
130
|
+
# In that case set strategy to :exclude and use `rake sneakers:run` for native and `rake sneakers:active_job` for ActiveJob consumers
|
131
|
+
config.activejob_workers_strategy = :include
|
132
|
+
|
133
|
+
# All delayed messages delays are rounded to seconds.
|
134
|
+
config.delay_proc = ->(timestamp) { (timestamp - Time.now.to_f).round } } # integer result is expected
|
135
|
+
|
136
|
+
# Delayed queues can be filtered by this prefix (e.g. delayed:60 - queue for messages with 1 minute delay)
|
137
|
+
config.delayed_queue_prefix = 'delayed'
|
138
|
+
|
139
|
+
# Custom sneakers configuration for ActiveJob publisher & runner
|
140
|
+
config.sneakers = {
|
141
|
+
exchange: 'activejob',
|
142
|
+
handler: AdvancedSneakersActiveJob::Handler
|
143
|
+
}
|
144
|
+
|
145
|
+
# Define custom delay for retries, but remember - each unique delay leads to new queue on RabbitMQ side
|
146
|
+
config.retry_delay_proc = ->(count) { AdvancedSneakersActiveJob::EXPONENTIAL_BACKOFF[count] }
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
150
|
+
## Contributing
|
151
|
+
|
152
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/veeqo/advanced-sneakers-activejob.
|
153
|
+
|
154
|
+
## License
|
155
|
+
|
156
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'advanced_sneakers_activejob/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'advanced-sneakers-activejob'
|
9
|
+
spec.version = AdvancedSneakersActiveJob::VERSION
|
10
|
+
spec.authors = ['Rustam Sharshenov', 'Vlad Bokov']
|
11
|
+
spec.email = ['rustam@sharshenov.com', 'vlad@lunatic.cat']
|
12
|
+
|
13
|
+
spec.summary = 'Advanced Sneakers adapter for ActiveJob'
|
14
|
+
spec.description = 'Advanced Sneakers adapter for ActiveJob'
|
15
|
+
spec.homepage = 'https://github.com/veeqo/advanced-sneakers-activejob'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
if spec.respond_to?(:metadata)
|
19
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
20
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
21
|
+
spec.metadata['changelog_uri'] = 'https://github.com/veeqo/advanced-sneakers-activejob/blob/master/CHANGELOG.md'
|
22
|
+
end
|
23
|
+
|
24
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
25
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|\.ci)/}) }
|
26
|
+
end
|
27
|
+
spec.require_paths = ['lib']
|
28
|
+
|
29
|
+
spec.add_dependency 'activejob', '>= 4.2'
|
30
|
+
spec.add_dependency 'sneakers', '~> 2.7'
|
31
|
+
|
32
|
+
spec.add_development_dependency 'bundler'
|
33
|
+
spec.add_development_dependency 'pry-byebug'
|
34
|
+
spec.add_development_dependency 'rabbitmq_http_api_client', '~> 1.13'
|
35
|
+
spec.add_development_dependency 'rails', '>= 4.2'
|
36
|
+
spec.add_development_dependency 'rake'
|
37
|
+
spec.add_development_dependency 'rspec'
|
38
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveJob
|
4
|
+
module QueueAdapters
|
5
|
+
# == Active Job advanced Sneakers adapter
|
6
|
+
#
|
7
|
+
# A high-performance RabbitMQ background processing framework for Ruby.
|
8
|
+
# Sneakers is being used in production for both I/O and CPU intensive
|
9
|
+
# workloads, and have achieved the goals of high-performance and
|
10
|
+
# 0-maintenance, as designed.
|
11
|
+
#
|
12
|
+
# Read more about Sneakers {here}[https://github.com/jondot/sneakers].
|
13
|
+
#
|
14
|
+
# To use the advanced Sneakers adapter set the queue_adapter config to +:advanced_sneakers+.
|
15
|
+
#
|
16
|
+
# Rails.application.config.active_job.queue_adapter = :advanced_sneakers
|
17
|
+
class AdvancedSneakersAdapter
|
18
|
+
@monitor = Monitor.new
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def enqueue(job) #:nodoc:
|
22
|
+
AdvancedSneakersActiveJob.publisher.publish(*publish_params(job))
|
23
|
+
end
|
24
|
+
|
25
|
+
def enqueue_at(job, timestamp) #:nodoc:
|
26
|
+
delay = AdvancedSneakersActiveJob.config.delay_proc.call(timestamp).to_i
|
27
|
+
|
28
|
+
if delay.positive?
|
29
|
+
AdvancedSneakersActiveJob.publisher.publish_delayed(*publish_params(job).tap { |params| params.last[:delay] = delay })
|
30
|
+
else
|
31
|
+
enqueue(job)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def publish_params(job)
|
38
|
+
@monitor.synchronize do
|
39
|
+
[
|
40
|
+
Sneakers::ContentType.serialize(job.serialize, AdvancedSneakersActiveJob::CONTENT_TYPE),
|
41
|
+
{ routing_key: routing_key(job) }
|
42
|
+
]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def routing_key(job)
|
47
|
+
queue_name = job.queue_name.respond_to?(:call) ? job.queue_name.call : job.queue_name
|
48
|
+
job.respond_to?(:routing_key) ? job.routing_key : queue_name
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
delegate :enqueue, :enqueue_at, to: :'ActiveJob::QueueAdapters::AdvancedSneakersAdapter' # compatibility with Rails 5+
|
53
|
+
|
54
|
+
class JobWrapper #:nodoc:
|
55
|
+
def work_with_params(msg, delivery_info, headers)
|
56
|
+
# compatibility with :sneakers adapter
|
57
|
+
msg = ActiveSupport::JSON.decode(msg) unless headers[:content_type] == AdvancedSneakersActiveJob::CONTENT_TYPE
|
58
|
+
|
59
|
+
msg['delivery_info'] = delivery_info
|
60
|
+
msg['headers'] = headers
|
61
|
+
Base.execute msg
|
62
|
+
ack!
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/core_ext'
|
5
|
+
|
6
|
+
require 'sneakers'
|
7
|
+
require 'advanced_sneakers_activejob/workers_registry'
|
8
|
+
Sneakers::Worker.send(:remove_const, :Classes)
|
9
|
+
Sneakers::Worker::Classes = AdvancedSneakersActiveJob::WorkersRegistry.new
|
10
|
+
|
11
|
+
require 'advanced_sneakers_activejob/version'
|
12
|
+
require 'advanced_sneakers_activejob/content_type'
|
13
|
+
require 'advanced_sneakers_activejob/exponential_backoff'
|
14
|
+
require 'advanced_sneakers_activejob/handler'
|
15
|
+
require 'advanced_sneakers_activejob/configuration'
|
16
|
+
require 'advanced_sneakers_activejob/errors'
|
17
|
+
require 'advanced_sneakers_activejob/publisher'
|
18
|
+
require 'advanced_sneakers_activejob/active_job_patch'
|
19
|
+
require 'advanced_sneakers_activejob/railtie' if defined?(::Rails::Railtie)
|
20
|
+
require 'active_job/queue_adapters/advanced_sneakers_adapter'
|
21
|
+
|
22
|
+
# Advanced Sneakers adapter for ActiveJob
|
23
|
+
module AdvancedSneakersActiveJob
|
24
|
+
class << self
|
25
|
+
def config
|
26
|
+
@config ||= Configuration.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def configure
|
30
|
+
yield config
|
31
|
+
end
|
32
|
+
|
33
|
+
def define_consumer(queue_name:)
|
34
|
+
@consumers ||= {}
|
35
|
+
|
36
|
+
@consumers[queue_name] ||= begin
|
37
|
+
klass = Class.new(ActiveJob::QueueAdapters::AdvancedSneakersAdapter::JobWrapper)
|
38
|
+
klass.include Sneakers::Worker
|
39
|
+
const_set([queue_name, 'queue_consumer'].join('_').classify, klass)
|
40
|
+
klass.from_queue(queue_name, AdvancedSneakersActiveJob.config.sneakers)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def publisher
|
45
|
+
@publisher ||= AdvancedSneakersActiveJob::Publisher.new
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AdvancedSneakersActiveJob
|
4
|
+
module ActiveJobPatch
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
# AMQP message contains metadata which might be helpful for consumer (e.g. job.delivery_info.routing_key)
|
9
|
+
attr_accessor :delivery_info, :headers
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def deserialize(job_data)
|
14
|
+
super(job_data).tap do |job|
|
15
|
+
job.delivery_info = job_data['delivery_info']
|
16
|
+
job.headers = job_data['headers']
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def queue_as(*args)
|
21
|
+
super(*args)
|
22
|
+
define_consumer
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def define_consumer
|
28
|
+
name = queue_name.respond_to?(:call) ? queue_name.call : queue_name
|
29
|
+
|
30
|
+
AdvancedSneakersActiveJob.define_consumer(queue_name: name)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AdvancedSneakersActiveJob
|
4
|
+
# Advanced Sneakers adapter allows to patch Sneakers with custom configuration.
|
5
|
+
# It is useful when already have Sneakers workers running and you want to run ActiveJob Sneakers process with another options.
|
6
|
+
class Configuration
|
7
|
+
include ActiveSupport::Configurable
|
8
|
+
|
9
|
+
DEFAULT_SNEAKERS_CONFIG = {
|
10
|
+
exchange: 'activejob',
|
11
|
+
handler: AdvancedSneakersActiveJob::Handler
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
config_accessor(:handle_unrouted_messages) { true } # create queue/binding and re-publish if message is unrouted
|
15
|
+
config_accessor(:activejob_workers_strategy) { :include } # [:include, :exclude, :only]
|
16
|
+
config_accessor(:delay_proc) { ->(timestamp) { (timestamp - Time.now.to_f).round } } # seconds
|
17
|
+
config_accessor(:delayed_queue_prefix) { 'delayed' }
|
18
|
+
config_accessor(:retry_delay_proc) { ->(count) { AdvancedSneakersActiveJob::EXPONENTIAL_BACKOFF[count] } } # seconds
|
19
|
+
|
20
|
+
def sneakers
|
21
|
+
custom_config = DEFAULT_SNEAKERS_CONFIG.deep_merge(config.sneakers || {})
|
22
|
+
|
23
|
+
if custom_config[:amqp].present? & custom_config[:vhost].nil?
|
24
|
+
custom_config[:vhost] = AMQ::Settings.parse_amqp_url(custom_config[:amqp]).fetch(:vhost, '/')
|
25
|
+
end
|
26
|
+
|
27
|
+
Sneakers::CONFIG.to_hash.deep_merge(custom_config)
|
28
|
+
end
|
29
|
+
|
30
|
+
def sneakers=(custom)
|
31
|
+
config.sneakers = custom
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AdvancedSneakersActiveJob
|
4
|
+
CONTENT_TYPE = 'application/vnd.activejob+json'
|
5
|
+
end
|
6
|
+
|
7
|
+
Sneakers::ContentType.register(
|
8
|
+
content_type: AdvancedSneakersActiveJob::CONTENT_TYPE,
|
9
|
+
deserializer: ->(payload) { ActiveSupport::JSON.decode(payload) },
|
10
|
+
serializer: ->(payload) { ActiveSupport::JSON.encode(payload) }
|
11
|
+
)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AdvancedSneakersActiveJob
|
4
|
+
# Calculating exponential backoff by formulas with randomization leads to numerous RabbitMQ queues.
|
5
|
+
EXPONENTIAL_BACKOFF = {
|
6
|
+
1 => 3, # 3 seconds
|
7
|
+
2 => 30, # 30 seconds
|
8
|
+
3 => 90, # 1.5 minutes
|
9
|
+
4 => 240, # 4 minutes
|
10
|
+
5 => 600, # 10 minutes
|
11
|
+
6 => 1200, # 20 minutes
|
12
|
+
7 => 2400, # 40 minutes
|
13
|
+
8 => 3600, # 1 hour
|
14
|
+
9 => 7200, # 2 hours
|
15
|
+
10 => 10_800, # 3 hours
|
16
|
+
11 => 14_400, # 4 hours
|
17
|
+
12 => 21_600, # 6 hours
|
18
|
+
13 => 28_800, # 8 hours
|
19
|
+
14 => 36_000, # 10 hours
|
20
|
+
15 => 50_400, # 14 hours
|
21
|
+
16 => 64_800, # 18 hours
|
22
|
+
17 => 86_400 # 24 hours
|
23
|
+
}.tap { |h| h.default = 86_400 }.freeze
|
24
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AdvancedSneakersActiveJob
|
4
|
+
# Handler puts error details to message header and reenqueues job with delay
|
5
|
+
class Handler < Sneakers::Handlers::Oneshot
|
6
|
+
def error(delivery_info, properties, message, error)
|
7
|
+
params = properties.to_h
|
8
|
+
params[:headers] = patch_headers(params[:headers], delivery_info, error)
|
9
|
+
params[:routing_key] = delivery_info.routing_key
|
10
|
+
params[:delay] = calculate_delay(params[:headers], delivery_info)
|
11
|
+
|
12
|
+
AdvancedSneakersActiveJob.publisher.publish_delayed(message, params)
|
13
|
+
|
14
|
+
acknowledge(delivery_info, properties, message)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def patch_headers(headers, delivery_info, error)
|
20
|
+
queue = queue_name(delivery_info)
|
21
|
+
exchange = delivery_info.exchange
|
22
|
+
routing_key = delivery_info.routing_key
|
23
|
+
|
24
|
+
track_error_in_headers(headers, error)
|
25
|
+
track_death_in_headers(headers, queue, exchange, routing_key)
|
26
|
+
|
27
|
+
headers
|
28
|
+
end
|
29
|
+
|
30
|
+
# Headers are patched to mimic behavior of "nack" and DLX
|
31
|
+
def track_death_in_headers(headers, queue, exchange, routing_key)
|
32
|
+
headers['x-first-death-exchange'] ||= exchange
|
33
|
+
headers['x-first-death-queue'] ||= queue
|
34
|
+
headers['x-first-death-reason'] ||= 'rejected'
|
35
|
+
headers['x-death'] ||= []
|
36
|
+
|
37
|
+
if (death = death_header(headers, queue))
|
38
|
+
death['count'] += 1
|
39
|
+
else
|
40
|
+
headers['x-death'] << build_death_row(queue, exchange, routing_key)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def build_death_row(queue, exchange, routing_key)
|
45
|
+
{
|
46
|
+
'count' => 1,
|
47
|
+
'reason' => 'rejected',
|
48
|
+
'queue' => queue,
|
49
|
+
'time' => Time.now,
|
50
|
+
'exchange' => exchange,
|
51
|
+
'routing-keys' => [routing_key]
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def track_error_in_headers(headers, error)
|
56
|
+
details = if error.respond_to?(:full_message) # ruby 2.5+
|
57
|
+
error.full_message
|
58
|
+
else
|
59
|
+
([error.message] + error.backtrace).join("\n")
|
60
|
+
end
|
61
|
+
|
62
|
+
headers['x-last-error-name'] = error.class.name
|
63
|
+
headers['x-last-error-details'] = Base64.encode64(ActiveSupport::Gzip.compress(details))
|
64
|
+
end
|
65
|
+
|
66
|
+
def calculate_delay(headers, delivery_info)
|
67
|
+
death_count = death_header(headers, queue_name(delivery_info)).fetch('count')
|
68
|
+
|
69
|
+
AdvancedSneakersActiveJob.config.retry_delay_proc.call(death_count)
|
70
|
+
end
|
71
|
+
|
72
|
+
def queue_name(delivery_info)
|
73
|
+
delivery_info.consumer.queue.name
|
74
|
+
end
|
75
|
+
|
76
|
+
def death_header(headers, queue_name)
|
77
|
+
headers.fetch('x-death').detect { |death| death.fetch('queue') == queue_name }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AdvancedSneakersActiveJob
|
4
|
+
# Based on Sneakers::Publisher, but refactored to utilize :mandatory option to handle unrouted messages
|
5
|
+
# http://rubybunny.info/articles/exchanges.html#publishing_messages_as_mandatory
|
6
|
+
class Publisher
|
7
|
+
WAIT_FOR_UNROUTED_MESSAGES_AT_EXIT_TIMEOUT = 30
|
8
|
+
|
9
|
+
delegate :sneakers, :handle_unrouted_messages, :delayed_queue_prefix,
|
10
|
+
to: :'AdvancedSneakersActiveJob.config', prefix: :config
|
11
|
+
|
12
|
+
delegate :logger, to: :'ActiveJob::Base'
|
13
|
+
|
14
|
+
attr_reader :publish_channel, :republish_channel,
|
15
|
+
:publish_exchange, :republish_exchange,
|
16
|
+
:publish_delayed_exchange, :republish_delayed_exchange
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@mutex = Mutex.new
|
20
|
+
at_exit { wait_for_unrouted_messages_processing(timeout: WAIT_FOR_UNROUTED_MESSAGES_AT_EXIT_TIMEOUT) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def publish(message, routing_key:, headers: {}, **properties)
|
24
|
+
ensure_connection!
|
25
|
+
|
26
|
+
logger.debug "Publishing <#{message}> to [#{publish_exchange.name}] with routing_key [#{routing_key}]"
|
27
|
+
|
28
|
+
params = properties.deep_symbolize_keys.merge(
|
29
|
+
routing_key: routing_key,
|
30
|
+
mandatory: true,
|
31
|
+
content_type: AdvancedSneakersActiveJob::CONTENT_TYPE,
|
32
|
+
headers: headers
|
33
|
+
)
|
34
|
+
|
35
|
+
publish_exchange.publish(message, params)
|
36
|
+
end
|
37
|
+
|
38
|
+
def publish_delayed(message, routing_key:, delay:, headers: {}, **properties)
|
39
|
+
ensure_connection!
|
40
|
+
|
41
|
+
logger.debug "Publishing <#{message}> to [#{publish_delayed_exchange.name}] with routing_key [#{routing_key}] and delay [#{delay}]"
|
42
|
+
|
43
|
+
params = properties.deep_symbolize_keys.merge(
|
44
|
+
routing_key: routing_key,
|
45
|
+
mandatory: true,
|
46
|
+
content_type: AdvancedSneakersActiveJob::CONTENT_TYPE,
|
47
|
+
headers: headers.deep_symbolize_keys.merge(delay: delay.to_i) # do not use x- prefix because headers exchanges ignore such headers
|
48
|
+
)
|
49
|
+
|
50
|
+
publish_delayed_exchange.publish(message, params)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def ensure_connection!
|
56
|
+
@mutex.synchronize do
|
57
|
+
unless connected?
|
58
|
+
start_connections!
|
59
|
+
create_channels!
|
60
|
+
configure_exchanges!
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def start_connections!
|
66
|
+
@publish_connection ||= create_bunny_connection
|
67
|
+
@publish_connection.start
|
68
|
+
|
69
|
+
@republish_connection ||= create_bunny_connection
|
70
|
+
@republish_connection.start
|
71
|
+
end
|
72
|
+
|
73
|
+
def create_channels!
|
74
|
+
@publish_channel = @publish_connection.create_channel
|
75
|
+
@republish_channel = @republish_connection.create_channel
|
76
|
+
end
|
77
|
+
|
78
|
+
def configure_exchanges!
|
79
|
+
@publish_exchange = build_exchange(@publish_channel)
|
80
|
+
@publish_exchange.on_return { |*attrs| handle_unrouted_messages(*attrs) }
|
81
|
+
|
82
|
+
@publish_delayed_exchange = build_delayed_exchange(@publish_channel)
|
83
|
+
@publish_delayed_exchange.on_return { |*attrs| handle_unrouted_delayed_messages(*attrs) }
|
84
|
+
|
85
|
+
@republish_exchange = build_exchange(republish_channel)
|
86
|
+
@republish_delayed_exchange = build_delayed_exchange(republish_channel)
|
87
|
+
end
|
88
|
+
|
89
|
+
def connected?
|
90
|
+
@publish_connection&.connected? &&
|
91
|
+
@republish_connection&.connected? &&
|
92
|
+
@publish_channel &&
|
93
|
+
@republish_channel
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returned messages are processed asynchronously and there is a probability for messages loses on program exit or network failure.
|
97
|
+
# Second connection is required because `on_return` is called within a frameset of amqp connection.
|
98
|
+
# Any interaction within the connection (even by another channel) can lead to connection error.
|
99
|
+
# https://github.com/ruby-amqp/bunny/blob/7fb05abf36637557f75a69790be78f9cc1cea807/lib/bunny/session.rb#L683
|
100
|
+
def handle_unrouted_messages(return_info, properties, message)
|
101
|
+
@unrouted_message = true
|
102
|
+
|
103
|
+
params = { message: message, return_info: return_info, properties: properties }
|
104
|
+
|
105
|
+
raise(PublishError, params) if return_info.reply_code != 312 # NO_ROUTE
|
106
|
+
|
107
|
+
if config_handle_unrouted_messages
|
108
|
+
setup_routing_and_republish_message(params)
|
109
|
+
else
|
110
|
+
logger.warn("Message is not routed! #{params}")
|
111
|
+
end
|
112
|
+
|
113
|
+
@unrouted_message = false
|
114
|
+
end
|
115
|
+
|
116
|
+
def handle_unrouted_delayed_messages(return_info, properties, message)
|
117
|
+
@unrouted_delayed_message = true
|
118
|
+
|
119
|
+
params = { message: message, return_info: return_info, properties: properties }
|
120
|
+
|
121
|
+
raise(PublishError, params) if return_info.reply_code != 312 # NO_ROUTE
|
122
|
+
|
123
|
+
setup_routing_and_republish_delayed_message(params)
|
124
|
+
|
125
|
+
@unrouted_delayed_message = false
|
126
|
+
end
|
127
|
+
|
128
|
+
# TODO: introduce more reliable way to wait for handling of unrouted messages at exit
|
129
|
+
def wait_for_unrouted_messages_processing(timeout:)
|
130
|
+
sleep(0.05) # gives publish_exchange some time to receive retuned message
|
131
|
+
|
132
|
+
return unless @unrouted_message || @unrouted_delayed_message
|
133
|
+
|
134
|
+
logger.warn("Waiting up to #{timeout} seconds for unrouted messages handling")
|
135
|
+
|
136
|
+
Timeout.timeout(timeout) { sleep 0.01 while @unrouted_message || @unrouted_delayed_message }
|
137
|
+
rescue Timeout::Error
|
138
|
+
logger.warn('Some unrouted messages are lost on process exit!')
|
139
|
+
end
|
140
|
+
|
141
|
+
def setup_routing_and_republish_message(message:, return_info:, properties:)
|
142
|
+
logger.debug("Performing queue/binding setup & re-publish for unrouted message. #{{ message: message, return_info: return_info }}")
|
143
|
+
|
144
|
+
routing_key = return_info.routing_key
|
145
|
+
|
146
|
+
create_queue_and_binding(queue_name: deserialize(message).fetch('queue_name'), routing_key: routing_key)
|
147
|
+
|
148
|
+
logger.debug "Re-publishing <#{message}> to [#{republish_exchange.name}] with routing_key [#{routing_key}]"
|
149
|
+
republish_exchange.publish(message, properties.to_h.merge(routing_key: routing_key))
|
150
|
+
end
|
151
|
+
|
152
|
+
def create_queue_and_binding(queue_name:, routing_key:)
|
153
|
+
logger.debug "Creating queue [#{queue_name}] and binding with routing_key [#{routing_key}] to [#{republish_exchange.name}]"
|
154
|
+
republish_channel.queue(queue_name, config_sneakers[:queue_options]).tap do |queue|
|
155
|
+
queue.bind(republish_exchange, routing_key: routing_key)
|
156
|
+
republish_channel.deregister_queue(queue) # we are not going to work with this queue in this channel
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def setup_routing_and_republish_delayed_message(message:, return_info:, properties:)
|
161
|
+
delay = properties.headers.fetch('delay').to_i
|
162
|
+
queue_name = delayed_queue_name(delay: delay)
|
163
|
+
|
164
|
+
logger.debug "Creating delayed queue [#{queue_name}]"
|
165
|
+
|
166
|
+
create_delayed_queue_and_binding(queue_name: queue_name, delay: delay)
|
167
|
+
|
168
|
+
republish_delayed_exchange.publish message, properties.to_h.merge(routing_key: return_info.routing_key)
|
169
|
+
end
|
170
|
+
|
171
|
+
def delayed_queue_name(delay:)
|
172
|
+
[
|
173
|
+
config_delayed_queue_prefix,
|
174
|
+
delay
|
175
|
+
].join(':')
|
176
|
+
end
|
177
|
+
|
178
|
+
def create_delayed_queue_and_binding(queue_name:, delay:)
|
179
|
+
queue_arguments = {
|
180
|
+
'x-queue-mode' => 'lazy', # tell RabbitMQ not to use RAM for this queue as it won't be consumed
|
181
|
+
'x-message-ttl' => delay * 1000, # make messages die after requested time
|
182
|
+
'x-dead-letter-exchange' => republish_exchange.name # died messages go to original exchange and then routed to consumers
|
183
|
+
}
|
184
|
+
|
185
|
+
republish_channel.queue(queue_name, durable: true, arguments: queue_arguments).tap do |queue|
|
186
|
+
queue.bind(republish_delayed_exchange, arguments: { delay: delay })
|
187
|
+
republish_channel.deregister_queue(queue) # we are not going to work with this queue in this channel
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def build_exchange(channel)
|
192
|
+
channel.exchange(config_sneakers[:exchange], config_sneakers[:exchange_options])
|
193
|
+
end
|
194
|
+
|
195
|
+
def build_delayed_exchange(channel)
|
196
|
+
channel.exchange([config_sneakers[:exchange], 'delayed'].join('-'), type: 'headers', durable: true)
|
197
|
+
end
|
198
|
+
|
199
|
+
def create_bunny_connection
|
200
|
+
Bunny.new config_sneakers[:amqp],
|
201
|
+
vhost: config_sneakers[:vhost],
|
202
|
+
heartbeat: config_sneakers[:heartbeat],
|
203
|
+
properties: config_sneakers.fetch(:properties, {})
|
204
|
+
end
|
205
|
+
|
206
|
+
def deserialize(message)
|
207
|
+
Sneakers::ContentType.deserialize(message, AdvancedSneakersActiveJob::CONTENT_TYPE)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AdvancedSneakersActiveJob
|
4
|
+
# Rails integration
|
5
|
+
class Railtie < ::Rails::Railtie
|
6
|
+
initializer 'advanced_sneakers_activejob.discover_mailer_job' do
|
7
|
+
ActiveSupport.on_load(:action_mailer) do
|
8
|
+
require 'action_mailer/delivery_job' # Enforce definition of ActionMailer::DeliveryJob::Consumer
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
initializer 'advanced_sneakers_activejob.discover_default_job' do
|
13
|
+
ActiveSupport.on_load(:active_job) do
|
14
|
+
ActiveJob::Base.include AdvancedSneakersActiveJob::ActiveJobPatch
|
15
|
+
ActiveJob::Base.queue_as # Enforce definition of ActiveJob::Base::Consumer (default queue)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
rake_tasks do
|
20
|
+
require 'advanced_sneakers_activejob/tasks'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sneakers/tasks'
|
4
|
+
|
5
|
+
task :environment
|
6
|
+
|
7
|
+
namespace :sneakers do
|
8
|
+
desc 'Start work for ActiveJob only (set $WORKERS=ActiveJobKlass1::Consumer,ActiveJobKlass2::Consumer)'
|
9
|
+
task :active_job do
|
10
|
+
Rake::Task['environment'].invoke
|
11
|
+
|
12
|
+
# Enforsing ActiveJob-only workers
|
13
|
+
AdvancedSneakersActiveJob.configure { |c| c.activejob_workers_strategy = :only }
|
14
|
+
|
15
|
+
Sneakers.configure(AdvancedSneakersActiveJob.config.sneakers)
|
16
|
+
|
17
|
+
Sneakers.logger.level = Logger::INFO # debug logs are too noizy because of bunny
|
18
|
+
|
19
|
+
Rake::Task['sneakers:run'].invoke
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AdvancedSneakersActiveJob
|
4
|
+
# Sneakers uses Sneakers::Worker::Classes array to track all workers.
|
5
|
+
# WorkersRegistry mocks original array to track ActiveJob workers separately.
|
6
|
+
class WorkersRegistry
|
7
|
+
attr_reader :sneakers_workers, :activejob_workers
|
8
|
+
|
9
|
+
delegate :activejob_workers_strategy, to: :'AdvancedSneakersActiveJob.config'
|
10
|
+
|
11
|
+
delegate :empty?, to: :call
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@sneakers_workers = []
|
15
|
+
@activejob_workers = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def <<(worker)
|
19
|
+
if worker <= ActiveJob::QueueAdapters::AdvancedSneakersAdapter::JobWrapper
|
20
|
+
activejob_workers << worker
|
21
|
+
else
|
22
|
+
sneakers_workers << worker
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Sneakers workergroup supports callable objects.
|
27
|
+
# https://github.com/jondot/sneakers/pull/210/files
|
28
|
+
# https://github.com/jondot/sneakers/blob/7a972d22a58de8a261a738d9a1e5fb51f9608ede/lib/sneakers/workergroup.rb#L28
|
29
|
+
def call
|
30
|
+
case activejob_workers_strategy
|
31
|
+
when :only then activejob_workers
|
32
|
+
when :exclude then sneakers_workers
|
33
|
+
when :include then sneakers_workers + activejob_workers
|
34
|
+
else
|
35
|
+
raise "Unknown activejob_workers_strategy '#{activejob_workers_strategy}'"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# For cleaner output on inspecting Sneakers::Worker::Classes in console.
|
40
|
+
def inspect
|
41
|
+
{
|
42
|
+
sneakers_workers: sneakers_workers,
|
43
|
+
activejob_workers: activejob_workers
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
metadata
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: advanced-sneakers-activejob
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rustam Sharshenov
|
8
|
+
- Vlad Bokov
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2020-03-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activejob
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '4.2'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '4.2'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: sneakers
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '2.7'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '2.7'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: bundler
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: pry-byebug
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: rabbitmq_http_api_client
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - "~>"
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '1.13'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '1.13'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: rails
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '4.2'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '4.2'
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: rake
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
type: :development
|
106
|
+
prerelease: false
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
name: rspec
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
description: Advanced Sneakers adapter for ActiveJob
|
127
|
+
email:
|
128
|
+
- rustam@sharshenov.com
|
129
|
+
- vlad@lunatic.cat
|
130
|
+
executables: []
|
131
|
+
extensions: []
|
132
|
+
extra_rdoc_files: []
|
133
|
+
files:
|
134
|
+
- ".gitignore"
|
135
|
+
- ".rspec"
|
136
|
+
- ".rubocop.yml"
|
137
|
+
- ".travis.yml"
|
138
|
+
- CHANGELOG.md
|
139
|
+
- Gemfile
|
140
|
+
- LICENSE.txt
|
141
|
+
- README.md
|
142
|
+
- Rakefile
|
143
|
+
- advanced-sneakers-activejob.gemspec
|
144
|
+
- lib/active_job/queue_adapters/advanced_sneakers_adapter.rb
|
145
|
+
- lib/advanced/sneakers/activejob.rb
|
146
|
+
- lib/advanced_sneakers_activejob.rb
|
147
|
+
- lib/advanced_sneakers_activejob/active_job_patch.rb
|
148
|
+
- lib/advanced_sneakers_activejob/configuration.rb
|
149
|
+
- lib/advanced_sneakers_activejob/content_type.rb
|
150
|
+
- lib/advanced_sneakers_activejob/errors.rb
|
151
|
+
- lib/advanced_sneakers_activejob/exponential_backoff.rb
|
152
|
+
- lib/advanced_sneakers_activejob/handler.rb
|
153
|
+
- lib/advanced_sneakers_activejob/publisher.rb
|
154
|
+
- lib/advanced_sneakers_activejob/railtie.rb
|
155
|
+
- lib/advanced_sneakers_activejob/tasks.rb
|
156
|
+
- lib/advanced_sneakers_activejob/version.rb
|
157
|
+
- lib/advanced_sneakers_activejob/workers_registry.rb
|
158
|
+
homepage: https://github.com/veeqo/advanced-sneakers-activejob
|
159
|
+
licenses:
|
160
|
+
- MIT
|
161
|
+
metadata:
|
162
|
+
homepage_uri: https://github.com/veeqo/advanced-sneakers-activejob
|
163
|
+
source_code_uri: https://github.com/veeqo/advanced-sneakers-activejob
|
164
|
+
changelog_uri: https://github.com/veeqo/advanced-sneakers-activejob/blob/master/CHANGELOG.md
|
165
|
+
post_install_message:
|
166
|
+
rdoc_options: []
|
167
|
+
require_paths:
|
168
|
+
- lib
|
169
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
175
|
+
requirements:
|
176
|
+
- - ">="
|
177
|
+
- !ruby/object:Gem::Version
|
178
|
+
version: '0'
|
179
|
+
requirements: []
|
180
|
+
rubygems_version: 3.0.6
|
181
|
+
signing_key:
|
182
|
+
specification_version: 4
|
183
|
+
summary: Advanced Sneakers adapter for ActiveJob
|
184
|
+
test_files: []
|