rabbitek 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: 48e1e5b2bc2ea1d5a8de3e5e6e35ec5f8ef3e6961a99987f544f87dfc043d48f
4
- data.tar.gz: b413bb3111ab56d0cf135bc5af3c6a754c675d945cf5919932881e57f31c6d94
2
+ SHA1:
3
+ metadata.gz: abffed1ec6fb3e1837dc3f75bd819742e1350935
4
+ data.tar.gz: 72f165622c0f6346dfacb2666db46e2a0cc787b1
5
5
  SHA512:
6
- metadata.gz: 04dc3c2f72c7157d9d7eb987aa91c9e97fcbd588c2a06079e37db8fba3a5d2ef202cb52337cc9b7065d4ec711e1976a132efbd53dce5fba8f01a037b1afe9585
7
- data.tar.gz: c5b4d5946c8ebf3a8f374de46ec73ce40504d32a0fed75329fc568e027b12697d19c2c2dab1562da03a9ea60abce242c9ebe907c96a8f66b5f9963fbcff0aced
6
+ metadata.gz: a228c331c23fe8e80d9ace1a4c7847521b79a562cc14f01f413ea419337afb08e979f1673508b247dabb2e0565571f6e2b2ae0113d6fc60e51f50fa1cd29c00f
7
+ data.tar.gz: ce5ad374f81f11b23345f6105261af2204ad4c19421d97c6345e35ba8f4d3ccb169697212e02ef866c057865a64aa3d2f42950ffea6f198cef54337e7569a72a
data/.rubocop.yml CHANGED
@@ -3,7 +3,7 @@ AllCops:
3
3
  - 'vendor/**/*'
4
4
  - 'spec/fixtures/**/*'
5
5
  - 'tmp/**/*'
6
- TargetRubyVersion: 2.4
6
+ TargetRubyVersion: 2.5
7
7
 
8
8
  Metrics/LineLength:
9
9
  Max: 120
data/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ ## v0.2.0
2
+
3
+ ### Breaking changes
4
+
5
+ * Retry queue now uses direct exchange with routing (prevent message duplicating on retry)
6
+ * Change API to be much more universal
7
+ * Auto ACK on job success
8
+
9
+ ### Other
10
+
11
+ * Add batching
12
+
13
+ ## v0.1.0
14
+
15
+ * Initial version
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rabbitek (0.1.0)
4
+ rabbitek (0.1.2)
5
5
  activesupport (> 3.0)
6
6
  bunny (~> 2.11.0)
7
7
  oj (~> 3.6)
@@ -11,7 +11,7 @@ PATH
11
11
  GEM
12
12
  remote: https://rubygems.org/
13
13
  specs:
14
- activesupport (5.2.1)
14
+ activesupport (5.2.2)
15
15
  concurrent-ruby (~> 1.0, >= 1.0.2)
16
16
  i18n (>= 0.7, < 2)
17
17
  minitest (~> 5.1)
@@ -20,13 +20,13 @@ GEM
20
20
  ast (2.4.0)
21
21
  bunny (2.11.0)
22
22
  amq-protocol (~> 2.3.0)
23
- concurrent-ruby (1.0.5)
23
+ concurrent-ruby (1.1.3)
24
24
  diff-lcs (1.3)
25
25
  i18n (1.1.1)
26
26
  concurrent-ruby (~> 1.0)
27
27
  jaro_winkler (1.5.1)
28
28
  minitest (5.11.3)
29
- oj (3.7.0)
29
+ oj (3.7.4)
30
30
  opentracing (0.4.3)
31
31
  parallel (1.12.1)
32
32
  parser (2.5.1.2)
@@ -73,4 +73,4 @@ DEPENDENCIES
73
73
  rubocop (~> 0.58.0)
74
74
 
75
75
  BUNDLED WITH
76
- 1.16.4
76
+ 1.17.1
data/README.md CHANGED
@@ -50,8 +50,12 @@ and create `perform` method same way as on example:
50
50
 
