async-limiter 0.0.1 → 1.3.1

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: c1e4bf6b1b1477475fc5bf247364592ebd062e86cd5039614a9963cfdc9e23ab
4
- data.tar.gz: 82f5fc81a61d6d8cc0400d86b4a84977379f3edc8f6951bba738e2215e1e3445
3
+ metadata.gz: 5b8fe52d0adb39187c467e5a9195df3fc974cdad0c66e20ca4fbb8f2331e108a
4
+ data.tar.gz: 65aee35742f72694b2a1c1e4d34b41ee0d6e03eedc0f82029dbac31d042a1ad1
5
5
  SHA512:
6
- metadata.gz: 8c2698448ec6846405dc647c0e11b37616fbd80027b92a03e28a849d511164835909d4982ddbe6cfff7e963b00bedc6699dd90dbc33067f52969480170bc1c4a
7
- data.tar.gz: 62e777daf244f5cfe6b9ef89cdcdd8b07791808324713db58a7242825cf9d3d82f4978c920639bbc603dae625fbe819a41fdb7b95897d3f619d8d80eb9195794
6
+ metadata.gz: 575d83be39d544535c060e113934f147e94a81f97423b3207036610edc7b8e5a230dd46301e7e20253cbd477cf1ef2ba8ead19dc610a465665c10238c5357090
7
+ data.tar.gz: 033f5f5b7063c908fb250198789e053c02b512e9c04e1d56fe8ac01486c93030df7dc7046cf4923cdb2f3282c8f1ad066f97ed8319f6061ba9f1a5a275b9abda
@@ -1,114 +1,10 @@
1
- require "async/task"
1
+ require_relative "limiter/concurrent"
2
+ require_relative "limiter/unlimited"
3
+ require_relative "limiter/window/continuous"
4
+ require_relative "limiter/window/fixed"
5
+ require_relative "limiter/window/sliding"
2
6
 
3
7
  module Async
4
- # Base class for all the limiters.
5
- class Limiter
6
- Error = Class.new(StandardError)
7
- ArgumentError = Class.new(Error)
8
-
9
- MAX_LIMIT = Float::INFINITY
10
- MIN_LIMIT = Float::MIN
11
-
12
- attr_reader :count
13
-
14
- attr_reader :limit
15
-
16
- attr_reader :waiting
17
-
18
- def initialize(limit = 1, parent: nil,
19
- max_limit: MAX_LIMIT, min_limit: MIN_LIMIT)
20
- @count = 0
21
- @limit = limit
22
- @waiting = []
23
- @parent = parent
24
- @max_limit = max_limit
25
- @min_limit = min_limit
26
-
27
- validate!
28
- end
29
-
30
- def blocking?
31
- @count >= @limit
32
- end
33
-
34
- def async(parent: (@parent or Task.current), **options)
35
- acquire
36
- parent.async(**options) do |task|
37
- yield task
38
- ensure
39
- release
40
- end
41
- end
42
-
43
- def acquire
44
- wait
45
- @count += 1
46
- end
47
-
48
- def release
49
- @count -= 1
50
-
51
- while under_limit? && (fiber = @waiting.shift)
52
- fiber.resume if fiber.alive?
53
- end
54
- end
55
-
56
- def increase_limit(number = 1)
57
- new_limit = @limit + number
58
- return false if new_limit > @max_limit
59
-
60
- @limit = new_limit
61
- end
62
-
63
- def decrease_limit(number = 1)
64
- new_limit = @limit - number
65
- return false if new_limit < @min_limit
66
-
67
- @limit = new_limit
68
- end
69
-
70
- def waiting_count
71
- @waiting.size
72
- end
73
-
74
- private
75
-
76
- def under_limit?
77
- available_units.positive?
78
- end
79
-
80
- def available_units
81
- @limit - @count
82
- end
83
-
84
- def wait
85
- fiber = Fiber.current
86
-
87
- if blocking?
88
- @waiting << fiber
89
- Task.yield while blocking?
90
- end
91
- rescue Exception
92
- @waiting.delete(fiber)
93
- raise
94
- end
95
-
96
- def validate!
97
- if @max_limit < @min_limit
98
- raise ArgumentError, "max_limit is lower than min_limit"
99
- end
100
-
101
- unless @max_limit.positive?
102
- raise ArgumentError, "max_limit must be positive"
103
- end
104
-
105
- unless @min_limit.positive?
106
- raise ArgumentError, "min_limit must be positive"
107
- end
108
-
109
- unless @limit.between?(@min_limit, @max_limit)
110
- raise ArgumentError, "limit not between min_limit and max_limit"
111
- end
112
- end
8
+ module Limiter
113
9
  end
