upperkut 0.7.2 → 0.7.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: 9fb4b45a40c37d26aa8d96f199c79141f8b34d1edc2fd48c751a6d8567b77deb
4
- data.tar.gz: b4b91ae1d8d2737d20b97c23744cc5cc8052681ede5c68f28790c50f3a092882
3
+ metadata.gz: d7c6cb99c0744e428ea946a58b2aa5b1065bb5abdd37487cd01caeb5589ecec1
4
+ data.tar.gz: 9d560415137c6e60d9588a625a5f79446af88dedfd492a32e03ccea73fc45008
5
5
  SHA512:
6
- metadata.gz: 2c934b63764f8a673762cdb163989b18fae91d8b2be30e8b99f5db93e4329c3c63f2beb75a8e38c46b966bffb7f4e8e7f09c4f5246e8806b001b0da2ce056f88
7
- data.tar.gz: '038a2489af4d5b8b0811f30402922e3a7ed4524dceee4b3dcdff1d13c417a037f2f7f88c61122ba65cd0ad2ee1ed9760a4210a94427e69c3d7b2f635e2c3694c'
6
+ metadata.gz: cafd45d08f7677f4c8b9624fe97303e9d0365f7d466467a6e52d152392ac272a635cf572f304ba811f2340dfe83406474de468aa5e4a1f90dd6c12e58dd76e26
7
+ data.tar.gz: 8e365782ec521ee2370b5f8fefcf2b38bf6ff610f66dd6392887fa38fc544e03bb80fa02467e6d5a7aebf2be486a39b87de8f61a7fa96a6e0d43249730b4c3e5
data/CHANGELOG.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  0.7.x
4
4
  ---------
