async-limiter 1.5.4 → 2.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
- checksums.yaml.gz.sig +0 -0
- data/context/generic-limiter.md +167 -0
- data/context/getting-started.md +226 -0
- data/context/index.yaml +41 -0
- data/context/limited-limiter.md +184 -0
- data/context/queued-limiter.md +109 -0
- data/context/timing-strategies.md +666 -0
- data/context/token-usage.md +85 -0
- data/lib/async/limiter/generic.rb +160 -0
- data/lib/async/limiter/limited.rb +103 -0
- data/lib/async/limiter/queued.rb +85 -0
- data/lib/async/limiter/timing/burst.rb +153 -0
- data/lib/async/limiter/timing/fixed_window.rb +42 -0
- data/lib/async/limiter/timing/leaky_bucket.rb +146 -0
- data/lib/async/limiter/timing/none.rb +56 -0
- data/lib/async/limiter/timing/ordered.rb +58 -0
- data/lib/async/limiter/timing/sliding_window.rb +152 -0
- data/lib/async/limiter/token.rb +102 -0
- data/lib/async/limiter/version.rb +10 -3
- data/lib/async/limiter.rb +21 -7
- data/lib/metrics/provider/async/limiter/generic.rb +74 -0
- data/lib/metrics/provider/async/limiter.rb +7 -0
- data/lib/traces/provider/async/limiter/generic.rb +41 -0
- data/lib/traces/provider/async/limiter.rb +7 -0
- data/license.md +25 -0
- data/readme.md +45 -0
- data/releases.md +50 -0
- data.tar.gz.sig +0 -0
- metadata +68 -83
- metadata.gz.sig +0 -0
- data/lib/async/limiter/concurrent.rb +0 -101
- data/lib/async/limiter/constants.rb +0 -6
- data/lib/async/limiter/unlimited.rb +0 -53
- data/lib/async/limiter/window/continuous.rb +0 -21
- data/lib/async/limiter/window/fixed.rb +0 -21
- data/lib/async/limiter/window/sliding.rb +0 -21
- data/lib/async/limiter/window.rb +0 -296
data/lib/async/limiter/window.rb
DELETED
@@ -1,296 +0,0 @@
|
|
1
|
-
require "async/clock"
|
2
|
-
require "async/notification"
|
3
|
-
require "async/task"
|
4
|
-
require_relative "constants"
|
5
|
-
|
6
|
-
module Async
|
7
|
-
module Limiter
|
8
|
-
class Window
|
9
|
-
TYPES = %i[fixed sliding].freeze
|
10
|
-
NULL_TIME = -1
|
11
|
-
|
12
|
-
attr_reader :count
|
13
|
-
|
14
|
-
attr_reader :type
|
15
|
-
|
16
|
-
attr_reader :lock
|
17
|
-
|
18
|
-
def initialize(limit = 1, type: :fixed, window: 1, parent: nil,
|
19
|
-
burstable: true, lock: true, queue: [])
|
20
|
-
@count = 0
|
21
|
-
@input_limit = @limit = limit
|
22
|
-
@type = type
|
23
|
-
@input_window = @window = window
|
24
|
-
@parent = parent
|
25
|
-
@burstable = burstable
|
26
|
-
@lock = lock
|
27
|
-
|
28
|
-
@waiting = queue
|
29
|
-
@scheduler = nil
|
30
|
-
@yield_wait = false
|
31
|
-
@yield_notification = Notification.new
|
32
|
-
|
33
|
-
@window_frame_start_time = NULL_TIME
|
34
|
-
@window_start_time = NULL_TIME
|
35
|
-
@window_count = 0
|
36
|
-
|
37
|
-
update_concurrency
|
38
|
-
validate!
|
39
|
-
end
|
40
|
-
|
41
|
-
def limit
|
42
|
-
@input_limit
|
43
|
-
end
|
44
|
-
|
45
|
-
def window
|
46
|
-
@input_window
|
47
|
-
end
|
48
|
-
|
49
|
-
def blocking?
|
50
|
-
limit_blocking? || window_blocking? || window_frame_blocking?
|
51
|
-
end
|
52
|
-
|
53
|
-
def async(*queue_args, parent: (@parent || Task.current), **options)
|
54
|
-
acquire(*queue_args)
|
55
|
-
parent.async(**options) do |task|
|
56
|
-
yield task
|
57
|
-
ensure
|
58
|
-
release
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def sync(*queue_args)
|
63
|
-
acquire(*queue_args) do
|
64
|
-
yield Task.current
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
def acquire(*queue_args)
|
69
|
-
wait(*queue_args)
|
70
|
-
@count += 1
|
71
|
-
|
72
|
-
current_time = Clock.now
|
73
|
-
|
74
|
-
if window_changed?(current_time)
|
75
|
-
@window_start_time =
|
76
|
-
if @type == :sliding
|
77
|
-
current_time
|
78
|
-
elsif @type == :fixed
|
79
|
-
(current_time / @window).to_i * @window
|
80
|
-
else
|
81
|
-
raise "invalid type #{@type}"
|
82
|
-
end
|
83
|
-
|
84
|
-
@window_count = 1
|
85
|
-
else
|
86
|
-
@window_count += 1
|
87
|
-
end
|
88
|
-
|
89
|
-
@window_frame_start_time = current_time
|
90
|
-
|
91
|
-
return unless block_given?
|
92
|
-
|
93
|
-
begin
|
94
|
-
yield
|
95
|
-
ensure
|
96
|
-
release
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
def release
|
101
|
-
@count -= 1
|
102
|
-
|
103
|
-
# We're resuming waiting fibers when lock is released.
|
104
|
-
resume_waiting if @lock
|
105
|
-
end
|
106
|
-
|
107
|
-
def limit=(new_limit)
|
108
|
-
validate_limit!(new_limit)
|
109
|
-
@input_limit = @limit = new_limit
|
110
|
-
|
111
|
-
update_concurrency
|
112
|
-
resume_waiting
|
113
|
-
reschedule if reschedule?
|
114
|
-
|
115
|
-
limit
|
116
|
-
end
|
117
|
-
|
118
|
-
def window=(new_window)
|
119
|
-
validate_window!(new_window)
|
120
|
-
@input_window = @window = new_window
|
121
|
-
|
122
|
-
update_concurrency
|
123
|
-
resume_waiting
|
124
|
-
reschedule if reschedule?
|
125
|
-
|
126
|
-
window
|
127
|
-
end
|
128
|
-
|
129
|
-
private
|
130
|
-
|
131
|
-
def limit_blocking?
|
132
|
-
@lock && @count >= @limit
|
133
|
-
end
|
134
|
-
|
135
|
-
def window_blocking?
|
136
|
-
return false unless @burstable
|
137
|
-
return false if window_changed?
|
138
|
-
|
139
|
-
@window_count >= @limit
|
140
|
-
end
|
141
|
-
|
142
|
-
def window_frame_blocking?
|
143
|
-
return false if @burstable
|
144
|
-
return false if window_frame_changed?
|
145
|
-
|
146
|
-
true
|
147
|
-
end
|
148
|
-
|
149
|
-
def window_changed?(time = Clock.now)
|
150
|
-
@window_start_time + @window <= time
|
151
|
-
end
|
152
|
-
|
153
|
-
def window_frame_changed?
|
154
|
-
@window_frame_start_time + window_frame <= Clock.now
|
155
|
-
end
|
156
|
-
|
157
|
-
def wait(*queue_args)
|
158
|
-
fiber = Fiber.current
|
159
|
-
|
160
|
-
# @waiting.any? check prevents fibers resumed via scheduler from
|
161
|
-
# slipping in operations before other waiting fibers get resumed.
|
162
|
-
if blocking? || @waiting.any?
|
163
|
-
@waiting.push(fiber, *queue_args) # queue_args used for custom queues
|
164
|
-
@yield_wait = true
|
165
|
-
schedule if schedule?
|
166
|
-
|
167
|
-
# Non-blocking signal, prevents race condition where scheduler would
|
168
|
-
# start resuming waiting fibers before below 'Task.yield' was reached.
|
169
|
-
@yield_notification.signal
|
170
|
-
@yield_wait = false # we're out of the woods
|
171
|
-
|
172
|
-
loop do
|
173
|
-
Task.yield # run this line at least once
|
174
|
-
break unless blocking?
|
175
|
-
end
|
176
|
-
end
|
177
|
-
rescue Exception # rubocop:disable Lint/RescueException
|
178
|
-
@waiting.delete(fiber)
|
179
|
-
raise
|
180
|
-
end
|
181
|
-
|
182
|
-
def schedule?
|
183
|
-
@scheduler.nil? &&
|
184
|
-
@waiting.any? &&
|
185
|
-
!limit_blocking?
|
186
|
-
end
|
187
|
-
|
188
|
-
# Schedule resuming waiting tasks.
|
189
|
-
def schedule(parent: @parent || Task.current)
|
190
|
-
return @scheduler if @scheduler
|
191
|
-
|
192
|
-
parent.async(transient: true) do |task|
|
193
|
-
@scheduler = task
|
194
|
-
task.annotate("scheduling tasks for #{self.class}.")
|
195
|
-
|
196
|
-
while @waiting.any? && !limit_blocking?
|
197
|
-
delay = [next_acquire_time - Clock.now, 0].max
|
198
|
-
task.sleep(delay) if delay.positive?
|
199
|
-
|
200
|
-
# Waits for the task that started the scheduler to yield.
|
201
|
-
# See #wait for more details.
|
202
|
-
@yield_wait && @yield_notification&.wait
|
203
|
-
|
204
|
-
resume_waiting
|
205
|
-
end
|
206
|
-
ensure
|
207
|
-
@scheduler = nil
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
def reschedule?
|
212
|
-
@scheduler &&
|
213
|
-
@waiting.any? &&
|
214
|
-
!limit_blocking?
|
215
|
-
end
|
216
|
-
|
217
|
-
def reschedule
|
218
|
-
@scheduler.stop
|
219
|
-
@scheduler = nil
|
220
|
-
|
221
|
-
schedule
|
222
|
-
end
|
223
|
-
|
224
|
-
def resume_waiting
|
225
|
-
while !blocking? && (fiber = @waiting.shift)
|
226
|
-
fiber.resume if fiber.alive?
|
227
|
-
end
|
228
|
-
|
229
|
-
# Long running non-burstable tasks may end while
|
230
|
-
# #window_frame_blocking?. Start a scheduler if one is not running.
|
231
|
-
schedule if schedule?
|
232
|
-
end
|
233
|
-
|
234
|
-
def next_acquire_time
|
235
|
-
if @burstable
|
236
|
-
@window_start_time + @window # next window start time
|
237
|
-
else
|
238
|
-
@window_frame_start_time + window_frame # next window frame start time
|
239
|
-
end
|
240
|
-
end
|
241
|
-
|
242
|
-
def window_frame
|
243
|
-
@window.to_f / @limit
|
244
|
-
end
|
245
|
-
|
246
|
-
# If limit is a decimal number (e.g. 0.5) it needs to be adjusted.
|
247
|
-
# Make @limit a whole number and adjust @window appropriately.
|
248
|
-
def update_concurrency
|
249
|
-
# reset @limit and @window
|
250
|
-
@limit = @input_limit
|
251
|
-
@window = @input_window
|
252
|
-
|
253
|
-
return if @input_limit.infinite?
|
254
|
-
return if (@input_limit % 1).zero?
|
255
|
-
|
256
|
-
# @input_limit is a decimal number
|
257
|
-
case @input_limit
|
258
|
-
when 0...1
|
259
|
-
@window = @input_window / @input_limit
|
260
|
-
@limit = 1
|
261
|
-
when (1..)
|
262
|
-
if @input_window >= 2
|
263
|
-
@window = @input_window * @input_limit.floor / @input_limit
|
264
|
-
@limit = @input_limit.floor
|
265
|
-
else
|
266
|
-
@window = @input_window * @input_limit.ceil / @input_limit
|
267
|
-
@limit = @input_limit.ceil
|
268
|
-
end
|
269
|
-
else
|
270
|
-
raise "invalid limit #{@input_limit}"
|
271
|
-
end
|
272
|
-
end
|
273
|
-
|
274
|
-
def validate!
|
275
|
-
unless TYPES.include?(@type)
|
276
|
-
raise ArgumentError, "invalid type #{@type.inspect}"
|
277
|
-
end
|
278
|
-
|
279
|
-
validate_limit!
|
280
|
-
validate_window!
|
281
|
-
end
|
282
|
-
|
283
|
-
def validate_limit!(value = @input_limit)
|
284
|
-
unless value.positive?
|
285
|
-
raise ArgumentError, "limit must be positive number"
|
286
|
-
end
|
287
|
-
end
|
288
|
-
|
289
|
-
def validate_window!(value = @input_window)
|
290
|
-
unless value.positive?
|
291
|
-
raise ArgumentError, "window must be positive number"
|
292
|
-
end
|
293
|
-
end
|
294
|
-
end
|
295
|
-
end
|
296
|
-
end
|