114
10
  end
@@ -1,10 +1,99 @@
1
- require_relative "../limiter"
1
+ require "async/task"
2
+ require_relative "constants"
2
3
 
3
4
  module Async
4
- class Limiter
5
- # Allows running x units of work concurrently.
6
- # Has the same logic as Async::Semaphore.
7
- class Concurrent < Limiter
5
+ module Limiter
6
+ class Concurrent
7
+ attr_reader :count
8
+
9
+ attr_reader :limit
10
+
11
+ def initialize(limit = 1, parent: nil, queue: [])
12
+ @count = 0
13
+ @limit = limit
14
+ @waiting = queue
15
+ @parent = parent
16
+
17
+ validate!
18
+ end
19
+
20
+ def blocking?
21
+ limit_blocking?
22
+ end
23
+
24
+ def async(*queue_args, parent: (@parent || Task.current), **options)
25
+ acquire(*queue_args)
26
+ parent.async(**options) do |task|
27
+ yield task
28
+ ensure
29
+ release
30
+ end
31
+ end
32
+
33
+ def sync(*queue_args, &block)
34
+ acquire(*queue_args, &block)
35
+ end
36
+
37
+ def acquire(*queue_args)
38
+ wait(*queue_args)
39
+ @count += 1
40
+
41
+ return unless block_given?
42
+
43
+ begin
44
+ yield
45
+ ensure
46
+ release
47
+ end
48
+ end
49
+
50
+ def release
51
+ @count -= 1
52
+
53
+ resume_waiting
54
+ end
55
+
56
+ def limit=(new_limit)
57
+ validate_limit!(new_limit)
58
+
59
+ @limit = new_limit
60
+ end
61
+
62
+ private
63
+
64
+ def limit_blocking?
65
+ @count >= @limit
66
+ end
67
+
68
+ def wait(*queue_args)
69
+ fiber = Fiber.current
70
+
71
+ if blocking?
72
+ @waiting.push(fiber, *queue_args) # queue_args used for custom queues
73
+ Task.yield while blocking?
74
+ end
75
+ rescue Exception # rubocop:disable Lint/RescueException
76
+ @waiting.delete(fiber)
77
+ raise
78
+ end
79
+
80
+ def resume_waiting
81
+ while !blocking? && (fiber = @waiting.shift)
82
+ fiber.resume if fiber.alive?
83
+ end
84
+ end
85
+
86
+ def validate!
87
+ if @limit.finite? && (@limit % 1).nonzero?
88
+ raise ArgumentError, "limit must be a whole number"
89
+ end
90
+
91
+ validate_limit!
92
+ end
93
+
94
+ def validate_limit!(value = @limit)
95
+ raise ArgumentError, "limit must be greater than 1" if value < 1
96
+ end
8
97
  end
9
98
  end
10
99
  end
