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