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
@@ -0,0 +1,85 @@
|
|
1
|
+
# Token Usage
|
2
|
+
|
3
|
+
This guide explains how to use tokens for advanced resource management with `async-limiter`. Tokens provide sophisticated resource handling with support for re-acquisition and automatic cleanup.
|
4
|
+
|
5
|
+
## What Are Tokens?
|
6
|
+
|
7
|
+
Tokens encapsulate acquired resources and provide advanced resource management capabilities:
|
8
|
+
|
9
|
+
- **Re-acquisition**: Re-acquire resources after release with new options.
|
10
|
+
- **Resource tracking**: Know whether a token is active or released.
|
11
|
+
- **Automatic cleanup**: Guaranteed resource release with block usage
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
Use `Token.acquire` instead of limiter-specific methods:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
require "async"
|
19
|
+
require "async/limiter"
|
20
|
+
|
21
|
+
limiter = Async::Limiter::Limited.new(5)
|
22
|
+
|
23
|
+
# Acquire a token:
|
24
|
+
token = Async::Limiter::Token.acquire(limiter)
|
25
|
+
puts "Acquired: #{token.resource}"
|
26
|
+
|
27
|
+
# Use the resource
|
28
|
+
perform_operation(token.resource)
|
29
|
+
|
30
|
+
# Release when done:
|
31
|
+
token.release
|
32
|
+
```
|
33
|
+
|
34
|
+
For most limiters, `token.resource` will simply be the value `true`.
|
35
|
+
|
36
|
+
### Block-Based Token Usage
|
37
|
+
|
38
|
+
For automatic cleanup, use blocks:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
# Automatic release with blocks:
|
42
|
+
Async::Limiter::Token.acquire(limiter) do |token|
|
43
|
+
puts "Using: #{token.resource}"
|
44
|
+
perform_operation(token.resource)
|
45
|
+
# Automatically released when block exits.
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
## Re-Acquisition
|
50
|
+
|
51
|
+
Tokens can be released and re-acquired:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
# Initial acquisition:
|
55
|
+
token = Async::Limiter::Token.acquire(limiter)
|
56
|
+
|
57
|
+
# Use and release:
|
58
|
+
perform_operation(token.resource)
|
59
|
+
token.release
|
60
|
+
|
61
|
+
# Re-acquire later with new options:
|
62
|
+
token.acquire(priority: 5, cost: 2.0)
|
63
|
+
puts "Re-acquired: #{token.resource}"
|
64
|
+
|
65
|
+
token.release
|
66
|
+
```
|
67
|
+
|
68
|
+
If you specify a timeout, and the limiter cannot be acquired, `nil` will be returned.
|
69
|
+
|
70
|
+
### Re-Acquisition with Blocks
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
token = Async::Limiter::Token.acquire(limiter)
|
74
|
+
|
75
|
+
# Use the resource:
|
76
|
+
perform_operation(token.resource)
|
77
|
+
token.release
|
78
|
+
|
79
|
+
# Re-acquire and execute with automatic cleanup:
|
80
|
+
result = token.acquire(cost: 2.0) do |resource|
|
81
|
+
perform_expensive_operation(resource)
|
82
|
+
end
|
83
|
+
|
84
|
+
puts "Operation result: #{result}"
|
85
|
+
```
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2025, by Shopify Inc.
|
5
|
+
# Copyright, 2025, by Samuel Williams.
|
6
|
+
|
7
|
+
require "async/task"
|
8
|
+
require "async/deadline"
|
9
|
+
require_relative "timing/none"
|
10
|
+
require_relative "timing/sliding_window"
|
11
|
+
require_relative "token"
|
12
|
+
|
13
|
+
module Async
|
14
|
+
module Limiter
|
15
|
+
# Generic limiter class with unlimited concurrency by default.
|
16
|
+
#
|
17
|
+
# This provides the foundation for rate limiting and concurrency control.
|
18
|
+
# Subclasses can override methods to implement specific limiting behaviors.
|
19
|
+
#
|
20
|
+
# The Generic limiter coordinates timing strategies with concurrency control,
|
21
|
+
# providing thread-safe acquisition with deadline tracking and cost-based consumption.
|
22
|
+
class Generic
|
23
|
+
# Initialize a new generic limiter.
|
24
|
+
# @parameter timing [#acquire, #wait, #maximum_cost] Strategy for timing constraints.
|
25
|
+
# @parameter parent [Async::Task, nil] Parent task for creating child tasks.
|
26
|
+
def initialize(timing: Timing::None, parent: nil, tags: nil)
|
27
|
+
@timing = timing
|
28
|
+
@parent = parent
|
29
|
+
@tags = tags
|
30
|
+
|
31
|
+
@mutex = Mutex.new
|
32
|
+
end
|
33
|
+
|
34
|
+
# @attribute [Array(String)] Tags associated with this limiter for identification or categorization.
|
35
|
+
attr :tags
|
36
|
+
|
37
|
+
# @returns [Boolean] Whether this limiter is currently limiting concurrency.
|
38
|
+
def limited?
|
39
|
+
false
|
40
|
+
end
|
41
|
+
|
42
|
+
# Execute a task asynchronously with unlimited concurrency.
|
43
|
+
# @parameter parent [Async::Task] Parent task for the new task.
|
44
|
+
# @parameter options [Hash] Additional options passed to {Async::Task#async}.
|
45
|
+
# @yields {|task| ...} The block to execute within the limiter constraints.
|
46
|
+
# @parameter task [Async::Task] The async task context.
|
47
|
+
# @returns [Async::Task] The created task.
|
48
|
+
# @asynchronous
|
49
|
+
def async(parent: (@parent || Task.current), **options)
|
50
|
+
acquire
|
51
|
+
parent.async(**options) do |task|
|
52
|
+
yield task
|
53
|
+
ensure
|
54
|
+
release
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Execute a task synchronously with unlimited concurrency.
|
59
|
+
# @yields {|task| ...} The block to execute within the limiter constraints.
|
60
|
+
# @parameter task [Async::Task] The current task context.
|
61
|
+
# @asynchronous
|
62
|
+
def sync
|
63
|
+
acquire do
|
64
|
+
yield(Task.current)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Manually acquire a resource with timing and concurrency coordination.
|
69
|
+
#
|
70
|
+
# This method provides the core acquisition logic with support for:
|
71
|
+
# - Flexible timeout handling (blocking, non-blocking, timed)
|
72
|
+
# - Cost-based consumption for timing strategies
|
73
|
+
# - Deadline tracking to prevent timeout violations
|
74
|
+
# - Automatic resource cleanup with block usage
|
75
|
+
#
|
76
|
+
# @parameter timeout [Numeric, nil] Timeout in seconds (nil = wait forever, 0 = non-blocking).
|
77
|
+
# @parameter cost [Numeric] The cost/weight of this operation for timing strategies (default: 1).
|
78
|
+
# @parameter options [Hash] Additional options passed to concurrency acquisition.
|
79
|
+
# @yields {|resource| ...} Optional block executed with automatic resource release.
|
80
|
+
# @parameter resource [Object] The acquired resource.
|
81
|
+
# @returns [Object, nil] The acquired resource, or nil if acquisition failed/timed out.
|
82
|
+
# When used with a block, returns the result of the block execution.
|
83
|
+
# @raises [ArgumentError] If cost exceeds the timing strategy's maximum supported cost.
|
84
|
+
# @asynchronous
|
85
|
+
def acquire(timeout: nil, cost: 1, **options)
|
86
|
+
# Validate cost against timing strategy capacity:
|
87
|
+
maximum_cost = @timing.maximum_cost
|
88
|
+
if cost > maximum_cost
|
89
|
+
raise ArgumentError, "Cost #{cost} exceeds maximum supported cost #{maximum_cost} for timing strategy!"
|
90
|
+
end
|
91
|
+
|
92
|
+
resource = nil
|
93
|
+
deadline = Deadline.start(timeout)
|
94
|
+
|
95
|
+
# Atomically handle timing constraints and concurrency logic:
|
96
|
+
acquire_synchronized(timeout, cost, **options) do
|
97
|
+
# Wait for timing constraints to be satisfied (mutex released during sleep)
|
98
|
+
return nil unless @timing.wait(@mutex, deadline, cost)
|
99
|
+
|
100
|
+
# Execute the concurrency-specific acquisition logic
|
101
|
+
resource = acquire_resource(deadline, **options)
|
102
|
+
|
103
|
+
# Record timing acquisition if successful
|
104
|
+
if resource
|
105
|
+
@timing.acquire(cost)
|
106
|
+
else
|
107
|
+
# `acquire_concurrency` should return nil if deadline reached:
|
108
|
+
return nil
|
109
|
+
end
|
110
|
+
|
111
|
+
resource
|
112
|
+
end
|
113
|
+
|
114
|
+
return resource unless block_given?
|
115
|
+
|
116
|
+
begin
|
117
|
+
yield(resource)
|
118
|
+
ensure
|
119
|
+
release(resource)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Release a previously acquired resource.
|
124
|
+
def release(resource = true)
|
125
|
+
release_resource(resource)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Get current limiter statistics.
|
129
|
+
# @returns [Hash] Statistics hash with current state.
|
130
|
+
def statistics
|
131
|
+
@mutex.synchronize do
|
132
|
+
{
|
133
|
+
timing: @timing.statistics
|
134
|
+
}
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
protected
|
139
|
+
|
140
|
+
def acquire_synchronized(timeout, cost, **options)
|
141
|
+
@mutex.synchronize do
|
142
|
+
yield
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Default resource acquisition for unlimited semaphore.
|
147
|
+
# Subclasses should override this method.
|
148
|
+
def acquire_resource(deadline, **options)
|
149
|
+
# Default unlimited behavior - always succeeds
|
150
|
+
true
|
151
|
+
end
|
152
|
+
|
153
|
+
# Default resource release for unlimited semaphore.
|
154
|
+
# Subclasses should override this method.
|
155
|
+
def release_resource(resource)
|
156
|
+
# Default implementation - subclasses should override.
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2020, by Bruno Sutic.
|
5
|
+
# Copyright, 2025, by Shopify Inc.
|
6
|
+
# Copyright, 2025, by Samuel Williams.
|
7
|
+
|
8
|
+
require_relative "generic"
|
9
|
+
|
10
|
+
module Async
|
11
|
+
module Limiter
|
12
|
+
# Limited concurrency limiter that enforces a strict task limit.
|
13
|
+
#
|
14
|
+
# This implements a counting semaphore that limits the number of concurrent
|
15
|
+
# operations. It coordinates with timing strategies to provide both concurrency
|
16
|
+
# and rate limiting.
|
17
|
+
#
|
18
|
+
# The Limited limiter uses a mutex and condition variable for thread-safe
|
19
|
+
# coordination, with support for deadline-aware timeout handling.
|
20
|
+
class Limited < Generic
|
21
|
+
# Initialize a limited concurrency limiter.
|
22
|
+
# @parameter limit [Integer] Maximum concurrent tasks allowed.
|
23
|
+
# @parameter timing [#acquire, #wait, #maximum_cost] Strategy for timing constraints.
|
24
|
+
# @parameter parent [Async::Task, nil] Parent task for creating child tasks.
|
25
|
+
# @raises [ArgumentError] If limit is not positive.
|
26
|
+
def initialize(limit = 1, timing: Timing::None, parent: nil)
|
27
|
+
super(timing: timing, parent: parent)
|
28
|
+
|
29
|
+
@limit = limit
|
30
|
+
@count = 0
|
31
|
+
|
32
|
+
@available = ConditionVariable.new
|
33
|
+
end
|
34
|
+
|
35
|
+
# @attribute [Integer] The maximum number of concurrent tasks.
|
36
|
+
attr_reader :limit
|
37
|
+
|
38
|
+
# @attribute [Integer] Current count of active tasks.
|
39
|
+
attr_reader :count
|
40
|
+
|
41
|
+
# Check if a new task can be acquired.
|
42
|
+
# @returns [Boolean] True if under the limit.
|
43
|
+
def limited?
|
44
|
+
@mutex.synchronize{@count >= @limit}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Update the concurrency limit.
|
48
|
+
# @parameter new_limit [Integer] The new maximum number of concurrent tasks.
|
49
|
+
# @raises [ArgumentError] If new_limit is not positive.
|
50
|
+
def limit=(new_limit)
|
51
|
+
@mutex.synchronize do
|
52
|
+
old_limit = @limit
|
53
|
+
@limit = new_limit
|
54
|
+
|
55
|
+
# Wake up waiting tasks if limit increased:
|
56
|
+
@available.broadcast if new_limit > old_limit
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Get current limiter statistics.
|
61
|
+
# @returns [Hash] Statistics hash with current state.
|
62
|
+
def statistics
|
63
|
+
@mutex.synchronize do
|
64
|
+
{
|
65
|
+
limit: @limit,
|
66
|
+
count: @count,
|
67
|
+
timing: @timing.statistics
|
68
|
+
}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
protected
|
73
|
+
|
74
|
+
# Acquire resource with optional deadline.
|
75
|
+
def acquire_resource(deadline, **options)
|
76
|
+
# Fast path: immediate return for expired deadlines, but only if at capacity
|
77
|
+
return nil if deadline&.expired? && @count >= @limit
|
78
|
+
|
79
|
+
# Wait for capacity with deadline tracking
|
80
|
+
while @count >= @limit
|
81
|
+
remaining = deadline&.remaining
|
82
|
+
return nil if remaining && remaining <= 0
|
83
|
+
|
84
|
+
unless @available.wait(@mutex, remaining)
|
85
|
+
return nil # Timeout exceeded
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
@count += 1
|
90
|
+
|
91
|
+
return true
|
92
|
+
end
|
93
|
+
|
94
|
+
# Release resource.
|
95
|
+
def release_resource(resource)
|
96
|
+
@mutex.synchronize do
|
97
|
+
@count -= 1
|
98
|
+
@available.signal
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2025, by Francisco Mejia.
|
5
|
+
# Copyright, 2025, by Shopify Inc.
|
6
|
+
# Copyright, 2025, by Samuel Williams.
|
7
|
+
|
8
|
+
require_relative "generic"
|
9
|
+
require_relative "token"
|
10
|
+
|
11
|
+
module Async
|
12
|
+
module Limiter
|
13
|
+
# Queue-based limiter that distributes pre-existing resources with priority/timeout support.
|
14
|
+
#
|
15
|
+
# This limiter manages a finite set of resources (connections, API keys, etc.)
|
16
|
+
# that are pre-populated in a queue. It provides priority-based allocation
|
17
|
+
# and timeout support for resource acquisition.
|
18
|
+
#
|
19
|
+
# Unlike Limited which counts abstract permits, Queued distributes actual
|
20
|
+
# resource objects and supports priority queues for fair allocation.
|
21
|
+
class Queued < Generic
|
22
|
+
# @returns [Queue] A default queue with a single true value.
|
23
|
+
def self.default_queue
|
24
|
+
Queue.new.tap do |queue|
|
25
|
+
queue.push(true)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Initialize a queue-based limiter.
|
30
|
+
# @parameter queue [#push, #pop, #empty?] Thread-safe queue containing pre-existing resources.
|
31
|
+
# @parameter timing [#acquire, #wait, #maximum_cost] Strategy for timing constraints.
|
32
|
+
# @parameter parent [Async::Task, nil] Parent task for creating child tasks.
|
33
|
+
def initialize(queue = self.class.default_queue, timing: Timing::None, parent: nil)
|
34
|
+
super(timing: timing, parent: parent)
|
35
|
+
@queue = queue
|
36
|
+
end
|
37
|
+
|
38
|
+
# @attribute [Queue] The queue managing resources.
|
39
|
+
attr_reader :queue
|
40
|
+
|
41
|
+
# Check if a new task can be acquired.
|
42
|
+
# @returns [Boolean] True if resources are available.
|
43
|
+
def limited?
|
44
|
+
@queue.empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
# Expand the queue with additional resources.
|
48
|
+
# @parameter count [Integer] Number of resources to add.
|
49
|
+
# @parameter value [Object] The value to add to the queue.
|
50
|
+
def expand(count, value = true)
|
51
|
+
count.times do
|
52
|
+
@queue.push(value)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Get current limiter statistics.
|
57
|
+
# @returns [Hash] Statistics hash with current state.
|
58
|
+
def statistics
|
59
|
+
@mutex.synchronize do
|
60
|
+
{
|
61
|
+
waiting: @queue.waiting_count,
|
62
|
+
available: @queue.size,
|
63
|
+
timing: @timing.statistics
|
64
|
+
}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
protected
|
69
|
+
|
70
|
+
# Acquire a resource from the queue with optional deadline.
|
71
|
+
def acquire_resource(deadline, reacquire: false, **options)
|
72
|
+
@mutex.unlock
|
73
|
+
return @queue.pop(timeout: deadline&.remaining, **options)
|
74
|
+
ensure
|
75
|
+
@mutex.lock
|
76
|
+
end
|
77
|
+
|
78
|
+
# Release a previously acquired resource back to the queue.
|
79
|
+
def release_resource(value)
|
80
|
+
# Return a default resource to the queue:
|
81
|
+
@queue.push(value)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2025, by Shopify Inc.
|
5
|
+
# Copyright, 2025, by Samuel Williams.
|
6
|
+
|
7
|
+
module Async
|
8
|
+
module Limiter
|
9
|
+
module Timing
|
10
|
+
# Provides burst control strategies for timing limiters.
|
11
|
+
#
|
12
|
+
# Burst strategies are stateless modules that determine whether tasks can execute
|
13
|
+
# immediately or must wait for frame boundaries, controlling task distribution over time.
|
14
|
+
#
|
15
|
+
# ## Strategy Comparison
|
16
|
+
#
|
17
|
+
# Greedy vs Smooth behavior with 5 tasks per 60-second window:
|
18
|
+
#
|
19
|
+
# Greedy Strategy (allows clustering):
|
20
|
+
# |█████ |█████ |█████ |█████ |
|
21
|
+
# 0s 60s 120s 180s
|
22
|
+
# All 5 tasks execute immediately when window opens
|
23
|
+
#
|
24
|
+
# Smooth Strategy (enforces even distribution):
|
25
|
+
# |█ █ █ █ █ |█ █ █ █ █ |█ █ █ █ █ |█ █ █ █ █ |
|
26
|
+
# 0s 60s 120s 180s
|
27
|
+
# Tasks spread evenly: 0s, 12s, 24s, 36s, 48s
|
28
|
+
#
|
29
|
+
module Burst
|
30
|
+
# Allows tasks to cluster within windows for high-throughput scenarios.
|
31
|
+
#
|
32
|
+
# Greedy strategies permit multiple tasks to start immediately as long as
|
33
|
+
# the window limit hasn't been exceeded. This creates "bursts" of activity
|
34
|
+
# at window boundaries, maximizing throughput when resources become available.
|
35
|
+
#
|
36
|
+
# ## Greedy Behavior
|
37
|
+
#
|
38
|
+
# Greedy behavior with 3 tasks per 10-second window:
|
39
|
+
#
|
40
|
+
# Window 1: [Task1, Task2, Task3] at 0s -------- (all immediate)
|
41
|
+
# Window 2: [Task4, Task5, Task6] at 10s ------- (all immediate)
|
42
|
+
#
|
43
|
+
# Perfect for: Batch processing, high-throughput scenarios
|
44
|
+
module Greedy
|
45
|
+
# Check if a task can be acquired in burstable mode.
|
46
|
+
# @parameter window_count [Integer] Number of tasks started in current window.
|
47
|
+
# @parameter limit [Integer] Maximum tasks allowed in the window.
|
48
|
+
# @parameter frame_changed [Boolean] Ignored in burstable mode.
|
49
|
+
# @returns [Boolean] True if under the window limit.
|
50
|
+
def self.can_acquire?(window_count, limit, frame_changed)
|
51
|
+
window_count < limit
|
52
|
+
end
|
53
|
+
|
54
|
+
# Calculate the next time a task can be acquired.
|
55
|
+
# @parameter window_start_time [Numeric] When the current window started.
|
56
|
+
# @parameter window_duration [Numeric] Duration of the window.
|
57
|
+
# @parameter frame_start_time [Numeric] Ignored in burstable mode.
|
58
|
+
# @parameter frame_duration [Numeric] Ignored in burstable mode.
|
59
|
+
# @returns [Numeric] The next window start time.
|
60
|
+
def self.next_acquire_time(window_start_time, window_duration, frame_start_time, frame_duration)
|
61
|
+
window_start_time + window_duration
|
62
|
+
end
|
63
|
+
|
64
|
+
# Check if window constraints are blocking new tasks.
|
65
|
+
# @parameter window_count [Integer] Number of tasks started in current window.
|
66
|
+
# @parameter limit [Integer] Maximum tasks allowed in the window.
|
67
|
+
# @parameter window_changed [Boolean] Whether the window has reset.
|
68
|
+
# @returns [Boolean] True if window is blocking new tasks.
|
69
|
+
def self.window_blocking?(window_count, limit, window_changed)
|
70
|
+
return false if window_changed
|
71
|
+
window_count >= limit
|
72
|
+
end
|
73
|
+
|
74
|
+
# Check if frame constraints are blocking new tasks.
|
75
|
+
# @parameter frame_changed [Boolean] Whether the frame boundary has been crossed.
|
76
|
+
# @returns [Boolean] Always false for burstable mode.
|
77
|
+
def self.frame_blocking?(frame_changed)
|
78
|
+
false # Burstable mode doesn't use frame blocking
|
79
|
+
end
|
80
|
+
|
81
|
+
# Get current burst strategy statistics.
|
82
|
+
# @returns [Hash] Statistics hash with current state.
|
83
|
+
def self.statistics
|
84
|
+
{
|
85
|
+
name: "Greedy",
|
86
|
+
}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Enforces even task distribution to prevent clustering.
|
91
|
+
#
|
92
|
+
# Smooth strategies prevent clustering by requiring tasks to wait for
|
93
|
+
# frame boundaries, ensuring smooth and predictable task execution timing.
|
94
|
+
# This creates consistent load patterns and prevents resource spikes.
|
95
|
+
#
|
96
|
+
# ## Smooth Behavior
|
97
|
+
#
|
98
|
+
# Smooth behavior with 3 tasks per 15-second window:
|
99
|
+
#
|
100
|
+
# Window 1: [Task1] -- [Task2] -- [Task3] ---- (evenly spaced)
|
101
|
+
# 0s 5s 10s 15s
|
102
|
+
# Window 2: [Task4] -- [Task5] -- [Task6] ---- (evenly spaced)
|
103
|
+
# 15s 20s 25s 30s
|
104
|
+
#
|
105
|
+
# Perfect for: API rate limiting, smooth resource usage
|
106
|
+
module Smooth
|
107
|
+
# Check if a task can be acquired in continuous mode.
|
108
|
+
# @parameter window_count [Integer] Ignored in continuous mode.
|
109
|
+
# @parameter limit [Integer] Ignored in continuous mode.
|
110
|
+
# @parameter frame_changed [Boolean] Whether the frame boundary has been crossed.
|
111
|
+
# @returns [Boolean] True only if the frame boundary has been crossed.
|
112
|
+
def self.can_acquire?(window_count, limit, frame_changed)
|
113
|
+
frame_changed
|
114
|
+
end
|
115
|
+
|
116
|
+
# Calculate the next time a task can be acquired.
|
117
|
+
# @parameter window_start_time [Numeric] Ignored in continuous mode.
|
118
|
+
# @parameter window_duration [Numeric] Ignored in continuous mode.
|
119
|
+
# @parameter frame_start_time [Numeric] When the current frame started.
|
120
|
+
# @parameter frame_duration [Numeric] Duration of each frame.
|
121
|
+
# @returns [Numeric] The next frame start time.
|
122
|
+
def self.next_acquire_time(window_start_time, window_duration, frame_start_time, frame_duration)
|
123
|
+
frame_start_time + frame_duration
|
124
|
+
end
|
125
|
+
|
126
|
+
# Check if window constraints are blocking new tasks.
|
127
|
+
# @parameter window_count [Integer] Ignored in continuous mode.
|
128
|
+
# @parameter limit [Integer] Ignored in continuous mode.
|
129
|
+
# @parameter window_changed [Boolean] Ignored in continuous mode.
|
130
|
+
# @returns [Boolean] Always false for continuous mode.
|
131
|
+
def self.window_blocking?(window_count, limit, window_changed)
|
132
|
+
false # Continuous mode doesn't use window blocking
|
133
|
+
end
|
134
|
+
|
135
|
+
# Check if frame constraints are blocking new tasks.
|
136
|
+
# @parameter frame_changed [Boolean] Whether the frame boundary has been crossed.
|
137
|
+
# @returns [Boolean] True if frame hasn't changed (blocking until next frame).
|
138
|
+
def self.frame_blocking?(frame_changed)
|
139
|
+
!frame_changed
|
140
|
+
end
|
141
|
+
|
142
|
+
# Get current burst strategy statistics.
|
143
|
+
# @returns [Hash] Statistics hash with current state.
|
144
|
+
def self.statistics
|
145
|
+
{
|
146
|
+
name: "Smooth",
|
147
|
+
}
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2020, by Bruno Sutic.
|
5
|
+
# Copyright, 2025, by Shopify Inc.
|
6
|
+
# Copyright, 2025, by Samuel Williams.
|
7
|
+
|
8
|
+
require_relative "sliding_window"
|
9
|
+
|
10
|
+
module Async
|
11
|
+
module Limiter
|
12
|
+
module Timing
|
13
|
+
# Fixed window timing strategy with discrete boundaries aligned to clock time.
|
14
|
+
#
|
15
|
+
# Fixed windows reset at specific intervals (e.g., every minute at :00 seconds),
|
16
|
+
# creating predictable timing patterns and allowing bursting at window boundaries.
|
17
|
+
class FixedWindow < SlidingWindow
|
18
|
+
# Calculate the start time of the fixed window containing the given time.
|
19
|
+
# @parameter current_time [Numeric] The current time.
|
20
|
+
# @returns [Numeric] The window start time aligned to window boundaries.
|
21
|
+
def window_start_time(current_time)
|
22
|
+
(current_time / @duration).to_i * @duration
|
23
|
+
end
|
24
|
+
|
25
|
+
# Get current timing strategy statistics.
|
26
|
+
# @returns [Hash] Statistics hash with current state.
|
27
|
+
def statistics
|
28
|
+
current_time = Time.now
|
29
|
+
|
30
|
+
{
|
31
|
+
name: "FixedWindow",
|
32
|
+
window_duration: @duration,
|
33
|
+
window_limit: @limit,
|
34
|
+
current_window_count: @window_count,
|
35
|
+
window_utilization_percentage: (@window_count.to_f / @limit) * 100,
|
36
|
+
burst: @burst.statistics,
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|