rabbitek 0.1.1 → 0.2.0

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
- 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