upperkut 0.7.2 → 0.7.4

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