ruby-limiter 1.0.1 → 2.2.2

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
- SHA1:
3
- metadata.gz: c8b855660b5c5b40e96437d66444cbba63b07af6
4
- data.tar.gz: f4486818e1c73464c3b6545ee35601282a1cb798
2
+ SHA256:
3
+ metadata.gz: 8414ef377bee89d172c77d547a9cc0468bbfbf5a4a2b775ffa8faa137fdbed5b
4
+ data.tar.gz: 9065acd27f48647174b0cce72bb919869addb4dca0e53e8b5e5d446dfaff53d0
5
5
  SHA512:
6
- metadata.gz: 53f3886fe4a1f71c5f7454eebfbc764b4880cca6ab1026bfadc3e2ee16709a95c5f01e4e2ff732b49b8a0332ffba06206715fda1561032d0916c47850f289f89
7
- data.tar.gz: 4b33aeea30a63521f97adbaa290604cc176788e7a8ea4ea643a94bf08604ce779b53fcb36b9ec8f44c6a5d1d684897d1596635e21c4c3d6ee4cbdd8f3c8443a9
6
+ metadata.gz: 58dbd481ed08a6ed0ab24664c94488acb2206590de98f6cc06b4770acd797d1744a39d51a362893b7162381edc6da005ee7ec247cdec1fab4f7d9d4686da8355
7
+ data.tar.gz: 90c28e01feb7964814da473d1bddbaca86a0167f9d580aa9ac3281fafff73bca8c8e7dbda4147a56fc53ad56b8f424f523c881294e888f810760ad6c47f44624
data/.rubocop.yml CHANGED
@@ -2,4 +2,4 @@ inherit_from:
2
2
  - https://shopify.github.io/ruby-style-guide/rubocop.yml
3
3
 
4
4
  AllCops:
5
- TargetRubyVersion: 2.3
5
+ TargetRubyVersion: 2.7
data/.travis.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.3
5
- - 2.4
6
- - 2.5
4
+ - 2.6
5
+ - 2.7
6
+ - 3.0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## v2.2.2
4
+
5
+ - security update to rake 13.0.6 [CVE-2020-8130]
6
+
7
+ ## v2.2.0
8
+
9
+ - adds support for "balancing" requests over time
10
+
11
+ ## v2.1.0
12
+
13
+ - add support to call a block when limiting takes place
14
+
15
+ ## v2.0.1
16
+
17
+ - eliminate kwarg warning in ruby 2.7 (while still supporting 2.6)
18
+
19
+ ## v2.0.0
20
+
21
+ - end support for ruby 2.3/2.4/2.5
22
+ - test on ruby 2.6/2.7/3.0 (using ruby 2.7 for development)
23
+
24
+ ## v1.1.0
25
+
26
+ - using Process.clock_gettime(Process::CLOCK_MONOTONIC) instead of Time.now for improved accuracy
27
+
28
+ ## v1.0.2
29
+
30
+ - DOCFIX: fix name of gem in README
31
+ - BUGFIX: add ruby-limiter.rb so that it works better with bundler
32
+
3
33
  ## v1.0.1
4
34
 
5
35
  - BUGFIX: support arguments for throttled methods
data/README.md CHANGED
@@ -7,7 +7,7 @@ This gem implements a simple mechanism to throttle or rate-limit operations in R
7
7
  Add this line to your application's Gemfile:
8
8
 
9
9
  ```ruby
10
- gem 'limiter'
10
+ gem 'ruby-limiter'
11
11
  ```
12
12
 
13
13
  And then execute:
@@ -16,13 +16,13 @@ And then execute:
16
16
 
17
17
  Or install it yourself as:
18
18
 
19
- $ gem install limiter
19
+ $ gem install ruby-limiter
20
20
 
21
21
  ## Usage
22
22
 
23
23
  ### Basic Usage
24
24
 
25
- To rate limit calling an instance method, a mixin is provided. Simply specify the method to me limited, and the maximum
25
+ To rate limit calling an instance method, a mixin is provided. Simply specify the method to be limited, and the maximum
26
26
  rate that the method can be called. This rate is (by default) a number of requests per minute.
27
27
 
28
28
  ``` ruby
@@ -46,22 +46,49 @@ class Widget
46
46
 
47
47
  # limit the rate we can call tick to 5 times per second
48
48
  # when the rate has been exceeded, a call to tick will block until the rate limit would not be exceeded
49
- limit_method :tick, rate: 5, interval: 1
49
+ # and the provided block will be executed
50
+ limit_method(:tick, rate: 5, interval: 1) do
51
+ puts 'Limit reached'
52
+ end
50
53
 
51
54
  ...
52
55
  end
53
56
  ```
54
57
 