51
51
  rabbit_options config_file: 'config/rabbitek.yml'
52
52
 
53
- def perform(payload, delivery_info, properties)
54
- ack!(delivery_info)
53
+ def perform(message)
54
+ puts "Payload: #{message.payload}"
55
+ puts "Delivery Info: "#{message.delivery_info}"
56
+ puts "Properties: "#{message.properties}"
57
+
58
+ # Mesage will be automatically acked unless exception is raised
55
59
  end
56
60
  end
57
61
  ```
@@ -64,13 +68,25 @@ bundle exec rabbitek
64
68
 
65
69
  You can schedule jobs e.g.: `ExampleCustomer.perform_async(some: :payload)`
66
70
 
71
+ ### Batching
72
+
73
+ ```
74
+ class ExampleConsumer
75
+ include Rabbitek::Consumer
76
+
77
+ rabbit_options config_file: 'config/rabbitek.yml', batch: 1000
78
+
79
+ # When batch is defined, the perform method will have batch of up to N messages yielded.
80
+ def perform(messages)
81
+ end
82
+ end
83
+ ```
67
84
 
68
85
  ## Roadmap
69
86
 
70
87
  * more tests!
71
88
  * dead queue
72
89
  * CRON jobs
73
- * job batching (run consumer with e.g. max 100 messages at once)
74
90
  * extended docs and how to
75
91
  * prometheus metrics
76
92
 
data/lib/rabbitek/cli.rb CHANGED
@@ -4,13 +4,15 @@ require 'slop'
4
4
  require 'yaml'
5
5
 
6
6
  require_relative './cli/signal_handlers'
7
+ require_relative './loggable'
8
+
7
9
  require 'rabbitek'
8
10
 
9
11
  module Rabbitek
10
12
  ##
11
13
  # Rabbitek server CLI
12
14
  class CLI
13
- include Loggable
15
+ include ::Rabbitek::Loggable
14
16
 
15
17
  def run
16
18
  opts
@@ -36,7 +38,7 @@ module Rabbitek
36
38
  private
37
39
 
38
40
  def start_log # rubocop:disable Metrics/AbcSize
39
- info "Rabbit consumers '[#{configuration[:workers].map(&:to_s).join(', ')}]' started with PID #{Process.pid}"
41
+ info "Rabbit consumers '[#{configuration[:consumers].map(&:to_s).join(', ')}]' started with PID #{Process.pid}"
40
42
  info "Client hooks: [#{Rabbitek.config.client_hooks.map(&:class).join(', ')}]"
41
43
  info "Server hooks: [#{Rabbitek.config.server_hooks.map(&:class).join(', ')}]"
42
44
  end
@@ -66,7 +68,7 @@ module Rabbitek
66
68
  end
67
69
 
68
70
  def map_consumer_workers!
69
- configuration[:workers].map!(&:constantize)
71
+ configuration[:consumers].map!(&:constantize)
70
72
  end
71
73
 
72
74
  def boot_consumers
@@ -12,17 +12,15 @@ module Rabbitek
12
12
  result = nil
13
13
 
14
14
  ::OpenTracing.start_active_span(params[:routing_key], opentracing_options(params)) do |scope|
15
- begin
16
- params[:headers] ||= {}
17
- Utils::OpenTracing.inject!(scope.span, params[:headers])
15
+ params[:headers] ||= {}
16
+ Utils::OpenTracing.inject!(scope.span, params[:headers])
18
17
 
19
- result = super
20
- rescue StandardError => e
21
- raise unless scope.span
18
+ result = super
19
+ rescue StandardError => e
20
+ raise unless scope.span
22
21
 
23
- Utils::OpenTracing.log_error(scope.span, e)
24
- raise
25
- end
22
+ Utils::OpenTracing.log_error(scope.span, e)
23
+ raise
26
24
  end
27
25
 
28
26
  result
@@ -7,7 +7,8 @@ module Rabbitek
7
7
  DEFAULTS = {
8
8
  bunny_configuration: { hosts: 'localhost:5672', vhost: '/' },
9
9
  log_format: 'json',
10
- enable_newrelic: true
10
+ enable_newrelic: true,
11
+ logger: Logger.new(STDOUT)
11
12
  }.freeze
12
13
 
13
14
  attr_accessor(*DEFAULTS.keys)
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabbitek
4
+ ##
5
+ # A service to group messages from queue by batches.
6
+ class Batcher
7
+ def initialize(consumer)
8
+ @consumer = consumer
9
+ @batch_size = consumer.batch_size
10
+ @batch = []
11
+ end
12
+
13
+ def perform(message)
14
+ collect_batch(message)
15
+ yield(@batch)
16
+ rescue StandardError
17
+ retry_all_messages
18
+ raise
19
+ end
20
+
21
+ private
22
+
23
+ def collect_batch(message)
24
+ loop do
25
+ @batch << message
26
+ break if @batch.size >= @batch_size # stop collecting batch when maximum batch size has been reached
27
+
28
+ message = @consumer.pop_message_manually
29
+ break unless message # stop collecting batch when there are no more messages waiting
30
+ end
31
+ end
32
+
33
+ def retry_all_messages
34
+ @batch.each { |message| Rabbitek::Retryer.call(@consumer, message) }
35
+ end
36
+ end
37
+ end
@@ -25,11 +25,11 @@ module Rabbitek
25
25
  Rabbitek.logger
26
26
  end
27
27
 
28
- def parse_message(message)
29
- Utils::Oj.load(message)
28
+ def parse_payload(payload)
29
+ Utils::Oj.load(payload)
30
30
  end
31
31
 
32
- def perform(*_args)
32
+ def perform(_message)
33
33
  raise NotImplementedError
34
34
  end
35
35
 
@@ -41,11 +41,23 @@ module Rabbitek
41
41
  Thread.current[:rabbit_context][:job_id]
42
42
  end
43
43
 
44
+ def pop_message_manually
45
+ delivery_info, properties, payload = queue.pop(manual_ack: true)
46
+ return nil unless payload
47
+
48
+ Message.new(delivery_info: delivery_info, properties: properties, payload: payload)
49
+ end
50
+
51
+ def batch_size
52
+ self.class.batch
53
+ end
54
+
44
55
  module ClassMethods # rubocop:disable Style/Documentation
45
- attr_accessor :rabbit_options_hash
56
+ attr_accessor :rabbit_options_hash, :batch
46
57
 
47
58
  def rabbit_options(opts)
48
59
  self.rabbit_options_hash = default_rabbit_options(opts).with_indifferent_access.merge(opts)
60
+ self.batch = opts[:batch]
49
61
  end
50
62
 
51
63
  def perform_async(payload, opts: {}, channel: nil)
@@ -62,7 +74,7 @@ module Rabbitek
62
74
  def perform_in(time, payload, opts: {}, channel: nil)
63
75
  publisher = Publisher.new(
64
76
  Utils::RabbitObjectNames.retry_or_delayed_bind_exchange(rabbit_options_hash[:bind_exchange]),
65
- exchange_type: :fanout,
77
+ exchange_type: :direct,
66
78
  channel: channel
67
79
  )
68
80
  publish_with_publisher(publisher, payload, {
@@ -8,16 +8,16 @@ module Rabbitek
8
8
  ##
9
9
  # OpenTracing server hook
10
10
  class OpenTracing < Rabbitek::ServerHook
11
- def call(consumer, delivery_info, properties, payload)
11
+ def call(consumer, message)
12
12
  response = nil
13
13
 
14
- ::OpenTracing.start_active_span(delivery_info.routing_key, opts(delivery_info, properties)) do |scope|
15
- begin
16
- response = super
17
- rescue StandardError => e
18
- Utils::OpenTracing.log_error(scope.span, e)
19
- raise
20
- end
14
+ ::OpenTracing.start_active_span(
15
+ message.delivery_info.routing_key, opts(message.delivery_info, message.properties)
16
+ ) do |scope|
17
+ response = super
18
+ rescue StandardError => e
19
+ Utils::OpenTracing.log_error(scope.span, e)
20
+ raise
21
21
  end
22
22
 
23
23
  response
@@ -8,62 +8,12 @@ module Rabbitek
8
8
  ##
9
9
  # Hook to retry failed jobs
10
10
  class Retry < Rabbitek::ServerHook
11
- include Loggable
12
-
13
- def call(consumer, delivery_info, properties, payload)
11
+ def call(consumer, message)
14
12
  super
15
13
  rescue StandardError
16
- retry_message(consumer, payload, delivery_info, properties)
14
+ Retryer.call(consumer, message) unless consumer.batch_size
17
15
  raise
18
16
  end
19
-
20
- private
21
-
22
- def retry_message(consumer, payload, delivery_info, properties)
23
- headers = properties.headers || {}
24
- dead_headers = headers.fetch('x-death', []).last || {}
25
-
26
- retry_count = headers.fetch('x-retry-count', 0)
27
- expiration = dead_headers.fetch('original-expiration', 1000).to_i
28
-
29
- warn_log(retry_count, expiration, consumer)
30
-
31
- # acknowledge existing message
32
- consumer.ack!(delivery_info)
33
-
34
- if retry_count <= 25
35
- # Set the new expiration with an increasing factor
36
- new_expiration = expiration * 1.5
37
-
38
- # Publish to retry queue with new expiration
39
- publish_to_retry_queue(consumer, new_expiration, delivery_info, payload, retry_count)
40
- else
41
- publish_to_dead_queue
42
- end
43
- end
44
-
45
- def warn_log(retry_count, expiration, consumer)
46
- warn(
47
- message: 'Failure!',
48
- retry_count: retry_count,
49
- expiration: expiration,
50
- consumer: consumer.class.to_s,
51
- jid: consumer.jid
52
- )
53
- end
54
-
55
- def publish_to_retry_queue(consumer, new_expiration, delivery_info, payload, retry_count)
56
- consumer.retry_or_delayed_exchange.publish(
57
- payload,
58
- expiration: new_expiration.to_i,
59
- routing_key: delivery_info.routing_key,
60
- headers: { 'x-retry-count': retry_count + 1, 'x-dead-letter-routing-key': delivery_info.routing_key }
61
- )
62
- end
63
-
64
- def publish_to_dead_queue
65
- # TODO: implement dead queue
66
- end
67
17
  end
68
18
  end
69
19
  end
@@ -10,8 +10,8 @@ module Rabbitek
10
10
  class TimeTracker < Rabbitek::ServerHook
11
11
  include Loggable
12
12
 
13
- def call(consumer, delivery_info, properties, payload)
14
- info(message: 'Starting', consumer: delivery_info.routing_key, jid: consumer.jid)
13
+ def call(consumer, message)
14
+ info(message: 'Starting', consumer: message.delivery_info.routing_key, jid: consumer.jid)
15
15
 
16
16
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
17
17
 
@@ -19,7 +19,7 @@ module Rabbitek
19
19
  ensure
20
20
  info(
21
21
  message: 'Finished',
22
- consumer: delivery_info.routing_key,
22
+ consumer: message.delivery_info.routing_key,
23
23
  time: Process.clock_gettime(Process::CLOCK_MONOTONIC) - start,
24
24
  jid: consumer.jid
25
25
  )
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabbitek
4
+ ##
5
+ # A model representing message that consumer receives to process
6
+ class Message
7
+ attr_reader :payload, :properties, :delivery_info, :raw_payload
8
+
9
+ # @param [Hash] payload
10
+ # @param [Bunny::MessageProperties] properties
11
+ # @param [Bunny::DeliveryInfo] delivery_info
12
+ def initialize(payload:, properties:, delivery_info:)
13
+ @payload = Utils::Oj.load(payload)
14
+ @properties = properties
15
+ @delivery_info = delivery_info
16
+
17
+ @raw_payload = payload
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabbitek
4
+ ##
5
+ # A service to retry a failed message consuming
6
+ class Retryer
7
+ include Loggable
8
+
9
+ def self.call(*args)
10
+ new(*args).call
11
+ end
12
+
13
+ def initialize(consumer, message)
14
+ @consumer = consumer
15
+ @message = message
16
+
17
+ headers = message.properties.headers || {}
18
+ dead_headers = headers.fetch('x-death', []).last || {}
19
+
20
+ @retry_count = headers.fetch('x-retry-count', 0)
21
+ @expiration = dead_headers.fetch('original-expiration', 1000).to_i
22
+ end
23
+
24
+ def call
25
+ warn_log
26
+
27
+ # acknowledge existing message
28
+ @consumer.ack!(@message.delivery_info)
29
+
30
+ if @retry_count <= 25
31
+ # Set the new expiration with an increasing factor
32
+ @expiration *= 1.5
33
+
34
+ # Publish to retry queue with new expiration
35
+ publish_to_retry_queue
36
+ else
37
+ publish_to_dead_queue
38
+ end
39
+ end
40
+
41
+ def warn_log
42
+ warn(
43
+ message: 'Failure!',
44
+ retry_count: @retry_count,
45
+ expiration: @expiration,
46
+ consumer: @consumer.class.to_s,
47
+ jid: @consumer.jid
48
+ )
49
+ end
50
+
51
+ def publish_to_retry_queue
52
+ @consumer.retry_or_delayed_exchange.publish(
53
+ @message.raw_payload,
54
+ expiration: @expiration.to_i,
55
+ routing_key: @message.delivery_info.routing_key,
56
+ headers: { 'x-retry-count': @retry_count + 1, 'x-dead-letter-routing-key': @message.delivery_info.routing_key }
57
+ )
58
+ end
59
+
60
+ def publish_to_dead_queue
61
+ # TODO: implement dead queue
62
+ end
63
+ end
64
+ end
@@ -4,8 +4,8 @@ module Rabbitek
4
4
  ##
5
5
  # Base server hook class
6
6
  class ServerHook
7
- def call(consumer, delivery_info, properties, payload)
8
- yield(consumer, delivery_info, properties, payload)
7
+ def call(consumer, message)
8
+ yield(consumer, message)
9
9
  end
10
10
  end
11
11
  end
@@ -17,7 +17,8 @@ module Rabbitek
17
17
  setup_bindings!
18
18
 
19
19
  work_queue.subscribe(manual_ack: true) do |delivery_info, properties, payload|
20
- on_message_received(delivery_info, properties, payload)
20
+ message = Message.new(delivery_info: delivery_info, properties: properties, payload: payload)
21
+ on_message_received(message)
21
22
  end
22
23
  end
23
24
 
@@ -28,27 +29,32 @@ module Rabbitek
28
29
  def setup_bindings!
29
30
  consumers.each do |worker_class|
30
31
  work_queue.bind(work_exchange, routing_key: worker_class.to_s)
32
+ retry_or_delayed_queue.bind(retry_or_delayed_exchange, routing_key: worker_class.to_s)
31
33
  end
32
- retry_or_delayed_queue.bind(retry_or_delayed_exchange)
33
34
  end
34
35
 
35
- def on_message_received(delivery_info, properties, payload)
36
- consumer = consumer_instance(delivery_info.routing_key)
36
+ def on_message_received(message)
37
+ consumer = consumer_instance(message.delivery_info.routing_key)
37
38
  consumer.set_context
38
39
 
39
40
  hook_walker = Utils::HookWalker.new(Rabbitek.config.server_hooks)
40
41
 
41
- hook_walker.call!(consumer, delivery_info, properties, payload) do |*args|
42
+ hook_walker.call!(consumer, message) do |*args|
42
43
  run_job(*args)
43
44
  end
44
- end
45
-
46
- def run_job(consumer, delivery_info, properties, payload)
47
- consumer.perform(consumer.parse_message(payload), delivery_info, properties)
48
45
  rescue StandardError => e
49
46
  error(message: e.inspect, backtrace: e.backtrace, consumer: consumer.class, jid: consumer.jid)
50
47
  end
51
48
 
49
+ def run_job(consumer, message)
50
+ if consumer.class.batch
51
+ run_job_batched(consumer, message)
52
+ else
53
+ consumer.perform(message)
54
+ consumer.ack!(message.delivery_info)
55
+ end
56
+ end
57
+
52
58
  def consumer_instance(routing_key)
53
59
  Thread.current[:worker_classes] ||= {}
54
60
  klass = Thread.current[:worker_classes][routing_key] ||= routing_key.constantize
@@ -84,9 +90,16 @@ module Rabbitek
84
90
  def retry_or_delayed_exchange
85
91
  @retry_or_delayed_exchange ||= Utils::Common.exchange(
86
92
  channel,
87
- :fanout,
93
+ :direct,
88
94
  Utils::RabbitObjectNames.retry_or_delayed_bind_exchange(opts[:bind_exchange])
89
95
  )
90
96
  end
97
+
98
+ def run_job_batched(consumer, message)
99
+ Batcher.new(consumer).perform(message) do |batch|
100
+ consumer.perform(batch)
101
+ consumer.ack!(batch.last.delivery_info, true)
102
+ end
103
+ end
91
104
  end
92
105
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rabbitek
4
- VERSION = '0.1.1'
4
+ VERSION = '0.2.0'
5
5
  end
data/lib/rabbitek.rb CHANGED
@@ -8,6 +8,7 @@ require 'opentracing'
8
8
  require 'logger'
9
9
 
10
10
  # active_support
11
+ require 'active_support/core_ext/module/attribute_accessors'
11
12
  require 'active_support/core_ext/hash/indifferent_access'
12
13
  require 'active_support/core_ext/string/inflections'
13
14
 
@@ -28,7 +29,7 @@ module Rabbitek
28
29
  end
29
30
 
30
31
  def self.logger
31
- @logger ||= Logger.new(STDOUT)
32
+ @config.logger
32
33
  end
33
34
 
34
35
  def self.create_channel
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rabbitek
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Boostcom
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-10-31 00:00:00.000000000 Z
11
+ date: 2019-01-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -149,6 +149,7 @@ files:
149
149
  - ".rspec"
150
150
  - ".rubocop.yml"
151
151
  - ".travis.yml"
152
+ - CHANGELOG.md
152
153
  - Gemfile
153
154
  - Gemfile.lock
154
155
  - LICENSE.txt
@@ -167,10 +168,13 @@ files:
167
168
  - lib/rabbitek/client/publisher.rb
168
169
  - lib/rabbitek/config.rb
169
170
  - lib/rabbitek/loggable.rb
171
+ - lib/rabbitek/server/batcher.rb
170
172
  - lib/rabbitek/server/consumer.rb
171
173
  - lib/rabbitek/server/hooks/opentracing.rb
172
174
  - lib/rabbitek/server/hooks/retry.rb
173
175
  - lib/rabbitek/server/hooks/time_tracker.rb
176
+ - lib/rabbitek/server/message.rb
177
+ - lib/rabbitek/server/retryer.rb
174
178
  - lib/rabbitek/server/server_hook.rb
175
179
  - lib/rabbitek/server/starter.rb
176
180
  - lib/rabbitek/utils/common.rb
@@ -199,7 +203,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
199
203
  version: '0'
200
204
  requirements: []
201
205
  rubyforge_project:
202
- rubygems_version: 2.7.6
206
+ rubygems_version: 2.5.1
203
207
  signing_key:
204
208
  specification_version: 4
205
209
  summary: High performance background job processing