async-limiter 0.0.1 → 1.3.1

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