58
+ #### Load balancing
59
+
60
+ By default all calls to the `limit_method` will be bursted, e.g. as quick as possible, until the rate is exceeded.
61
+ Then we wait for the remainder of the interval to continue. To even out the burst, an optional `balanced` parameter can be
62
+ provided to enable interleaving between the method calls, e.g: `interleave = interval / size`.
63
+
64
+ ``` ruby
65
+ ...
66
+ limit_method :tick, rate: 60, balanced: true
67
+ ...
68
+ ```
69
+
70
+ For example: with an interval of 60 seconds and a rate of 60:
71
+
72
+ `balanced: false`
73
+ : As quickly as possible we call the method 60 times, then we wait for the remainder of the time.
74
+
75
+ `balanced: true`
76
+ : We interleave each call with 1 second so we call this method every second.
77
+
78
+
55
79
  ### Advanced Usage
56
80
 
57
81
  In cases where the mixin is not appropriate the `RateQueue` class can be used directly. As in the mixin examples above,
58
- the `interval` parameter is optional (and defaults to 1 minute).
82
+ the `interval` parameter is optional (and defaults to 1 minute). It is also possible
83
+ to provide the block to `RateQueue`, which will be executed on each limit hit (useful for metrics).
59
84
 
60
85
  ``` ruby
61
86
  class Widget
62
87
  def initialize
63
88
  # create a rate-limited queue which allows 10000 operations per hour
64
- @queue = RateQueue.new(10000, interval: 3600)
89
+ @queue = Limiter::RateQueue.new(10000, interval: 3600) do
90
+ puts "Hit the limit, waiting"
91
+ end
65
92
  end
66
93
 
67
94
  def tick
data/Rakefile CHANGED
@@ -7,6 +7,7 @@ Rake::TestTask.new(:test) do |t|
7
7
  t.libs << "test"
8
8
  t.libs << "lib"
9
9
  t.test_files = FileList["test/**/*_test.rb"]
10
+ t.warning = true
10
11
  end
11
12
 
12
- task default: :test
13
+ task default: :test
data/dev.yml CHANGED
@@ -3,7 +3,7 @@
3
3
  name: limiter
4
4
 
5
5
  up:
6
- - ruby: 2.4.4
6
+ - ruby: 2.7.3
7
7
  - bundler
8
8
 
9
9
  commands:
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require 'forwardable'
5
+
6
+ module Limiter
7
+ class Clock
8
+ include Singleton
9
+
10
+ extend SingleForwardable
11
+ def_single_delegators :instance, :sleep, :time
12
+
13
+ def sleep(interval)
14
+ Kernel.sleep(interval)
15
+ end
16
+
17
+ def time
18
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
19
+ end
20
+ end
21
+ end
data/lib/limiter/mixin.rb CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  module Limiter
4
4
  module Mixin
5
- def limit_method(method, rate:, interval: 60)
6
- queue = RateQueue.new(rate, interval: interval)
5
+ def limit_method(method, rate:, interval: 60, balanced: false, &b)
6
+ queue = RateQueue.new(rate, interval: interval, balanced: balanced, &b)
7
7
 
8
8
  mixin = Module.new do
9
- define_method(method) do |*args|
9
+ define_method(method) do |*args, **options, &blk|
10
10
  queue.shift
11
- super(*args)
11
+ options.empty? ? super(*args, &blk) : super(*args, **options, &blk)
12
12
  end
13
13
  end
14
14
 
@@ -2,15 +2,16 @@
2
2
 
3
3
  module Limiter
4
4
  class RateQueue
5
- EPOCH = Time.at(0)
5
+ EPOCH = 0.0
6
6
 
7
- def initialize(size, interval: 60)
7
+ def initialize(size, interval: 60, balanced: false, &blk)
8
8
  @size = size
9
9
  @interval = interval
10
10
 
11
- @ring = Array.new(size, EPOCH)
11
+ @ring = balanced ? balanced_ring : unbalanced_ring
12
12
  @head = 0
13
13
  @mutex = Mutex.new
14
+ @blk = blk
14
15
  end
15
16
 
16
17
  def shift
@@ -21,7 +22,7 @@ module Limiter
21
22
 
22
23
  sleep_until(time + @interval)
23
24
 
24
- @ring[@head] = Time.now
25
+ @ring[@head] = clock.time
25
26
  @head = (@head + 1) % @size
26
27
  end
27
28
 
@@ -31,9 +32,30 @@ module Limiter
31
32
  private
32
33
 
33
34
  def sleep_until(time)
34
- interval = time - Time.now
35
+ interval = time - clock.time
35
36
  return unless interval.positive?
36
- sleep(interval)
37
+ @blk.call if @blk
38
+ clock.sleep(interval)
39
+ end
40
+
41
+ def clock
42
+ Clock
43
+ end
44
+
45
+ def unbalanced_ring
46
+ Array.new(@size, EPOCH)
47
+ end
48
+
49
+ def balanced_ring
50
+ (0...@size).map { |i| base_time + (gap * i) }
51
+ end
52
+
53
+ def gap
54
+ @interval.to_f / @size.to_f
55
+ end
56
+
57
+ def base_time
58
+ clock.time - @interval
37
59
  end
38
60
  end
39
61
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Limiter
4
- VERSION = '1.0.1'
4
+ VERSION = '2.2.2'
5
5
  end
data/lib/limiter.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'limiter/clock'
3
4
  require 'limiter/mixin'
4
5
  require 'limiter/rate_queue'
5
6
  require 'limiter/version'
@@ -0,0 +1 @@
1
+ require 'limiter'
data/limiter.gemspec CHANGED
@@ -13,6 +13,13 @@ Gem::Specification.new do |spec|
13
13
  spec.summary = 'Simple Ruby rate limiting mechanism.'
14
14
  spec.homepage = 'https://github.com/Shopify/limiter'
15
15
  spec.license = 'MIT'
16
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
17
+
18
+ if spec.respond_to?(:metadata)
19
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
20
+ else
21
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
22
+ end
16
23
 
17
24
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
25
  f.match(%r{^(test|spec|features)/})
@@ -21,9 +28,10 @@ Gem::Specification.new do |spec|
21
28
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
29
  spec.require_paths = %w(lib)
23
30
 
24
- spec.add_development_dependency 'bundler', '~> 1.15'
31
+ spec.add_development_dependency 'bundler'
25
32
  spec.add_development_dependency 'minitest', '~> 5.0'
26
- spec.add_development_dependency 'rake', '~> 10.0'
33
+ spec.add_development_dependency 'minitest-focus', '~> 1.3'
34
+ spec.add_development_dependency 'mocha', '~> 1.11'
35
+ spec.add_development_dependency 'rake', '~> 13.0'
27
36
  spec.add_development_dependency 'rubocop', '~> 0.56'
28
- spec.add_development_dependency 'timecop', '~> 0.8.0'
29
37
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-limiter
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 2.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - S. Brent Faulkner
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-07-06 00:00:00.000000000 Z
11
+ date: 2022-01-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.15'
19
+ version: '0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '1.15'
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: minitest
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -39,47 +39,61 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '5.0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rake
42
+ name: minitest-focus
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '10.0'
47
+ version: '1.3'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '10.0'
54
+ version: '1.3'
55
55
  - !ruby/object:Gem::Dependency
56
- name: rubocop
56
+ name: mocha
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '0.56'
61
+ version: '1.11'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '0.56'
68
+ version: '1.11'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '13.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '13.0'
69
83
  - !ruby/object:Gem::Dependency
70
- name: timecop
84
+ name: rubocop
71
85
  requirement: !ruby/object:Gem::Requirement
72
86
  requirements:
73
87
  - - "~>"
74
88
  - !ruby/object:Gem::Version
75
- version: 0.8.0
89
+ version: '0.56'
76
90
  type: :development
77
91
  prerelease: false
78
92
  version_requirements: !ruby/object:Gem::Requirement
79
93
  requirements:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
- version: 0.8.0
96
+ version: '0.56'
83
97
  description:
84
98
  email:
85
99
  - brent.faulkner@shopify.com
@@ -100,14 +114,17 @@ files:
100
114
  - bin/setup
101
115
  - dev.yml
102
116
  - lib/limiter.rb
117
+ - lib/limiter/clock.rb
103
118
  - lib/limiter/mixin.rb
104
119
  - lib/limiter/rate_queue.rb
105
120
  - lib/limiter/version.rb
121
+ - lib/ruby-limiter.rb
106
122
  - limiter.gemspec
107
123
  homepage: https://github.com/Shopify/limiter
108
124
  licenses:
109
125
  - MIT
110
- metadata: {}
126
+ metadata:
127
+ allowed_push_host: https://rubygems.org
111
128
  post_install_message:
112
129
  rdoc_options: []
113
130
  require_paths:
@@ -116,15 +133,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
116
133
  requirements:
117
134
  - - ">="
118
135
  - !ruby/object:Gem::Version
119
- version: '0'
136
+ version: 2.6.0
120
137
  required_rubygems_version: !ruby/object:Gem::Requirement
121
138
  requirements:
122
139
  - - ">="
123
140
  - !ruby/object:Gem::Version
124
141
  version: '0'
125
142
  requirements: []
126
- rubyforge_project:
127
- rubygems_version: 2.6.14
143
+ rubygems_version: 3.2.20
128
144
  signing_key:
129
145
  specification_version: 4
130
146
  summary: Simple Ruby rate limiting mechanism.