@@ -0,0 +1,6 @@
1
+ module Async
2
+ module Limiter
3
+ Error = Class.new(StandardError)
4
+ ArgumentError = Class.new(Error)
5
+ end
6
+ end
@@ -0,0 +1,51 @@
1
+ require "async/task"
2
+
3
+ module Async
4
+ module Limiter
5
+ class Unlimited
6
+ attr_reader :count
7
+
8
+ def initialize(parent: nil)
9
+ @count = 0
10
+ @parent = parent
11
+ end
12
+
13
+ def limit
14
+ Float::INFINITY
15
+ end
16
+
17
+ def blocking?
18
+ false
19
+ end
20
+
21
+ def async(parent: (@parent || Task.current), **options)
22
+ acquire
23
+ parent.async(**options) do |task|
24
+ yield task
25
+ ensure
26
+ release
27
+ end
28
+ end
29
+
30
+ def sync(*queue_args, &block)
31
+ acquire(*queue_args, &block)
32
+ end
33
+
34
+ def acquire
35
+ @count += 1
36
+
37
+ return unless block_given?
38
+
39
+ begin
40
+ yield
41
+ ensure
42
+ release
43
+ end
44
+ end
45
+
46
+ def release
47
+ @count -= 1
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,5 +1,5 @@
1
1
  module Async
2
- class Limiter
3
- VERSION = "0.0.1"
2
+ module Limiter
3
+ VERSION = "1.3.1"
4
4
  end
5
5
  end
