async-limiter 0.0.1 → 1.0.0
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 +4 -4
- data/lib/async/limiter.rb +6 -110
- data/lib/async/limiter/concurrent.rb +82 -5
- data/lib/async/limiter/constants.rb +6 -0
- data/lib/async/limiter/unlimited.rb +39 -0
- data/lib/async/limiter/version.rb +2 -2
- data/lib/async/limiter/window.rb +263 -0
- data/lib/async/limiter/window/continuous.rb +20 -0
- data/lib/async/limiter/window/fixed.rb +20 -0
- data/lib/async/limiter/window/sliding.rb +20 -0
- metadata +51 -6
- data/lib/async/limiter/delay.rb +0 -47
- data/lib/async/limiter/fixed_window.rb +0 -53
- data/lib/async/limiter/sliding_window.rb +0 -53
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6d203b60d1b006ce7dc199fd37dbfeac43468324a083a02a233dbf7fe1ebc61f
|
4
|
+
data.tar.gz: f7cd17453e047205743648458058f30365d795514fba956dca1aa910ab34816e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2721ba28df1d8aca742ba974ace3146d136734e9482c39429957dd0ff9022843eef5dc6bdbda9c9aa60017c019ab2011907a31a6d30d8a16f7392b2c70daf04a
|
7
|
+
data.tar.gz: 7ea2cbb70d1f192d947d42594711fdf37da8f86a56d624925ec3b8fdb31957248ecbb1b945f62f7c4dfc755c947ede0ec65f8035c2806fd942ebd60ab430194d
|
data/lib/async/limiter.rb
CHANGED
@@ -1,114 +1,10 @@
|
|
1
|
-
|
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
|
-
|
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,87 @@
|
|
1
|
-
|
1
|
+
require "async/task"
|
2
|
+
require_relative "constants"
|
2
3
|
|
3
4
|
module Async
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
module Limiter
|
6
|
+
class Concurrent
|
7
|
+
attr_reader :count
|
8
|
+
|
9
|
+
attr_reader :limit
|
10
|
+
|
11
|
+
def initialize(limit = 1, parent: nil)
|
12
|
+
@count = 0
|
13
|
+
@limit = limit
|
14
|
+
@waiting = []
|
15
|
+
@parent = parent
|
16
|
+
|
17
|
+
validate!
|
18
|
+
end
|
19
|
+
|
20
|
+
def blocking?
|
21
|
+
limit_blocking?
|
22
|
+
end
|
23
|
+
|
24
|
+
def async(parent: (@parent || Task.current), **options)
|
25
|
+
acquire
|
26
|
+
parent.async(**options) do |task|
|
27
|
+
yield task
|
28
|
+
ensure
|
29
|
+
release
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def acquire
|
34
|
+
wait
|
35
|
+
@count += 1
|
36
|
+
end
|
37
|
+
|
38
|
+
def release
|
39
|
+
@count -= 1
|
40
|
+
|
41
|
+
resume_waiting
|
42
|
+
end
|
43
|
+
|
44
|
+
def limit=(new_limit)
|
45
|
+
validate_limit!(new_limit)
|
46
|
+
|
47
|
+
@limit = new_limit
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def limit_blocking?
|
53
|
+
@count >= @limit
|
54
|
+
end
|
55
|
+
|
56
|
+
def wait
|
57
|
+
fiber = Fiber.current
|
58
|
+
|
59
|
+
if blocking?
|
60
|
+
@waiting << fiber
|
61
|
+
Task.yield while blocking?
|
62
|
+
end
|
63
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
64
|
+
@waiting.delete(fiber)
|
65
|
+
raise
|
66
|
+
end
|
67
|
+
|
68
|
+
def resume_waiting
|
69
|
+
while !blocking? && (fiber = @waiting.shift)
|
70
|
+
fiber.resume if fiber.alive?
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def validate!
|
75
|
+
if @limit.finite? && (@limit % 1).nonzero?
|
76
|
+
raise ArgumentError, "limit must be a whole number"
|
77
|
+
end
|
78
|
+
|
79
|
+
validate_limit!
|
80
|
+
end
|
81
|
+
|
82
|
+
def validate_limit!(value = @limit)
|
83
|
+
raise ArgumentError, "limit must be greater than 1" if value < 1
|
84
|
+
end
|
8
85
|
end
|
9
86
|
end
|
10
87
|
end
|
@@ -0,0 +1,39 @@
|
|
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 acquire
|
31
|
+
@count += 1
|
32
|
+
end
|
33
|
+
|
34
|
+
def release
|
35
|
+
@count -= 1
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,263 @@
|
|
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)
|
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 = []
|
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(parent: (@parent || Task.current), **options)
|
51
|
+
acquire
|
52
|
+
parent.async(**options) do |task|
|
53
|
+
yield task
|
54
|
+
ensure
|
55
|
+
release
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def acquire
|
60
|
+
wait
|
61
|
+
@count += 1
|
62
|
+
|
63
|
+
current_time = Clock.now
|
64
|
+
|
65
|
+
if window_changed?(current_time)
|
66
|
+
@window_start_time =
|
67
|
+
if @type == :sliding
|
68
|
+
current_time
|
69
|
+
elsif @type == :fixed
|
70
|
+
(current_time / @window).to_i * @window
|
71
|
+
else
|
72
|
+
raise "invalid type #{@type}"
|
73
|
+
end
|
74
|
+
|
75
|
+
@window_count = 1
|
76
|
+
else
|
77
|
+
@window_count += 1
|
78
|
+
end
|
79
|
+
|
80
|
+
@window_frame_start_time = current_time
|
81
|
+
end
|
82
|
+
|
83
|
+
def release
|
84
|
+
@count -= 1
|
85
|
+
|
86
|
+
# We're resuming waiting fibers when lock is released.
|
87
|
+
resume_waiting if @lock
|
88
|
+
end
|
89
|
+
|
90
|
+
def limit=(new_limit)
|
91
|
+
validate_limit!(new_limit)
|
92
|
+
@input_limit = @limit = new_limit
|
93
|
+
|
94
|
+
update_concurrency
|
95
|
+
resume_waiting
|
96
|
+
reschedule if reschedule?
|
97
|
+
|
98
|
+
limit
|
99
|
+
end
|
100
|
+
|
101
|
+
def window=(new_window)
|
102
|
+
validate_window!(new_window)
|
103
|
+
@input_window = @window = new_window
|
104
|
+
|
105
|
+
update_concurrency
|
106
|
+
resume_waiting
|
107
|
+
reschedule if reschedule?
|
108
|
+
|
109
|
+
window
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def limit_blocking?
|
115
|
+
@lock && @count >= @limit
|
116
|
+
end
|
117
|
+
|
118
|
+
def window_blocking?
|
119
|
+
return false unless @burstable
|
120
|
+
return false if window_changed?
|
121
|
+
|
122
|
+
@window_count >= @limit
|
123
|
+
end
|
124
|
+
|
125
|
+
def window_frame_blocking?
|
126
|
+
return false if @burstable
|
127
|
+
return false if window_frame_changed?
|
128
|
+
|
129
|
+
true
|
130
|
+
end
|
131
|
+
|
132
|
+
def window_changed?(time = Clock.now)
|
133
|
+
@window_start_time + @window <= time
|
134
|
+
end
|
135
|
+
|
136
|
+
def window_frame_changed?
|
137
|
+
@window_frame_start_time + window_frame <= Clock.now
|
138
|
+
end
|
139
|
+
|
140
|
+
def wait
|
141
|
+
fiber = Fiber.current
|
142
|
+
|
143
|
+
# @waiting.any? check prevents fibers resumed via scheduler from
|
144
|
+
# slipping in operations before other waiting fibers get resumed.
|
145
|
+
if blocking? || @waiting.any?
|
146
|
+
@waiting << fiber
|
147
|
+
schedule if schedule?
|
148
|
+
loop do
|
149
|
+
Task.yield # run this line at least once
|
150
|
+
break unless blocking?
|
151
|
+
end
|
152
|
+
end
|
153
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
154
|
+
@waiting.delete(fiber)
|
155
|
+
raise
|
156
|
+
end
|
157
|
+
|
158
|
+
def schedule?
|
159
|
+
@scheduler_task.nil? &&
|
160
|
+
@waiting.any? &&
|
161
|
+
!limit_blocking?
|
162
|
+
end
|
163
|
+
|
164
|
+
# Schedule resuming waiting tasks.
|
165
|
+
def schedule(parent: @parent || Task.current)
|
166
|
+
@scheduler_task ||=
|
167
|
+
parent.async { |task|
|
168
|
+
while @waiting.any? && !limit_blocking?
|
169
|
+
delay = [next_acquire_time - Async::Clock.now, 0].max
|
170
|
+
task.sleep(delay) if delay.positive?
|
171
|
+
resume_waiting
|
172
|
+
end
|
173
|
+
|
174
|
+
@scheduler_task = nil
|
175
|
+
}
|
176
|
+
end
|
177
|
+
|
178
|
+
def reschedule?
|
179
|
+
@scheduler_task &&
|
180
|
+
@waiting.any? &&
|
181
|
+
!limit_blocking?
|
182
|
+
end
|
183
|
+
|
184
|
+
def reschedule
|
185
|
+
@scheduler_task.stop
|
186
|
+
@scheduler_task = nil
|
187
|
+
|
188
|
+
schedule
|
189
|
+
end
|
190
|
+
|
191
|
+
def resume_waiting
|
192
|
+
while !blocking? && (fiber = @waiting.shift)
|
193
|
+
fiber.resume if fiber.alive?
|
194
|
+
end
|
195
|
+
|
196
|
+
# Long running non-burstable tasks may end while
|
197
|
+
# #window_frame_blocking?. Start a scheduler if one is not running.
|
198
|
+
schedule if schedule?
|
199
|
+
end
|
200
|
+
|
201
|
+
def next_acquire_time
|
202
|
+
if @burstable
|
203
|
+
@window_start_time + @window # next window start time
|
204
|
+
else
|
205
|
+
@window_frame_start_time + window_frame # next window frame start time
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def window_frame
|
210
|
+
@window.to_f / @limit
|
211
|
+
end
|
212
|
+
|
213
|
+
# If limit is a decimal number (e.g. 0.5) it needs to be adjusted.
|
214
|
+
# Make @limit a whole number and adjust @window appropriately.
|
215
|
+
def update_concurrency
|
216
|
+
# reset @limit and @window
|
217
|
+
@limit = @input_limit
|
218
|
+
@window = @input_window
|
219
|
+
|
220
|
+
return if @input_limit.infinite?
|
221
|
+
return if (@input_limit % 1).zero?
|
222
|
+
|
223
|
+
# @input_limit is a decimal number
|
224
|
+
case @input_limit
|
225
|
+
when 0...1
|
226
|
+
@window = @input_window / @input_limit
|
227
|
+
@limit = 1
|
228
|
+
when (1..)
|
229
|
+
if @input_window >= 2
|
230
|
+
@window = @input_window * @input_limit.floor / @input_limit
|
231
|
+
@limit = @input_limit.floor
|
232
|
+
else
|
233
|
+
@window = @input_window * @input_limit.ceil / @input_limit
|
234
|
+
@limit = @input_limit.ceil
|
235
|
+
end
|
236
|
+
else
|
237
|
+
raise "invalid limit #{@input_limit}"
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def validate!
|
242
|
+
unless TYPES.include?(@type)
|
243
|
+
raise ArgumentError, "invalid type #{@type.inspect}"
|
244
|
+
end
|
245
|
+
|
246
|
+
validate_limit!
|
247
|
+
validate_window!
|
248
|
+
end
|
249
|
+
|
250
|
+
def validate_limit!(value = @input_limit)
|
251
|
+
unless value.positive?
|
252
|
+
raise ArgumentError, "limit must be positive number"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def validate_window!(value = @input_window)
|
257
|
+
unless value.positive?
|
258
|
+
raise ArgumentError, "window must be positive number"
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
@@ -0,0 +1,20 @@
|
|
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)
|
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
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
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)
|
8
|
+
super(
|
9
|
+
limit,
|
10
|
+
type: :fixed,
|
11
|
+
burstable: true,
|
12
|
+
window: window,
|
13
|
+
parent: parent,
|
14
|
+
lock: lock
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
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)
|
8
|
+
super(
|
9
|
+
limit,
|
10
|
+
type: :sliding,
|
11
|
+
burstable: true,
|
12
|
+
window: window,
|
13
|
+
parent: parent,
|
14
|
+
lock: lock
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
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
|
4
|
+
version: 1.0.0
|
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-
|
11
|
+
date: 2020-10-10 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/
|
50
|
-
- lib/async/limiter/
|
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
|
117
|
+
rubygems_version: 3.2.0.rc.1
|
73
118
|
signing_key:
|
74
119
|
specification_version: 4
|
75
120
|
summary: Async limiters
|
data/lib/async/limiter/delay.rb
DELETED
@@ -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
|