5
+ - Add handle_error method #44
6
+ - Added Datahog Middleware (#42)
7
+ - Added Priority Queue (#39) thanks to @jeangnc and @jeanmatheussouto
5
8
  - Added Scheduled Queue Implementation thanks to @rodrigo-araujo #38
6
9
  - Added Datahog middleware #42 by @gabriel-augusto
7
10
  - Added redis to CI #40 by #henrich-m
data/Gemfile CHANGED
@@ -5,7 +5,6 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
5
5
  # Specify your gem's dependencies in upperkut.gemspec
6
6
  gemspec
7
7
 
8
- gem 'fakeredis'
9
8
  gem 'fivemat'
10
9
  gem 'pry'
11
10
  gem 'rspec_junit_formatter'
data/Gemfile.lock CHANGED
@@ -1,9 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- upperkut (0.7.2)
4
+ upperkut (0.7.4)
5
5
  connection_pool (~> 2.2, >= 2.2.2)
6
- redis (>= 3.3.3, < 5)
6
+ redis (>= 4.1.0, < 5.0.0)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
@@ -12,29 +12,27 @@ GEM
12
12
  connection_pool (2.2.2)
13
13
  diff-lcs (1.3)
14
14
  docile (1.3.1)
15
- fakeredis (0.7.0)
16
- redis (>= 3.2, < 5.0)
17
- fivemat (1.3.6)
18
- json (2.1.0)
19
- method_source (0.9.0)
20
- pry (0.11.3)
15
+ fivemat (1.3.7)
16
+ json (2.2.0)
17
+ method_source (0.9.2)
18
+ pry (0.12.2)
21
19
  coderay (~> 1.1.0)
22
20
  method_source (~> 0.9.0)
23
21
  rake (10.5.0)
24
- redis (4.0.1)
25
- rspec (3.7.0)
26
- rspec-core (~> 3.7.0)
27
- rspec-expectations (~> 3.7.0)
28
- rspec-mocks (~> 3.7.0)
29
- rspec-core (3.7.1)
30
- rspec-support (~> 3.7.0)
31
- rspec-expectations (3.7.0)
22
+ redis (4.1.0)
23
+ rspec (3.8.0)
24
+ rspec-core (~> 3.8.0)
25
+ rspec-expectations (~> 3.8.0)
26
+ rspec-mocks (~> 3.8.0)
27
+ rspec-core (3.8.0)
28
+ rspec-support (~> 3.8.0)
29
+ rspec-expectations (3.8.3)
32
30
  diff-lcs (>= 1.2.0, < 2.0)
33
- rspec-support (~> 3.7.0)
34
- rspec-mocks (3.7.0)
31
+ rspec-support (~> 3.8.0)
32
+ rspec-mocks (3.8.0)
35
33
  diff-lcs (>= 1.2.0, < 2.0)
36
- rspec-support (~> 3.7.0)
37
- rspec-support (3.7.1)
34
+ rspec-support (~> 3.8.0)
35
+ rspec-support (3.8.0)
38
36
  rspec_junit_formatter (0.4.1)
39
37
  rspec-core (>= 2, < 4, != 2.12.0)
40
38
  simplecov (0.16.1)
@@ -47,8 +45,7 @@ PLATFORMS
47
45
  ruby
48
46
 
49
47
  DEPENDENCIES
50
- bundler (~> 1.16)
51
- fakeredis
48
+ bundler (>= 1.16)
52
49
  fivemat
53
50
  pry
54
51
  rake (~> 10.0)
@@ -58,4 +55,4 @@ DEPENDENCIES
58
55
  upperkut!
59
56
 
60
57
  BUNDLED WITH
61
- 1.16.6
58
+ 1.17.2
data/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
  [![Maintainability](https://api.codeclimate.com/v1/badges/ece40319b0db03af891d/maintainability)](https://codeclimate.com/repos/5b318a7c6d37b70272008676/maintainability)
5
5
  [![Test Coverage](https://api.codeclimate.com/v1/badges/ece40319b0db03af891d/test_coverage)](https://codeclimate.com/repos/5b318a7c6d37b70272008676/test_coverage)
6
6
 
7
+ [[Docs]](https://www.rubydoc.info/gems/upperkut/0.7.2/Upperkut)
8
+
7
9
  Background processing framework for Ruby applications.
8
10
 
9
11
  ## Installation
@@ -31,25 +33,6 @@ Or install it yourself as:
31
33
  class MyWorker
32
34
  include Upperkut::Worker
33
35
 
34
- # This is optional
35
-
36
- setup_upperkut do |config|
37
- # Define which redis instance you want to use
38
- config.strategy = Upperkut::Strategies::BufferedQueue.new(
39
- self,
40
- redis: { url: ENV['ANOTHER_REDIS_INSTANCE_URL'] },
41
- batch_size: 400, # How many events should be dispatched to worker.
42
- max_wait: 300 # How long Processor wait in seconds to process batch.
43
- # even though the amount of items did not reached the
44
- # the batch_size.
45
- )
46
-
47
- # How frequent the Processor should hit redis looking for elegible
48
- # batch. The default value is 5 seconds. You can also set the env
49
- # UPPERKUT_POLLING_INTERVAL.
50
- config.polling_interval = 4
51
- end
52
-
53
36
  def perform(batch_items)
54
37
  heavy_processing(batch_items)
55
38
  process_metrics(batch_items)
@@ -57,13 +40,13 @@ Or install it yourself as:
57
40
  end
58
41
  ```
59
42
 
60
- 2) Start pushings items;
43
+ 2) Start pushing items;
61
44
  ```ruby
62
45
  Myworker.push_items(
63
46
  [
64
47
  {
65
- 'id' => SecureRandom.uuid,
66
- 'name' => 'Robert C Hall',
48
+ 'id' => SecureRandom.uuid,
49
+ 'name' => 'Robert C Hall',
67
50
  'action' => 'EMAIL_OPENNED'
68
51
  }
69
52
  ]
@@ -94,15 +77,15 @@ Or install it yourself as:
94
77
  end
95
78
  ```
96
79
 
97
- 2) Start pushings items with `timestamp` param;
80
+ 2) Start pushing items with `timestamp` parameter;
98
81
  ```ruby
99
82
  # timestamp is 'Thu, 10 May 2019 23:43:58 GMT'
100
83
  Myworker.push_items(
101
84
  [
102
85
  {
103
- 'timestamp' => '1557531838',
104
- 'id' => SecureRandom.uuid,
105
- 'name' => 'Robert C Hall',
86
+ 'timestamp' => '1557531838',
87
+ 'id' => SecureRandom.uuid,
88
+ 'name' => 'Robert C Hall',
106
89
  'action' => 'SEND_NOTIFICATION'
107
90
  }
108
91
  ]
@@ -114,6 +97,52 @@ Or install it yourself as:
114
97
  $ bundle exec upperkut --worker MyWorker --concurrency 10
115
98
  ```
116
99
 
100
+ ### Example 3 - Priority Queue:
101
+
102
+ Note: priority queues requires redis 5.0.0+ as it uses ZPOP* commands.
103
+
104
+ 1) Create a Worker class and the define how to process the batch;
105
+ ```ruby
106
+ require 'upperkut/strategies/priority_queue'
107
+
108
+ class MyWorker
109
+ include Upperkut::Worker
110
+
111
+ setup_upperkut do |config|
112
+ config.strategy = Upperkut::Strategies::PriorityQueue.new(
113
+ self,
114
+ priority_key: -> { |item| item['tenant_id'] }
115
+ )
116
+ end
117
+
118
+ def perform(items)
119
+ items.each do |item|
120
+ puts "event dispatched: #{item.inspect}"
121
+ end
122
+ end
123
+ end
124
+ ```
125
+
126
+ 2) So you can enqueue items from different tenants;
127
+ ```ruby
128
+ MyWorker.push_items(
129
+ [
130
+ { 'tenant_id' => 1, 'id' => 1 },
131
+ { 'tenant_id' => 1, 'id' => 2 },
132
+ { 'tenant_id' => 1, 'id' => 3 },
133
+ { 'tenant_id' => 2, 'id' => 4 },
134
+ { 'tenant_id' => 3, 'id' => 5 },
135
+ ]
136
+ )
137
+ ```
138
+
139
+ The code above will enqueue items as follows `1, 4, 5, 2, 3`
140
+
141
+ 3) Start Upperkut;
142
+ ```bash
143
+ $ bundle exec upperkut --worker MyWorker --concurrency 10
144
+ ```
145
+
117
146
  ## Development
118
147
 
119
148
  After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rspec` to run the tests.
@@ -0,0 +1,21 @@
1
+ require_relative '../lib/upperkut/worker'
2
+ require_relative '../lib/upperkut/strategies/priority_queue'
3
+
4
+ class PriorityWorker
5
+ include Upperkut::Worker
6
+
7
+ setup_upperkut do |config|
8
+ config.strategy = Upperkut::Strategies::PriorityQueue.new(
9
+ self,
10
+ priority_key: -> { |item| item['tenant_id'] },
11
+ batch_size: 1
12
+ )
13
+ end
14
+
15
+ def perform(items)
16
+ items.each do |item|
17
+ puts "event dispatched: #{item.inspect}"
18
+ sleep 1
19
+ end
20
+ end
21
+ end
@@ -20,17 +20,23 @@ module Upperkut
20
20
  @worker.server_middlewares.invoke(@worker, items) do
21
21
  worker_instance.perform(items_body.dup)
22
22
  end
23
- rescue Exception => ex
24
- @worker.push_items(items_body)
25
-
23
+ rescue StandardError => error
26
24
  @logger.info(
27
25
  action: :requeue,
28
- ex: ex,
26
+ ex: error,
29
27
  item_size: items_body.size
30
28
  )
31
29
 
32
- @logger.error(ex.backtrace.join("\n"))
33
- raise ex
30
+ @logger.error(error.backtrace.join("\n"))
31
+
32
+ if worker_instance.respond_to?(:handle_error)
33
+ worker_instance.handle_error(error, items_body)
34
+ return
35
+ else
36
+ @worker.push_items(items_body)
37
+ end
38
+
39
+ raise error
34
40
  end
35
41
  end
36
42
  end
@@ -8,12 +8,12 @@ module Upperkut
8
8
  size: 2, # pool related option
9
9
  connect_timeout: 0.2,
10
10
  read_timeout: 5.0,
11
- write_timeout: 0.5,
12
- url: ENV['REDIS_URL']
11
+ write_timeout: 0.5
13
12
  }.freeze
14
13
 
15
14
  def initialize(options)
16
- @options = DEFAULT_OPTIONS.merge(options)
15
+ @options = DEFAULT_OPTIONS.merge(url: ENV['REDIS_URL'])
16
+ .merge(options)
17
17
 
18
18
  # Extract pool related options
19
19
  @size = @options.delete(:size)
@@ -12,7 +12,6 @@ module Upperkut
12
12
  def initialize(worker, options = {})
13
13
  @options = options
14
14
  @redis_options = options.fetch(:redis, {})
15
- @redis_pool = setup_redis_pool
16
15
  @worker = worker
17
16
  @max_wait = options.fetch(
18
17
  :max_wait,
@@ -75,6 +74,10 @@ module Upperkut
75
74
 
76
75
  private
77
76
 
77
+ def key
78
+ "upperkut:buffers:#{to_underscore(@worker.name)}"
79
+ end
80
+
78
81
  def fulfill_condition?(buff_size)
79
82
  return false if buff_size.zero?
80
83
 
@@ -96,22 +99,22 @@ module Upperkut
96
99
  now - item.fetch('enqueued_at', Time.now).to_f
97
100
  end
98
101
 
99
- def setup_redis_pool
100
- return @redis_options if @redis_options.is_a?(ConnectionPool)
101
-
102
- RedisPool.new(options.fetch(:redis, {})).create
103
- end
104
-
105
102
  def redis
106
103
  raise ArgumentError, 'requires a block' unless block_given?
107
104
 
108
- @redis_pool.with do |conn|
105
+ redis_pool.with do |conn|
109
106
  yield conn
110
107
  end
111
108
  end
112
109
 
113
- def key
114
- "upperkut:buffers:#{to_underscore(@worker.name)}"
110
+ def redis_pool
111
+ @redis_pool ||= begin
112
+ if @redis_options.is_a?(ConnectionPool)
113
+ @redis_options
114
+ else
115
+ RedisPool.new(@redis_options).create
116
+ end
117
+ end
115
118
  end
116
119
  end
117
120
  end
@@ -0,0 +1,197 @@
1
+ module Upperkut
2
+ module Strategies
3
+ # Public: Queue that prevent a single tenant from taking over.
4
+ class PriorityQueue < Upperkut::Strategies::Base
5
+ include Upperkut::Util
6
+
7
+ ONE_DAY_IN_SECONDS = 86400
8
+
9
+ # Logic as follows:
10
+ #
11
+ # We keep the last score used for each tenant key. One tenant_key is
12
+ # an tenant unique id. To calculate the next_score we use
13
+ # max(current_tenant_score, current_global_score) + increment we store
14
+ # the queue in a sorted set using the next_score as ordering key if one
15
+ # tenant sends lots of messages, this tenant ends up with lots of
16
+ # messages in the queue spaced by increment if another tenant then
17
+ # sends a message, since it previous_tenant_score is lower than the
18
+ # first tenant, it will be inserted before it in the queue.
19
+ #
20
+ # In other words, the idea of this queue is to not allowing an tenant
21
+ # that sends a lot of messages to dominate processing and give a chance
22
+ # for tenants that sends few messages to have a fair share of
23
+ # processing time.
24
+ ENQUEUE_ITEM = %(
25
+ local increment = 1
26
+ local current_checkpoint = tonumber(redis.call("GET", KEYS[1])) or 0
27
+ local score_key = KEYS[2]
28
+ local current_score = tonumber(redis.call("GET", score_key)) or 0
29
+ local queue_key = KEYS[3]
30
+ local next_score = nil
31
+
32
+ if current_score >= current_checkpoint then
33
+ next_score = current_score + increment
34
+ else
35
+ next_score = current_checkpoint + increment
36
+ end
37
+
38
+ redis.call("SETEX", score_key, #{ONE_DAY_IN_SECONDS}, next_score)
39
+ redis.call("ZADD", queue_key, next_score, ARGV[1])
40
+
41
+ return next_score
42
+ ).freeze
43
+
44
+ # Uses ZPOP* functions available only on redis 5.0.0+
45
+ DEQUEUE_ITEM = %(
46
+ local checkpoint_key = KEYS[1]
47
+ local queue_key = KEYS[2]
48
+ local batch_size = ARGV[1]
49
+ local popped_items = redis.call("ZPOPMIN", queue_key, batch_size)
50
+ local items = {}
51
+ local last_score = 0
52
+
53
+ for i, v in ipairs(popped_items) do
54
+ if i % 2 == 1 then
55
+ table.insert(items, v)
56
+ else
57
+ last_score = v
58
+ end
59
+ end
60
+
61
+ redis.call("SETEX", checkpoint_key, 86400, last_score)
62
+ return items
63
+ ).freeze
64
+
65
+ def initialize(worker, options)
66
+ @worker = worker
67
+ @options = options
68
+ @priority_key = options.fetch(:priority_key)
69
+ @redis_options = options.fetch(:redis, {})
70
+
71
+ @max_wait = options.fetch(
72
+ :max_wait,
73
+ Integer(ENV['UPPERKUT_MAX_WAIT'] || 20)
74
+ )
75
+
76
+ @batch_size = options.fetch(
77
+ :batch_size,
78
+ Integer(ENV['UPPERKUT_BATCH_SIZE'] || 1000)
79
+ )
80
+
81
+ @waiting_time = 0
82
+
83
+ raise ArgumentError, 'Invalid priority_key. ' \
84
+ 'Must be a lambda' unless @priority_key.respond_to?(:call)
85
+ end
86
+
87
+ # Public: Ingests the event into strategy.
88
+ #
89
+ # items - The Array of items do be inserted.
90
+ #
91
+ # Returns true when success, raise when error.
92
+ def push_items(items = [])
93
+ items = [items] if items.is_a?(Hash)
94
+ return false if items.empty?
95
+
96
+ redis do |conn|
97
+ items.each do |item|
98
+ priority_key = @priority_key.call(item)
99
+ score_key = "#{queue_key}:#{priority_key}:score"
100
+
101
+ keys = [queue_checkpoint_key,
102
+ score_key,
103
+ queue_key]
104
+
105
+ conn.eval(ENQUEUE_ITEM,
106
+ keys: keys,
107
+ argv: [encode_json_items([item])])
108
+ end
109
+ end
110
+
111
+ true
112
+ end
113
+
114
+ # Public: Retrieve events from Strategy.
115
+ #
116
+ # Returns an Array containing events as hash.
117
+ def fetch_items
118
+ batch_size = [@batch_size, size].min
119
+
120
+ items = redis do |conn|
121
+ conn.eval(DEQUEUE_ITEM,
122
+ keys: [queue_checkpoint_key, queue_key],
123
+ argv: [batch_size])
124
+ end
125
+
126
+ decode_json_items(items)
127
+ end
128
+
129
+ # Public: Clear all data related to the strategy.
130
+ def clear
131
+ redis { |conn| conn.del(queue_key) }
132
+ end
133
+
134
+ # Public: Tells when to execute the event processing,
135
+ # when this condition is met so the events are dispatched to
136
+ # the worker.
137
+ def process?
138
+ if fulfill_condition?(size)
139
+ @waiting_time = 0
140
+ return true
141
+ end
142
+
143
+ @waiting_time += @worker.setup.polling_interval
144
+ false
145
+ end
146
+
147
+ # Public: Consolidated strategy metrics.
148
+ #
149
+ # Returns hash containing metric name and values.
150
+ def metrics
151
+ {
152
+ 'size' => size
153
+ }
154
+ end
155
+
156
+ private
157
+
158
+ def queue_checkpoint_key
159
+ "#{queue_key}:checkpoint"
160
+ end
161
+
162
+ def queue_key
163
+ "upperkut:priority_queue:#{to_underscore(@worker.name)}"
164
+ end
165
+
166
+ def fulfill_condition?(buff_size)
167
+ return false if buff_size.zero?
168
+
169
+ buff_size >= @batch_size || @waiting_time >= @max_wait
170
+ end
171
+
172
+ def size
173
+ redis do |conn|
174
+ conn.zcard(queue_key)
175
+ end
176
+ end
177
+
178
+ def redis
179
+ raise ArgumentError, 'requires a block' unless block_given?
180
+
181
+ redis_pool.with do |conn|
182
+ yield conn
183
+ end
184
+ end
185
+
186
+ def redis_pool
187
+ @redis_pool ||= begin
188
+ if @redis_options.is_a?(ConnectionPool)
189
+ @redis_options
190
+ else
191
+ RedisPool.new(@options.fetch(:redis, {})).create
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
@@ -1,3 +1,3 @@
1
1
  module Upperkut
2
- VERSION = '0.7.2'.freeze
2
+ VERSION = '0.7.4'.freeze
3
3
  end
data/upperkut.gemspec CHANGED
@@ -22,8 +22,8 @@ Gem::Specification.new do |spec|
22
22
  spec.required_ruby_version = '>= 2.2.2'
23
23
 
24
24
  spec.add_dependency 'connection_pool', '~> 2.2', '>= 2.2.2'
25
- spec.add_dependency 'redis', '>= 3.3.3', '< 5'
26
- spec.add_development_dependency 'bundler', '~> 1.16'
25
+ spec.add_dependency 'redis', '>= 4.1.0', '< 5.0.0'
26
+ spec.add_development_dependency 'bundler', '>= 1.16'
27
27
  spec.add_development_dependency 'rake', '~> 10.0'
28
28
  spec.add_development_dependency 'rspec', '~> 3.0'
29
29
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: upperkut
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.7.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nando Sousa
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-29 00:00:00.000000000 Z
11
+ date: 2019-05-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: connection_pool
@@ -36,32 +36,32 @@ dependencies:
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: 3.3.3
39
+ version: 4.1.0
40
40
  - - "<"
41
41
  - !ruby/object:Gem::Version
42
- version: '5'
42
+ version: 5.0.0
43
43
  type: :runtime
44
44
  prerelease: false
45
45
  version_requirements: !ruby/object:Gem::Requirement
46
46
  requirements:
47
47
  - - ">="
48
48
  - !ruby/object:Gem::Version
49
- version: 3.3.3
49
+ version: 4.1.0
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
- version: '5'
52
+ version: 5.0.0
53
53
  - !ruby/object:Gem::Dependency
54
54
  name: bundler
55
55
  requirement: !ruby/object:Gem::Requirement
56
56
  requirements:
57
- - - "~>"
57
+ - - ">="
58
58
  - !ruby/object:Gem::Version
59
59
  version: '1.16'
60
60
  type: :development
61
61
  prerelease: false
62
62
  version_requirements: !ruby/object:Gem::Requirement
63
63
  requirements:
64
- - - "~>"
64
+ - - ">="
65
65
  - !ruby/object:Gem::Version
66
66
  version: '1.16'
67
67
  - !ruby/object:Gem::Dependency
@@ -113,6 +113,7 @@ files:
113
113
  - Rakefile
114
114
  - bin/upperkut
115
115
  - examples/basic.rb
116
+ - examples/priority_worker.rb
116
117
  - examples/scheduled_worker.rb
117
118
  - examples/with_middlewares.rb
118
119
  - lib/upperkut.rb
@@ -129,6 +130,7 @@ files:
129
130
  - lib/upperkut/redis_pool.rb
130
131
  - lib/upperkut/strategies/base.rb
131
132
  - lib/upperkut/strategies/buffered_queue.rb
133
+ - lib/upperkut/strategies/priority_queue.rb
132
134
  - lib/upperkut/strategies/scheduled_queue.rb
133
135
  - lib/upperkut/util.rb
134
136
  - lib/upperkut/version.rb