@@ -0,0 +1,275 @@
1
+ require "async/clock"
2
+ require "async/task"
3
+ require_relative "constants"
4
+
5
+ module Async
6
+ module Limiter
7
+ class Window
8
+ TYPES = %i[fixed sliding].freeze
9
+ NULL_TIME = -1
10
+
11
+ attr_reader :count
12
+
13
+ attr_reader :type
14
+
15
+ attr_reader :lock
16
+
17
+ def initialize(limit = 1, type: :fixed, window: 1, parent: nil,
18
+ burstable: true, lock: true, queue: [])
19
+ @count = 0
20
+ @input_limit = @limit = limit
21
+ @type = type
22
+ @input_window = @window = window
23
+ @parent = parent
24
+ @burstable = burstable
25
+ @lock = lock
26
+
27
+ @waiting = queue
28
+ @scheduler_task = nil
29
+
30
+ @window_frame_start_time = NULL_TIME
31
+ @window_start_time = NULL_TIME
32
+ @window_count = 0
33
+
34
+ update_concurrency
35
+ validate!
36
+ end
37
+
38
+ def limit
39
+ @input_limit
40
+ end
41
+
42
+ def window
43
+ @input_window
44
+ end
45
+
46
+ def blocking?
47
+ limit_blocking? || window_blocking? || window_frame_blocking?
48
+ end
49
+
50
+ def async(*queue_args, parent: (@parent || Task.current), **options)
51
+ acquire(*queue_args)
52
+ parent.async(**options) do |task|
53
+ yield task
54
+ ensure
55
+ release
56
+ end
57
+ end
58
+
59
+ def sync(*queue_args, &block)
60
+ acquire(*queue_args, &block)
61
+ end
62
+
63
+ def acquire(*queue_args)
64
+ wait(*queue_args)
65
+ @count += 1
66
+
67
+ current_time = Clock.now
68
+
69
+ if window_changed?(current_time)
70
+ @window_start_time =
71
+ if @type == :sliding
72
+ current_time
73
+ elsif @type == :fixed
74
+ (current_time / @window).to_i * @window
75
+ else
76
+ raise "invalid type #{@type}"
77
+ end
78
+
79
+ @window_count = 1
80
+ else
81
+ @window_count += 1
82
+ end
83
+
84
+ @window_frame_start_time = current_time
85
+
86
+ return unless block_given?
87
+
88
+ begin
89
+ yield
90
+ ensure
91
+ release
92
+ end
93
+ end
94
+
95
+ def release
96
+ @count -= 1
97
+
98
+ # We're resuming waiting fibers when lock is released.
99
+ resume_waiting if @lock
100
+ end
101
+
102
+ def limit=(new_limit)
103
+ validate_limit!(new_limit)
104
+ @input_limit = @limit = new_limit
105
+
106
+ update_concurrency
107
+ resume_waiting
108
+ reschedule if reschedule?
109
+
110
+ limit
111
+ end
112
+
113
+ def window=(new_window)
114
+ validate_window!(new_window)
115
+ @input_window = @window = new_window
116
+
117
+ update_concurrency
118
+ resume_waiting
119
+ reschedule if reschedule?
120
+
121
+ window
122
+ end
123
+
124
+ private
125
+
126
+ def limit_blocking?
127
+ @lock && @count >= @limit
128
+ end
129
+
130
+ def window_blocking?
131
+ return false unless @burstable
132
+ return false if window_changed?
133
+
134
+ @window_count >= @limit
135
+ end
136
+
137
+ def window_frame_blocking?
138
+ return false if @burstable
139
+ return false if window_frame_changed?
140
+
141
+ true
142
+ end
143
+
144
+ def window_changed?(time = Clock.now)
145
+ @window_start_time + @window <= time
146
+ end
147
+
148
+ def window_frame_changed?
149
+ @window_frame_start_time + window_frame <= Clock.now
150
+ end
151
+
152
+ def wait(*queue_args)
153
+ fiber = Fiber.current
154
+
155
+ # @waiting.any? check prevents fibers resumed via scheduler from
156
+ # slipping in operations before other waiting fibers get resumed.
157
+ if blocking? || @waiting.any?
158
+ @waiting.push(fiber, *queue_args) # queue_args used for custom queues
159
+ schedule if schedule?
160
+ loop do
161
+ Task.yield # run this line at least once
162
+ break unless blocking?
163
+ end
164
+ end
165
+ rescue Exception # rubocop:disable Lint/RescueException
166
+ @waiting.delete(fiber)
167
+ raise
168
+ end
169
+
170
+ def schedule?
171
+ @scheduler_task.nil? &&
172
+ @waiting.any? &&
173
+ !limit_blocking?
174
+ end
175
+
176
+ # Schedule resuming waiting tasks.
177
+ def schedule(parent: @parent || Task.current)
178
+ @scheduler_task ||=
179
+ parent.async { |task|
180
+ while @waiting.any? && !limit_blocking?
181
+ delay = [next_acquire_time - Async::Clock.now, 0].max
182
+ task.sleep(delay) if delay.positive?
183
+ resume_waiting
184
+ end
185
+
186
+ @scheduler_task = nil
187
+ }
188
+ end
189
+
190
+ def reschedule?
191
+ @scheduler_task &&
192
+ @waiting.any? &&
193
+ !limit_blocking?
194
+ end
195
+
196
+ def reschedule
197
+ @scheduler_task.stop
198
+ @scheduler_task = nil
199
+
200
+ schedule
201
+ end
202
+
203
+ def resume_waiting
204
+ while !blocking? && (fiber = @waiting.shift)
205
+ fiber.resume if fiber.alive?
206
+ end
207
+
208
+ # Long running non-burstable tasks may end while
209
+ # #window_frame_blocking?. Start a scheduler if one is not running.
210
+ schedule if schedule?
211
+ end
212
+
213
+ def next_acquire_time
214
+ if @burstable
215
+ @window_start_time + @window # next window start time
216
+ else
217
+ @window_frame_start_time + window_frame # next window frame start time
218
+ end
219
+ end
220
+
221
+ def window_frame
222
+ @window.to_f / @limit
223
+ end
224
+
225
+ # If limit is a decimal number (e.g. 0.5) it needs to be adjusted.
226
+ # Make @limit a whole number and adjust @window appropriately.
227
+ def update_concurrency
228
+ # reset @limit and @window
229
+ @limit = @input_limit
230
+ @window = @input_window
231
+
232
+ return if @input_limit.infinite?
233
+ return if (@input_limit % 1).zero?
234
+
235
+ # @input_limit is a decimal number
236
+ case @input_limit
237
+ when 0...1
238
+ @window = @input_window / @input_limit
239
+ @limit = 1
240
+ when (1..)
241
+ if @input_window >= 2
242
+ @window = @input_window * @input_limit.floor / @input_limit
243
+ @limit = @input_limit.floor
244
+ else
245
+ @window = @input_window * @input_limit.ceil / @input_limit
246
+ @limit = @input_limit.ceil
247
+ end
248
+ else
249
+ raise "invalid limit #{@input_limit}"
250
+ end
251
+ end
252
+
253
+ def validate!
254
+ unless TYPES.include?(@type)
255
+ raise ArgumentError, "invalid type #{@type.inspect}"
256
+ end
257
+
258
+ validate_limit!
259
+ validate_window!
260
+ end
261
+
262
+ def validate_limit!(value = @input_limit)
263
+ unless value.positive?
264
+ raise ArgumentError, "limit must be positive number"
265
+ end
266
+ end
267
+
268
+ def validate_window!(value = @input_window)
269
+ unless value.positive?
270
+ raise ArgumentError, "window must be positive number"
271
+ end
272
+ end
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,21 @@
1
+ require_relative "../window"
2
+
3
+ module Async
4
+ module Limiter
5
+ class Window
6
+ class Continuous < Window
7
+ def initialize(limit = 1, window: 1, parent: nil, lock: true, queue: [])
8
+ super(
9
+ limit,
10
+ type: :sliding, # type doesn't matter, but sliding is less work
11
+ burstable: false,
12
+ window: window,
13
+ parent: parent,
14
+ lock: lock,
15
+ queue: queue
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ require_relative "../window"
2
+
3
+ module Async
4
+ module Limiter
5
+ class Window
6
+ class Fixed < Window
7
+ def initialize(limit = 1, window: 1, parent: nil, lock: true, queue: [])
8
+ super(
9
+ limit,
10
+ type: :fixed,
11
+ burstable: true,
12
+ window: window,
13
+ parent: parent,
14
+ lock: lock,
15
+ queue: queue
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ require_relative "../window"
2
+
3
+ module Async
4
+ module Limiter
5
+ class Window
6
+ class Sliding < Window
7
+ def initialize(limit = 1, window: 1, parent: nil, lock: true, queue: [])
8
+ super(
9
+ limit,
10
+ type: :sliding,
11
+ burstable: true,
12
+ window: window,
13
+ parent: parent,
14
+ lock: lock,
15
+ queue: queue
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-limiter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bruno Sutic
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-03 00:00:00.000000000 Z
11
+ date: 2020-10-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -24,6 +24,48 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.26'
27
+ - !ruby/object:Gem::Dependency
28
+ name: async-rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.14'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.14'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.9'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.9'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop-rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.43'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.43'
27
69
  - !ruby/object:Gem::Dependency
28
70
  name: standard
29
71
  requirement: !ruby/object:Gem::Requirement
@@ -46,10 +88,13 @@ extra_rdoc_files: []
46
88
  files:
47
89
  - lib/async/limiter.rb
48
90
  - lib/async/limiter/concurrent.rb
49
- - lib/async/limiter/delay.rb
50
- - lib/async/limiter/fixed_window.rb
51
- - lib/async/limiter/sliding_window.rb
91
+ - lib/async/limiter/constants.rb
92
+ - lib/async/limiter/unlimited.rb
52
93
  - lib/async/limiter/version.rb
94
+ - lib/async/limiter/window.rb
95
+ - lib/async/limiter/window/continuous.rb
96
+ - lib/async/limiter/window/fixed.rb
97
+ - lib/async/limiter/window/sliding.rb
53
98
  homepage: https://github.com/bruno-/async-limiter
54
99
  licenses:
55
100
  - MIT
@@ -69,7 +114,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
69
114
  - !ruby/object:Gem::Version
70
115
  version: '0'
71
116
  requirements: []
72
- rubygems_version: 3.1.2
117
+ rubygems_version: 3.2.0.rc.1
73
118
  signing_key:
74
119
  specification_version: 4
75
120
  summary: Async limiters
@@ -1,47 +0,0 @@
1
- require "async/clock"
2
- require_relative "../limiter"
3
-
4
- module Async
5
- class Limiter
6
- # Ensures units are evenly acquired during the sliding time window.
7
- # Example: If limit is 2 you can perform one operation every 500ms. First
8
- # operation at 10:10:10.000, and then another one at 10:10:10.500.
9
- class Throttle < Limiter
10
- attr_reader :window
11
-
12
- def initialize(*args, window: 1, min_limit: 0, **options)
13
- super(*args, min_limit: min_limit, **options)
14
-
15
- @window = window
16
- @last_acquired_time = -1
17
- end
18
-
19
- def blocking?
20
- super && current_delay.positive?
21
- end
22
-
23
- def acquire
24
- super
25
- @last_acquired_time = now
26
- end
27
-
28
- def delay
29
- @window.to_f / @limit
30
- end
31
-
32
- private
33
-
34
- def now
35
- Clock.now
36
- end
37
-
38
- def current_delay
39
- [delay - elapsed_time, 0].max
40
- end
41
-
42
- def elapsed_time
43
- now - @last_acquired_time
44
- end
45
- end
46
- end
47
- end
@@ -1,53 +0,0 @@
1
- require "async/clock"
2
- require_relative "../limiter"
3
-
4
- module Async
5
- class Limiter
6
- # Ensures units are acquired during the time window.
7
- # Example: You can perform N operations at 10:10:10.999, and then can
8
- # perform another N operations at 10:10:11.000.
9
- class FixedWindow < Limiter
10
- attr_reader :window
11
-
12
- def initialize(*args, window: 1, **options)
13
- super(*args, **options)
14
-
15
- @window = window
16
- @acquired_window_indexes = []
17
- end
18
-
19
- def blocking?
20
- super && window_limited?
21
- end
22
-
23
- def acquire
24
- super
25
- @acquired_window_indexes.unshift(window_index)
26
- # keep more entries in case a limit is increased
27
- @acquired_window_indexes = @acquired_window_indexes.first(keep_limit)
28
- end
29
-
30
- private
31
-
32
- def window_limited?
33
- first_index_in_limit_scope == window_index
34
- end
35
-
36
- def first_index_in_limit_scope
37
- @acquired_window_indexes.fetch(@limit - 1) { -1 }
38
- end
39
-
40
- def window_index
41
- (now / @window).floor
42
- end
43
-
44
- def keep_limit
45
- @max_limit.infinite? ? @limit * 10 : @max_limit
46
- end
47
-
48
- def now
49
- Clock.now
50
- end
51
- end
52
- end
53
- end
@@ -1,53 +0,0 @@
1
- require "async/clock"
2
- require_relative "../limiter"
3
-
4
- module Async
5
- class Limiter
6
- # Ensures units are acquired during the sliding time window.
7
- # Example: You can perform N operations at 10:10:10.999 but can't perform
8
- # another N operations until 10:10:11.999.
9
- class SlidingWindow < Limiter
10
- attr_reader :window
11
-
12
- def initialize(*args, window: 1, min_limit: 0, **options)
13
- super(*args, min_limit: min_limit, **options)
14
-
15
- @window = window
16
- @acquired_times = []
17
- end
18
-
19
- def blocking?
20
- super && window_limited?
21
- end
22
-
23
- def acquire
24
- super
25
- @acquired_times.unshift(now)
26
- # keep more entries in case a limit is increased
27
- @acquired_times = @acquired_times.first(keep_limit)
28
- end
29
-
30
- private
31
-
32
- def window_limited?
33
- first_time_in_limit_scope >= window_start_time
34
- end
35
-
36
- def first_time_in_limit_scope
37
- @acquired_times.fetch(@limit - 1) { -1 }
38
- end
39
-
40
- def window_start_time
41
- now - @window
42
- end
43
-
44
- def keep_limit
45
- @max_limit.infinite? ? @limit * 10 : @max_limit
46
- end
47
-
48
- def now
49
- Clock.now
50
- end
51
- end
52
- end
53
- end