advanced-sneakers-activejob 0.2.3 → 0.3.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +5 -5
- data/CHANGELOG.md +50 -4
- data/README.md +22 -2
- data/advanced-sneakers-activejob.gemspec +1 -0
- data/lib/active_job/queue_adapters/advanced_sneakers_adapter.rb +5 -2
- data/lib/advanced_sneakers_activejob.rb +14 -2
- data/lib/advanced_sneakers_activejob/configuration.rb +7 -0
- data/lib/advanced_sneakers_activejob/delayed_publisher.rb +62 -0
- data/lib/advanced_sneakers_activejob/handler.rb +7 -3
- data/lib/advanced_sneakers_activejob/publisher.rb +15 -192
- data/lib/advanced_sneakers_activejob/support/locate_workers_by_queues.rb +53 -0
- data/lib/advanced_sneakers_activejob/tasks.rb +13 -1
- data/lib/advanced_sneakers_activejob/version.rb +1 -1
- data/lib/advanced_sneakers_activejob/workers_registry.rb +13 -1
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 818f3505d0e58ddf098ed2884db036a0e16019e04641e6c8ca457e8c48850910
|
4
|
+
data.tar.gz: 374b620fbb9092a3c24547d6c036b6da0973e0059ce910cca559e7dae59eb028
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fc3de6bfbd43c907c242ac65104e7cd91523930f209e216511ad4877dbd0ac836d51d291c71eb776559eb1b5d3667236d36f6641a5255652f7be33dbb07bc820
|
7
|
+
data.tar.gz: 6dcc9e3426e5592738c44d3f9feb658ba57e17713ed185bf3f76b9cc6df7d2886064abd031efc8a3913c9b41c5dfff89671f59809af42c42b018e4b5d1d40bfb
|
data/.travis.yml
CHANGED
@@ -3,17 +3,17 @@ dist: xenial
|
|
3
3
|
language: ruby
|
4
4
|
cache: bundler
|
5
5
|
rvm:
|
6
|
-
- 2.
|
7
|
-
- 2.
|
8
|
-
- 2.
|
6
|
+
- 2.5.8
|
7
|
+
- 2.6.6
|
8
|
+
- 2.7.1
|
9
9
|
env:
|
10
10
|
- RAILS_VERSION="~> 4.2.11"
|
11
11
|
- RAILS_VERSION="~> 5.2.4"
|
12
12
|
- RAILS_VERSION="~> 6.0.2"
|
13
13
|
jobs:
|
14
14
|
exclude:
|
15
|
-
- rvm: 2.
|
16
|
-
env: RAILS_VERSION="~>
|
15
|
+
- rvm: 2.7.1
|
16
|
+
env: RAILS_VERSION="~> 4.2.11"
|
17
17
|
|
18
18
|
before_install: gem install bundler -v 1.17.3
|
19
19
|
before_script:
|
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,53 @@
|
|
1
|
+
## Changes Between 0.3.3 and 0.3.4
|
2
|
+
|
3
|
+
### [Support for wildcards to run ActiveJob consumers by queues](https://github.com/veeqo/advanced-sneakers-activejob/pull/10)
|
4
|
+
|
5
|
+
Works with `sneakers:active_job` task only!
|
6
|
+
|
7
|
+
```sh
|
8
|
+
QUEUES=mailers,index.*,telemetery.# rake sneakers:active_job
|
9
|
+
```
|
10
|
+
|
11
|
+
## Changes Between 0.3.2 and 0.3.3
|
12
|
+
|
13
|
+
### [Add ability to run ActiveJob consumers by queues](https://github.com/veeqo/advanced-sneakers-activejob/pull/9)
|
14
|
+
|
15
|
+
Works with `sneakers:active_job` task only!
|
16
|
+
|
17
|
+
```sh
|
18
|
+
QUEUES=mailers,default rake sneakers:active_job
|
19
|
+
```
|
20
|
+
|
21
|
+
## Changes Between 0.3.1 and 0.3.2
|
22
|
+
|
23
|
+
### [Add ability to run specified ActiveJob queues consumers](https://github.com/veeqo/advanced-sneakers-activejob/pull/8)
|
24
|
+
|
25
|
+
Sneakers allows to specify consumer classes to run:
|
26
|
+
|
27
|
+
```sh
|
28
|
+
WORKERS=MyConsumer rake sneakers:run
|
29
|
+
```
|
30
|
+
|
31
|
+
Now it works for ActiveJob queues consumers as well:
|
32
|
+
|
33
|
+
```sh
|
34
|
+
WORKERS=AdvancedSneakersActiveJob::MailersConsumer rake sneakers:run
|
35
|
+
```
|
36
|
+
|
37
|
+
## Changes Between 0.3.0 and 0.3.1
|
38
|
+
|
39
|
+
### [Restore Sneakers::Worker::Classes methods](https://github.com/veeqo/advanced-sneakers-activejob/pull/6)
|
40
|
+
|
41
|
+
## Changes Between 0.2.3 and 0.3.0
|
42
|
+
|
43
|
+
This release does not change the observed behavior, but replaces the publisher with completely new implementation.
|
44
|
+
|
45
|
+
### Reusable parts of publisher are extracted to [bunny-publisher](https://github.com/veeqo/bunny-publisher)
|
46
|
+
|
1
47
|
## Changes Between 0.2.2 and 0.2.3
|
2
48
|
|
3
|
-
### Refactored support for ActiveJob prefix
|
4
|
-
### Support for custom adapter per job
|
49
|
+
### [Refactored support for ActiveJob prefix](https://github.com/veeqo/advanced-sneakers-activejob/pull/3)
|
50
|
+
### [Support for custom adapter per job](https://github.com/veeqo/advanced-sneakers-activejob/pull/4)
|
5
51
|
|
6
52
|
## Changes Between 0.2.1 and 0.2.2
|
7
53
|
|
@@ -9,13 +55,13 @@ Cleanup of `puts` and logger mistakenly introduced in previous version
|
|
9
55
|
|
10
56
|
## Changes Between 0.2.0 and 0.2.1
|
11
57
|
|
12
|
-
### Support for ActiveJob prefix
|
58
|
+
### [Support for ActiveJob prefix](https://github.com/veeqo/advanced-sneakers-activejob/pull/2)
|
13
59
|
|
14
60
|
Fixed worker class name in rake task description
|
15
61
|
|
16
62
|
## Changes Between 0.1.0 and 0.2.0
|
17
63
|
|
18
|
-
### `message_options`
|
64
|
+
### [`message_options`](https://github.com/veeqo/advanced-sneakers-activejob/pull/1)
|
19
65
|
|
20
66
|
Customizable options for message publishing (`routing_key`, `headers`, etc)
|
21
67
|
|
data/README.md
CHANGED
@@ -34,11 +34,22 @@ Or install it yourself as:
|
|
34
34
|
config.active_job.queue_adapter = :advanced_sneakers
|
35
35
|
```
|
36
36
|
|
37
|
-
Run worker
|
37
|
+
Run worker for all queues of ActiveJob
|
38
38
|
```sh
|
39
39
|
rake sneakers:active_job
|
40
40
|
```
|
41
41
|
|
42
|
+
Run worker for picked queues of ActiveJob
|
43
|
+
```sh
|
44
|
+
QUEUES=mailers,foo,bar rake sneakers:active_job
|
45
|
+
```
|
46
|
+
|
47
|
+
Wildcards are supported for queues names with "words" (separator is `.`). Algorithm is similar to the way the [topic exchange matches routing keys](https://www.rabbitmq.com/tutorials/tutorial-five-python.html). `*` (star) substitutes for exactly one word. `#` (hash) substitutes for zero or more words
|
48
|
+
|
49
|
+
```sh
|
50
|
+
QUEUES=mailers,index.*,telemetery.# rake sneakers:active_job
|
51
|
+
```
|
52
|
+
|
42
53
|
## Unrouted messages
|
43
54
|
|
44
55
|
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:
|
@@ -49,7 +60,7 @@ If message is published before routing has been configured (e.g. by consumer), i
|
|
49
60
|
|
50
61
|
There is a setting `handle_unrouted_messages` in [configuration](#configuration) to disable this behavior. If it is disabled, publisher will only log unrouted messages.
|
51
62
|
|
52
|
-
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
|
63
|
+
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 5 seconds in case if there are unrouted messages, but it does not provide any guarantees.
|
53
64
|
|
54
65
|
**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.
|
55
66
|
|
@@ -152,12 +163,19 @@ AdvancedSneakersActiveJob.configure do |config|
|
|
152
163
|
|
153
164
|
# Custom sneakers configuration for ActiveJob publisher & runner
|
154
165
|
config.sneakers = {
|
166
|
+
connection: Bunny.new('CUSTOM_URL', with: { other: 'options' }),
|
155
167
|
exchange: 'activejob',
|
156
168
|
handler: AdvancedSneakersActiveJob::Handler
|
157
169
|
}
|
158
170
|
|
159
171
|
# Define custom delay for retries, but remember - each unique delay leads to new queue on RabbitMQ side
|
160
172
|
config.retry_delay_proc = ->(count) { AdvancedSneakersActiveJob::EXPONENTIAL_BACKOFF[count] }
|
173
|
+
|
174
|
+
# Connection for publisher (fallbacks to connection of consumers)
|
175
|
+
config.publish_connection = Bunny.new('CUSTOM_URL', with: { other: 'options' })
|
176
|
+
|
177
|
+
# Unrouted messages republish requires extra connection and will try to "clone" publish_connection unless it is provided
|
178
|
+
config.republish_connection = Bunny.new('CUSTOM_URL', with: { other: 'options' })
|
161
179
|
end
|
162
180
|
```
|
163
181
|
|
@@ -168,3 +186,5 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/veeqo/
|
|
168
186
|
## License
|
169
187
|
|
170
188
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
189
|
+
|
190
|
+
## Sponsored by [Veeqo](https://veeqo.com/)
|
@@ -26,7 +26,10 @@ module ActiveJob
|
|
26
26
|
delay = AdvancedSneakersActiveJob.config.delay_proc.call(timestamp).to_i
|
27
27
|
|
28
28
|
if delay.positive?
|
29
|
-
|
29
|
+
message, options = publish_params(job)
|
30
|
+
options[:headers] = { 'delay' => delay.to_i } # do not use x- prefix because headers exchanges ignore such headers
|
31
|
+
|
32
|
+
AdvancedSneakersActiveJob.delayed_publisher.publish(message, options)
|
30
33
|
else
|
31
34
|
enqueue(job)
|
32
35
|
end
|
@@ -38,7 +41,7 @@ module ActiveJob
|
|
38
41
|
@monitor.synchronize do
|
39
42
|
[
|
40
43
|
Sneakers::ContentType.serialize(job.serialize, AdvancedSneakersActiveJob::CONTENT_TYPE),
|
41
|
-
build_publish_params(job)
|
44
|
+
build_publish_params(job).merge(content_type: AdvancedSneakersActiveJob::CONTENT_TYPE)
|
42
45
|
]
|
43
46
|
end
|
44
47
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'active_support'
|
4
4
|
require 'active_support/core_ext'
|
5
|
+
require 'bunny_publisher'
|
5
6
|
|
6
7
|
require 'sneakers'
|
7
8
|
require 'advanced_sneakers_activejob/workers_registry'
|
@@ -15,6 +16,7 @@ require 'advanced_sneakers_activejob/handler'
|
|
15
16
|
require 'advanced_sneakers_activejob/configuration'
|
16
17
|
require 'advanced_sneakers_activejob/errors'
|
17
18
|
require 'advanced_sneakers_activejob/publisher'
|
19
|
+
require 'advanced_sneakers_activejob/delayed_publisher'
|
18
20
|
require 'advanced_sneakers_activejob/active_job_patch'
|
19
21
|
require 'advanced_sneakers_activejob/railtie' if defined?(::Rails::Railtie)
|
20
22
|
require 'active_job/queue_adapters/advanced_sneakers_adapter'
|
@@ -38,13 +40,17 @@ module AdvancedSneakersActiveJob
|
|
38
40
|
klass = Class.new(ActiveJob::QueueAdapters::AdvancedSneakersAdapter::JobWrapper)
|
39
41
|
const_set(name, klass)
|
40
42
|
klass.include Sneakers::Worker
|
41
|
-
klass.from_queue(queue_name,
|
43
|
+
klass.from_queue(queue_name, config.sneakers)
|
42
44
|
|
43
45
|
klass
|
44
46
|
end
|
45
47
|
|
46
48
|
def publisher
|
47
|
-
@publisher ||= AdvancedSneakersActiveJob::Publisher.new
|
49
|
+
@publisher ||= AdvancedSneakersActiveJob::Publisher.new(config.publisher_config)
|
50
|
+
end
|
51
|
+
|
52
|
+
def delayed_publisher
|
53
|
+
@delayed_publisher ||= AdvancedSneakersActiveJob::DelayedPublisher.new(config.publisher_config)
|
48
54
|
end
|
49
55
|
|
50
56
|
# Based on ActiveSupport::Inflector#parameterize
|
@@ -62,5 +68,11 @@ module AdvancedSneakersActiveJob
|
|
62
68
|
|
63
69
|
[parameterized_string, 'consumer'].join('_').classify
|
64
70
|
end
|
71
|
+
|
72
|
+
def const_missing(name)
|
73
|
+
Sneakers::Worker::Classes.define_active_job_consumers
|
74
|
+
|
75
|
+
constants.include?(name) ? const_get(name) : super
|
76
|
+
end
|
65
77
|
end
|
66
78
|
end
|
@@ -17,6 +17,9 @@ module AdvancedSneakersActiveJob
|
|
17
17
|
config_accessor(:delayed_queue_prefix) { 'delayed' }
|
18
18
|
config_accessor(:retry_delay_proc) { ->(count) { AdvancedSneakersActiveJob::EXPONENTIAL_BACKOFF[count] } } # seconds
|
19
19
|
|
20
|
+
config_accessor(:publish_connection)
|
21
|
+
config_accessor(:republish_connection)
|
22
|
+
|
20
23
|
def sneakers
|
21
24
|
custom_config = DEFAULT_SNEAKERS_CONFIG.deep_merge(config.sneakers || {})
|
22
25
|
|
@@ -30,5 +33,9 @@ module AdvancedSneakersActiveJob
|
|
30
33
|
def sneakers=(custom)
|
31
34
|
config.sneakers = custom
|
32
35
|
end
|
36
|
+
|
37
|
+
def publisher_config
|
38
|
+
sneakers.merge(publish_connection: publish_connection, republish_connection: republish_connection)
|
39
|
+
end
|
33
40
|
end
|
34
41
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AdvancedSneakersActiveJob
|
4
|
+
# This publisher relies on TTL to keep messages in a queue.
|
5
|
+
# When TTL is reached, messages go to another exchange (see dlx_exchange_name param).
|
6
|
+
class DelayedPublisher < ::BunnyPublisher::Base
|
7
|
+
include ::BunnyPublisher::Mandatory
|
8
|
+
|
9
|
+
delegate :logger, to: :'::ActiveJob::Base'
|
10
|
+
|
11
|
+
delegate :name_prefix, :delayed_queue_prefix,
|
12
|
+
to: :'AdvancedSneakersActiveJob.config',
|
13
|
+
prefix: :config
|
14
|
+
|
15
|
+
before_publish :log_message
|
16
|
+
|
17
|
+
attr_reader :dlx_exchange_name
|
18
|
+
|
19
|
+
def initialize(exchange:, **options)
|
20
|
+
super(options.merge(exchange: [exchange, 'delayed'].join('-'), exchange_options: { type: 'headers', durable: true }))
|
21
|
+
|
22
|
+
@dlx_exchange_name = exchange
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def log_message(publisher, message, options = {})
|
28
|
+
logger.debug do
|
29
|
+
delay = options.dig(:headers, 'delay')
|
30
|
+
|
31
|
+
"Publishing <#{message}> to [#{publisher.exchange.name}] with routing_key [#{options[:routing_key]}] and delay [#{delay}]"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def declare_republish_queue(_return_info, properties, _message)
|
36
|
+
delay = properties.headers.fetch('delay')
|
37
|
+
|
38
|
+
queue_name = delayed_queue_name(delay: delay)
|
39
|
+
|
40
|
+
queue_arguments = {
|
41
|
+
'x-queue-mode' => 'lazy', # tell RabbitMQ not to use RAM for this queue as it won't be consumed
|
42
|
+
'x-message-ttl' => delay * 1000, # make messages die after requested time
|
43
|
+
'x-dead-letter-exchange' => dlx_exchange_name # dead messages go to original exchange and then routed to proper queues
|
44
|
+
}
|
45
|
+
|
46
|
+
logger.debug { "Creating delayed queue [#{queue_name}]" }
|
47
|
+
|
48
|
+
republish_channel.queue(queue_name, durable: true, arguments: queue_arguments)
|
49
|
+
end
|
50
|
+
|
51
|
+
def declare_republish_queue_binding(queue, _return_info, properties, _message)
|
52
|
+
queue.bind(republish_exchange, arguments: { delay: properties.headers.fetch('delay') })
|
53
|
+
end
|
54
|
+
|
55
|
+
def delayed_queue_name(delay:)
|
56
|
+
[
|
57
|
+
::ActiveJob::Base.queue_name_prefix,
|
58
|
+
[config_delayed_queue_prefix, delay].join(':')
|
59
|
+
].compact.join(::ActiveJob::Base.queue_name_delimiter)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -5,11 +5,10 @@ module AdvancedSneakersActiveJob
|
|
5
5
|
class Handler < Sneakers::Handlers::Oneshot
|
6
6
|
def error(delivery_info, properties, message, error)
|
7
7
|
params = properties.to_h
|
8
|
-
params[:headers] = patch_headers(params[:headers], delivery_info, error)
|
8
|
+
params[:headers] = patch_headers(params[:headers] || {}, delivery_info, error)
|
9
9
|
params[:routing_key] = delivery_info.routing_key
|
10
|
-
params[:delay] = calculate_delay(params[:headers], delivery_info)
|
11
10
|
|
12
|
-
AdvancedSneakersActiveJob.
|
11
|
+
AdvancedSneakersActiveJob.delayed_publisher.publish(message, params)
|
13
12
|
|
14
13
|
acknowledge(delivery_info, properties, message)
|
15
14
|
end
|
@@ -23,6 +22,7 @@ module AdvancedSneakersActiveJob
|
|
23
22
|
|
24
23
|
track_error_in_headers(headers, error)
|
25
24
|
track_death_in_headers(headers, queue, exchange, routing_key)
|
25
|
+
set_delay_in_headers(headers, delivery_info)
|
26
26
|
|
27
27
|
headers
|
28
28
|
end
|
@@ -41,6 +41,10 @@ module AdvancedSneakersActiveJob
|
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
|
+
def set_delay_in_headers(headers, delivery_info)
|
45
|
+
headers['delay'] = calculate_delay(headers, delivery_info)
|
46
|
+
end
|
47
|
+
|
44
48
|
def build_death_row(queue, exchange, routing_key)
|
45
49
|
{
|
46
50
|
'count' => 1,
|
@@ -1,210 +1,33 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module AdvancedSneakersActiveJob
|
4
|
-
|
5
|
-
|
6
|
-
class Publisher
|
7
|
-
WAIT_FOR_UNROUTED_MESSAGES_AT_EXIT_TIMEOUT = 30
|
4
|
+
class Publisher < ::BunnyPublisher::Base
|
5
|
+
include ::BunnyPublisher::Mandatory
|
8
6
|
|
9
|
-
|
10
|
-
to: :'AdvancedSneakersActiveJob.config', prefix: :config
|
7
|
+
before_publish :log_message
|
11
8
|
|
12
|
-
delegate :logger, to: :'ActiveJob::Base'
|
9
|
+
delegate :logger, to: :'::ActiveJob::Base'
|
13
10
|
|
14
|
-
|
15
|
-
|
16
|
-
|
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: nil, 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, delay:, routing_key: nil, 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
|
11
|
+
delegate :handle_unrouted_messages,
|
12
|
+
to: :'AdvancedSneakersActiveJob.config',
|
13
|
+
prefix: :config
|
52
14
|
|
53
15
|
private
|
54
16
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
start_connections!
|
59
|
-
create_channels!
|
60
|
-
configure_exchanges!
|
61
|
-
end
|
17
|
+
def log_message(publisher, message, options = {})
|
18
|
+
logger.debug do
|
19
|
+
"Publishing <#{message}> to [#{publisher.exchange.name}] with routing_key [#{options[:routing_key]}]"
|
62
20
|
end
|
63
21
|
end
|
64
22
|
|
65
|
-
def
|
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
|
-
|
23
|
+
def on_message_return(return_info, properties, message)
|
107
24
|
if config_handle_unrouted_messages
|
108
|
-
|
25
|
+
super
|
109
26
|
else
|
110
|
-
logger.warn
|
111
|
-
|
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
|
-
::ActiveJob::Base.queue_name_prefix,
|
174
|
-
[config_delayed_queue_prefix, delay].join(':')
|
175
|
-
].compact.join(::ActiveJob::Base.queue_name_delimiter)
|
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
|
27
|
+
logger.warn do
|
28
|
+
"Message is not routed! #{{ message: message, return_info: return_info, properties: properties }}"
|
29
|
+
end
|
188
30
|
end
|
189
31
|
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
32
|
end
|
210
33
|
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AdvancedSneakersActiveJob
|
4
|
+
module Support
|
5
|
+
class LocateWorkersByQueues
|
6
|
+
def initialize(queues)
|
7
|
+
@queues = queues.uniq.reject(&:blank?)
|
8
|
+
@queues_without_workers = []
|
9
|
+
@workers = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def call
|
13
|
+
detect_workers_for_queues!
|
14
|
+
ensure_all_workers_found!
|
15
|
+
|
16
|
+
@workers.uniq
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def ensure_all_workers_found!
|
22
|
+
return if @queues_without_workers.empty?
|
23
|
+
|
24
|
+
raise("Missing workers for queues: #{@queues_without_workers.join(', ')}")
|
25
|
+
end
|
26
|
+
|
27
|
+
def all_workers
|
28
|
+
@all_workers ||= Sneakers::Worker::Classes.activejob_workers
|
29
|
+
end
|
30
|
+
|
31
|
+
def detect_workers_for_queues!
|
32
|
+
@queues.each do |queue|
|
33
|
+
matching_workers = all_workers.select { |klass| klass.queue_name.match?(queue_regex(queue)) }
|
34
|
+
|
35
|
+
if matching_workers.any?
|
36
|
+
@workers += matching_workers
|
37
|
+
else
|
38
|
+
@queues_without_workers << queue
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# https://www.rabbitmq.com/tutorials/tutorial-five-python.html
|
44
|
+
def queue_regex(queue)
|
45
|
+
regex = Regexp.escape(queue)
|
46
|
+
.gsub(/\A\\\*|(\.)\\\*/, '\1[^\.]+') # "*" (star) substitutes for exactly one word
|
47
|
+
.sub('\.\#', '(\.[^\.]+)*') # "#" (hash) substitutes for zero or more words
|
48
|
+
|
49
|
+
Regexp.new(['\A', regex, '\z'].join)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -5,10 +5,12 @@ require 'sneakers/tasks'
|
|
5
5
|
task :environment
|
6
6
|
|
7
7
|
namespace :sneakers do
|
8
|
-
desc 'Start work for ActiveJob only (set $
|
8
|
+
desc 'Start work for ActiveJob only (set $QUEUES=foo,bar.baz for processing of "foo" and "bar.baz" queues)'
|
9
9
|
task :active_job do
|
10
10
|
Rake::Task['environment'].invoke
|
11
11
|
|
12
|
+
populate_workers_by_queues if ENV['WORKERS'].blank? && ENV['QUEUES'].present?
|
13
|
+
|
12
14
|
# Enforsing ActiveJob-only workers
|
13
15
|
AdvancedSneakersActiveJob.configure { |c| c.activejob_workers_strategy = :only }
|
14
16
|
|
@@ -18,4 +20,14 @@ namespace :sneakers do
|
|
18
20
|
|
19
21
|
Rake::Task['sneakers:run'].invoke
|
20
22
|
end
|
23
|
+
|
24
|
+
def populate_workers_by_queues
|
25
|
+
require 'advanced_sneakers_activejob/support/locate_workers_by_queues'
|
26
|
+
::Rails.application.eager_load!
|
27
|
+
|
28
|
+
queues = ENV['QUEUES'].split(',')
|
29
|
+
workers = AdvancedSneakersActiveJob::Support::LocateWorkersByQueues.new(queues).call
|
30
|
+
|
31
|
+
ENV['WORKERS'] = workers.map(&:name).join(',')
|
32
|
+
end
|
21
33
|
end
|
@@ -54,7 +54,17 @@ module AdvancedSneakersActiveJob
|
|
54
54
|
@activejob_workers
|
55
55
|
end
|
56
56
|
|
57
|
-
|
57
|
+
def method_missing(method_name, *args, &block)
|
58
|
+
if call.respond_to?(method_name)
|
59
|
+
call.send(method_name, *args, &block)
|
60
|
+
else
|
61
|
+
super
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def respond_to_missing?(method_name, include_private = false)
|
66
|
+
call.respond_to?(method_name) || super
|
67
|
+
end
|
58
68
|
|
59
69
|
def define_active_job_consumers
|
60
70
|
active_job_classes_with_matching_adapter.each do |worker|
|
@@ -62,6 +72,8 @@ module AdvancedSneakersActiveJob
|
|
62
72
|
end
|
63
73
|
end
|
64
74
|
|
75
|
+
private
|
76
|
+
|
65
77
|
def active_job_classes_with_matching_adapter
|
66
78
|
([ActiveJob::Base] + ActiveJob::Base.descendants).select do |klass|
|
67
79
|
klass.queue_adapter == ::ActiveJob::QueueAdapters::AdvancedSneakersAdapter ||
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: advanced-sneakers-activejob
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rustam Sharshenov
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2020-
|
12
|
+
date: 2020-06-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activejob
|
@@ -25,6 +25,20 @@ dependencies:
|
|
25
25
|
- - ">="
|
26
26
|
- !ruby/object:Gem::Version
|
27
27
|
version: '4.2'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: bunny-publisher
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0.1'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0.1'
|
28
42
|
- !ruby/object:Gem::Dependency
|
29
43
|
name: sneakers
|
30
44
|
requirement: !ruby/object:Gem::Requirement
|
@@ -147,11 +161,13 @@ files:
|
|
147
161
|
- lib/advanced_sneakers_activejob/active_job_patch.rb
|
148
162
|
- lib/advanced_sneakers_activejob/configuration.rb
|
149
163
|
- lib/advanced_sneakers_activejob/content_type.rb
|
164
|
+
- lib/advanced_sneakers_activejob/delayed_publisher.rb
|
150
165
|
- lib/advanced_sneakers_activejob/errors.rb
|
151
166
|
- lib/advanced_sneakers_activejob/exponential_backoff.rb
|
152
167
|
- lib/advanced_sneakers_activejob/handler.rb
|
153
168
|
- lib/advanced_sneakers_activejob/publisher.rb
|
154
169
|
- lib/advanced_sneakers_activejob/railtie.rb
|
170
|
+
- lib/advanced_sneakers_activejob/support/locate_workers_by_queues.rb
|
155
171
|
- lib/advanced_sneakers_activejob/tasks.rb
|
156
172
|
- lib/advanced_sneakers_activejob/version.rb
|
157
173
|
- lib/advanced_sneakers_activejob/workers_registry.rb
|