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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2ae17fa99114e9d196dfb9c0597250dd67fabb59bbf5bd687a16e0ec08876457
4
- data.tar.gz: 655068e7718b730183051637ba4a9532e906655a811721df371eb7ba2b68ad41
3
+ metadata.gz: 818f3505d0e58ddf098ed2884db036a0e16019e04641e6c8ca457e8c48850910
4
+ data.tar.gz: 374b620fbb9092a3c24547d6c036b6da0973e0059ce910cca559e7dae59eb028
5
5
  SHA512:
6
- metadata.gz: 354d6f85a842408cb17e214b20ab5a0eb25815ce2e54ba727e8a4ac3309cd220e6a2e33c777f2bf13a97381d41777d8e50d63d1ab95a2b4c0a67680b3f992330
7
- data.tar.gz: 9a3552f273d1da9ace5d808aab2b519f1c21d2dbebcecb72c272c699f8914b2381e44e4c8e023f939391aa86f6648bc8673a1a763a705d8f4141024151c8437f
6
+ metadata.gz: fc3de6bfbd43c907c242ac65104e7cd91523930f209e216511ad4877dbd0ac836d51d291c71eb776559eb1b5d3667236d36f6641a5255652f7be33dbb07bc820
7
+ data.tar.gz: 6dcc9e3426e5592738c44d3f9feb658ba57e17713ed185bf3f76b9cc6df7d2886064abd031efc8a3913c9b41c5dfff89671f59809af42c42b018e4b5d1d40bfb
@@ -3,17 +3,17 @@ dist: xenial
3
3
  language: ruby
4
4
  cache: bundler
5
5
  rvm:
6
- - 2.4.9
7
- - 2.5.7
8
- - 2.6.5
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.4.9
16
- env: RAILS_VERSION="~> 6.0.2"
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:
@@ -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 https://github.com/veeqo/advanced-sneakers-activejob/pull/3
4
- ### Support for custom adapter per job https://github.com/veeqo/advanced-sneakers-activejob/pull/4
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 https://github.com/veeqo/advanced-sneakers-activejob/pull/2
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` https://github.com/veeqo/advanced-sneakers-activejob/pull/1
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 30 seconds in case if there are unrouted messages, but it does not provide any guarantees.
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/)
@@ -27,6 +27,7 @@ Gem::Specification.new do |spec|
27
27
  spec.require_paths = ['lib']
28
28
 
29
29
  spec.add_dependency 'activejob', '>= 4.2'
30
+ spec.add_dependency 'bunny-publisher', '~> 0.1'
30
31
  spec.add_dependency 'sneakers', '~> 2.7'
31
32
 
32
33
  spec.add_development_dependency 'bundler'
@@ -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
- AdvancedSneakersActiveJob.publisher.publish_delayed(*publish_params(job).tap { |params| params.last[:delay] = delay })
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, AdvancedSneakersActiveJob.config.sneakers)
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.publisher.publish_delayed(message, params)
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
- # 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
4
+ class Publisher < ::BunnyPublisher::Base
5
+ include ::BunnyPublisher::Mandatory
8
6
 
9
- delegate :sneakers, :handle_unrouted_messages, :delayed_queue_prefix,
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
- 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: 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 ensure_connection!
56
- @mutex.synchronize do
57
- unless connected?
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 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
-
23
+ def on_message_return(return_info, properties, message)
107
24
  if config_handle_unrouted_messages
108
- setup_routing_and_republish_message(params)
25
+ super
109
26
  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
- ::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 $WORKERS=AdvancedSneakersActiveJob::FooConsumer for processing of :foo queue)'
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AdvancedSneakersActiveJob
4
- VERSION = '0.2.3'
4
+ VERSION = '0.3.4'
5
5
  end
@@ -54,7 +54,17 @@ module AdvancedSneakersActiveJob
54
54
  @activejob_workers
55
55
  end
56
56
 
57
- private
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.2.3
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-04-06 00:00:00.000000000 Z
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