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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/context/generic-limiter.md +167 -0
  4. data/context/getting-started.md +226 -0
  5. data/context/index.yaml +41 -0
  6. data/context/limited-limiter.md +184 -0
  7. data/context/queued-limiter.md +109 -0
  8. data/context/timing-strategies.md +666 -0
  9. data/context/token-usage.md +85 -0
  10. data/lib/async/limiter/generic.rb +160 -0
  11. data/lib/async/limiter/limited.rb +103 -0
  12. data/lib/async/limiter/queued.rb +85 -0
  13. data/lib/async/limiter/timing/burst.rb +153 -0
  14. data/lib/async/limiter/timing/fixed_window.rb +42 -0
  15. data/lib/async/limiter/timing/leaky_bucket.rb +146 -0
  16. data/lib/async/limiter/timing/none.rb +56 -0
  17. data/lib/async/limiter/timing/ordered.rb +58 -0
  18. data/lib/async/limiter/timing/sliding_window.rb +152 -0
  19. data/lib/async/limiter/token.rb +102 -0
  20. data/lib/async/limiter/version.rb +10 -3
  21. data/lib/async/limiter.rb +21 -7
  22. data/lib/metrics/provider/async/limiter/generic.rb +74 -0
  23. data/lib/metrics/provider/async/limiter.rb +7 -0
  24. data/lib/traces/provider/async/limiter/generic.rb +41 -0
  25. data/lib/traces/provider/async/limiter.rb +7 -0
  26. data/license.md +25 -0
  27. data/readme.md +45 -0
  28. data/releases.md +50 -0
  29. data.tar.gz.sig +0 -0
  30. metadata +68 -83
  31. metadata.gz.sig +0 -0
  32. data/lib/async/limiter/concurrent.rb +0 -101
  33. data/lib/async/limiter/constants.rb +0 -6
  34. data/lib/async/limiter/unlimited.rb +0 -53
  35. data/lib/async/limiter/window/continuous.rb +0 -21
  36. data/lib/async/limiter/window/fixed.rb +0 -21
  37. data/lib/async/limiter/window/sliding.rb +0 -21
  38. data/lib/async/limiter/window.rb +0 -296
@@ -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