async-limiter 2.0.0 → 2.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 324fe64138f3bd6854bc8d5349b8404a01d6bd11ee64f5b0ad204d4154efa15b
4
- data.tar.gz: 4cc2963430befada104733f236d4e59b900750c1e3eeeb73fc18f2dece26c5b2
3
+ metadata.gz: d9b6e7e0d162f4068ced173efa8f365f53e809bf36b80fb41c21988865ea558b
4
+ data.tar.gz: d6ed3ed9482da942bf2fdec9403d2c842174eaa090cf0ea61036cf839c731d9c
5
5
  SHA512:
6
- metadata.gz: 815670fb97ad80d90e9a5a6ec9034f51242fe793784220af2b3ce769b3afd49389a03a0d53ac042aa98638affef8636607976f6908f834e667ab31ac96fbb628
7
- data.tar.gz: 5f8df98d26156a041023d05e9c758a17eeefca437cd9ffa34c9a102df820a3e45380dbda8e09b23dd14e189014a49554745349ff13de70647d59a7a5abe33d4d
6
+ metadata.gz: 496cd5748700aa4c7eb897fc2c47720ea10c7650ab2407e0d15394ce56d44c1d7bd82a31f4138b9437d1e0e474449defe2a5f190d2140f41bc44afafa68bfe8b
7
+ data.tar.gz: 8733a7138915188e423fee9f9b4213cc087d93b86f69a43e3ac7954272e94eb15ef99e2ea100ccfd621aebc6d4f292267c298d609f559448f92e45c7c8ffb0dd
checksums.yaml.gz.sig CHANGED
Binary file
@@ -35,7 +35,7 @@ require "async/limiter"
35
35
 
36
36
  Async do
37
37
  limiter = Async::Limiter::Generic.new
38
-
38
+
39
39
  # Create async tasks through the limiter:
40
40
  tasks = 5.times.map do |i|
41
41
  limiter.async do |task|
@@ -81,7 +81,7 @@ Async do
81
81
  # Allow unlimited concurrency but rate limit to 10 operations per second:
82
82
  timing = Async::Limiter::Timing::LeakyBucket.new(10.0, 50.0)
83
83
  limiter = Async::Limiter::Generic.new(timing: timing)
84
-
84
+
85
85
  # All tasks start immediately, but timing strategy controls rate:
86
86
  100.times do |i|
87
87
  limiter.async do |task|
@@ -86,7 +86,7 @@ Async do
86
86
 
87
87
  limiter.acquire(timeout: 3)
88
88
  # => nil
89
-
89
+
90
90
  limiter.acquire(timeout: 3) do
91
91
  puts "Acquired."
92
92
  end or puts "Timed out!"
@@ -199,8 +199,8 @@ fair_limiter.acquire(cost: 8.0) do
199
199
  end
200
200
 
201
201
  # These must wait even though they need fewer tokens
202
- fair_limiter.acquire(cost: 0.5) { puts "Quick op 1" } # Blocked
203
- fair_limiter.acquire(cost: 0.5) { puts "Quick op 2" } # Blocked
202
+ fair_limiter.acquire(cost: 0.5){puts "Quick op 1"} # Blocked
203
+ fair_limiter.acquire(cost: 0.5){puts "Quick op 2"} # Blocked
204
204
  ```
205
205
 
206
206
  #### Choosing the Right Strategy
@@ -89,16 +89,16 @@ The limiter prevents convoy effects where quick timeouts aren't blocked by slow
89
89
  limiter = Async::Limiter::Limited.new(1)
90
90
  Async do
91
91
  limiter.acquire # Fill to capacity.
92
-
92
+
93
93
  results = []
94
-
94
+
95
95
  # Start multiple tasks with different timeouts:
96
96
  tasks = [
97
- Async {limiter.acquire(timeout: 1.0); results << "Long timeout."},
98
- Async {limiter.acquire(timeout: 0.1); results << "Short timeout."},
99
- Async {limiter.acquire(timeout: 0); results << "Non-blocking."},
97
+ Async{limiter.acquire(timeout: 1.0); results << "Long timeout."},
98
+ Async{limiter.acquire(timeout: 0.1); results << "Short timeout."},
99
+ Async{limiter.acquire(timeout: 0); results << "Non-blocking."},
100
100
  ]
101
-
101
+
102
102
  # All tasks complete quickly, even with a long timeout task present:
103
103
  tasks.map(&:wait)
104
104
  puts results
@@ -42,7 +42,7 @@ For fine-grained control over resource lifecycle:
42
42
 
43
43
  ```ruby
44
44
  queue = Async::Queue.new
45
- 3.times { |i| queue.push("resource_#{i}") }
45
+ 3.times{|i| queue.push("resource_#{i}")}
46
46
 
47
47
  limiter = Async::Limiter::Queued.new(queue)
48
48
 
@@ -71,7 +71,7 @@ Async do
71
71
  queue = Async::PriorityQueue.new
72
72
  limiter = Async::Limiter::Queued.new(queue)
73
73
  results = []
74
-
74
+
75
75
  # Start tasks with different priorities
76
76
  tasks = [
77
77
  Async do
@@ -100,9 +100,9 @@ Async do
100
100
  2.times do |i|
101
101
  limiter.release("worker_#{i}")
102
102
  end
103
-
103
+
104
104
  tasks.each(&:wait)
105
-
105
+
106
106
  puts results
107
107
  # High priority task gets resource first, then medium, then low.
108
108
  end
@@ -385,7 +385,7 @@ require "async/queue"
385
385
 
386
386
  # Create resource queue
387
387
  queue = Async::Queue.new
388
- 3.times { |i| queue.push("worker_#{i}") }
388
+ 3.times{|i| queue.push("worker_#{i}")}
389
389
 
390
390
  # Add timing constraint
391
391
  timing = Async::Limiter::Timing::FixedWindow.new(2.0,
@@ -437,8 +437,8 @@ class RateLimitedAPIClient
437
437
 
438
438
  def make_request(endpoint, cost: 1.0)
439
439
  @limiter.acquire(cost: cost) do
440
- # Make actual HTTP request:
441
- puts "Making request to #{endpoint} at #{Time.now}"
440
+ # Make actual HTTP request:
441
+ puts "Making request to #{endpoint} at #{Time.now}"
442
442
  simulate_http_request(endpoint)
443
443
  end
444
444
  end
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2025, by Shopify Inc.
4
+ # Copyright, 2025-2026, by Shopify Inc.
5
5
  # Copyright, 2025, by Samuel Williams.
6
6
 
7
7
  require "async/task"
8
8
  require "async/deadline"
9
+ require "json"
9
10
  require_relative "timing/none"
10
11
  require_relative "timing/sliding_window"
11
12
  require_relative "token"
@@ -135,6 +136,18 @@ module Async
135
136
  end
136
137
  end
137
138
 
139
+ # Get a JSON-compatible representation of the limiter statistics.
140
+ # @returns [Hash] Statistics hash with current state.
141
+ def as_json(...)
142
+ statistics
143
+ end
144
+
145
+ # Get a JSON string representation of the limiter statistics.
146
+ # @returns [String] JSON encoded statistics.
147
+ def to_json(...)
148
+ as_json.to_json(...)
149
+ end
150
+
138
151
  protected
139
152
 
140
153
  def acquire_synchronized(timeout, cost, **options)
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2020, by Bruno Sutic.
5
- # Copyright, 2025, by Shopify Inc.
5
+ # Copyright, 2025-2026, by Shopify Inc.
6
6
  # Copyright, 2025, by Samuel Williams.
7
7
 
8
8
  require_relative "generic"
@@ -28,6 +28,8 @@ module Async
28
28
 
29
29
  @limit = limit
30
30
  @count = 0
31
+ @waiting_count = 0
32
+ @reacquire_waiting_count = 0
31
33
 
32
34
  @available = ConditionVariable.new
33
35
  end
@@ -38,6 +40,26 @@ module Async
38
40
  # @attribute [Integer] Current count of active tasks.
39
41
  attr_reader :count
40
42
 
43
+ # @returns [Integer] Current count of active tasks.
44
+ def acquired_count
45
+ @mutex.synchronize{@count}
46
+ end
47
+
48
+ # @returns [Integer] Current count of available capacity.
49
+ def available_count
50
+ @mutex.synchronize{@limit - @count}
51
+ end
52
+
53
+ # @returns [Integer] Current count of tasks waiting for capacity.
54
+ def waiting_count
55
+ @mutex.synchronize{@waiting_count}
56
+ end
57
+
58
+ # @returns [Integer] Current count of reacquiring tasks waiting for capacity.
59
+ def reacquire_waiting_count
60
+ @mutex.synchronize{@reacquire_waiting_count}
61
+ end
62
+
41
63
  # Check if a new task can be acquired.
42
64
  # @returns [Boolean] True if under the limit.
43
65
  def limited?
@@ -64,6 +86,10 @@ module Async
64
86
  {
65
87
  limit: @limit,
66
88
  count: @count,
89
+ acquired_count: @count,
90
+ available_count: @limit - @count,
91
+ waiting_count: @waiting_count,
92
+ reacquire_waiting_count: @reacquire_waiting_count,
67
93
  timing: @timing.statistics
68
94
  }
69
95
  end
@@ -72,15 +98,23 @@ module Async
72
98
  protected
73
99
 
74
100
  # Acquire resource with optional deadline.
75
- def acquire_resource(deadline, **options)
101
+ def acquire_resource(deadline, reacquire: false, **options)
76
102
  # Fast path: immediate return for expired deadlines, but only if at capacity
77
103
  return nil if deadline&.expired? && @count >= @limit
78
104
 
105
+ waiting = false
106
+
79
107
  # Wait for capacity with deadline tracking
80
108
  while @count >= @limit
81
109
  remaining = deadline&.remaining
82
110
  return nil if remaining && remaining <= 0
83
111
 
112
+ unless waiting
113
+ @waiting_count += 1
114
+ @reacquire_waiting_count += 1 if reacquire
115
+ waiting = true
116
+ end
117
+
84
118
  unless @available.wait(@mutex, remaining)
85
119
  return nil # Timeout exceeded
86
120
  end
@@ -89,6 +123,11 @@ module Async
89
123
  @count += 1
90
124
 
91
125
  return true
126
+ ensure
127
+ if waiting
128
+ @waiting_count -= 1
129
+ @reacquire_waiting_count -= 1 if reacquire
130
+ end
92
131
  end
93
132
 
94
133
  # Release resource.
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2025, by Francisco Mejia.
5
- # Copyright, 2025, by Shopify Inc.
5
+ # Copyright, 2025-2026, by Shopify Inc.
6
6
  # Copyright, 2025, by Samuel Williams.
7
7
 
8
8
  require_relative "generic"
@@ -33,11 +33,33 @@ module Async
33
33
  def initialize(queue = self.class.default_queue, timing: Timing::None, parent: nil)
34
34
  super(timing: timing, parent: parent)
35
35
  @queue = queue
36
+ @acquired_count = 0
37
+ @reacquire_waiting_count = 0
36
38
  end
37
39
 
38
40
  # @attribute [Queue] The queue managing resources.
39
41
  attr_reader :queue
40
42
 
43
+ # @returns [Integer] Current count of acquired resources.
44
+ def acquired_count
45
+ @mutex.synchronize{@acquired_count}
46
+ end
47
+
48
+ # @returns [Integer] Current count of available resources.
49
+ def available_count
50
+ @queue.size
51
+ end
52
+
53
+ # @returns [Integer] Current count of tasks waiting for resources.
54
+ def waiting_count
55
+ @queue.waiting_count
56
+ end
57
+
58
+ # @returns [Integer] Current count of reacquiring tasks waiting for resources.
59
+ def reacquire_waiting_count
60
+ @mutex.synchronize{@reacquire_waiting_count}
61
+ end
62
+
41
63
  # Check if a new task can be acquired.
42
64
  # @returns [Boolean] True if resources are available.
43
65
  def limited?
@@ -60,6 +82,10 @@ module Async
60
82
  {
61
83
  waiting: @queue.waiting_count,
62
84
  available: @queue.size,
85
+ acquired_count: @acquired_count,
86
+ available_count: @queue.size,
87
+ waiting_count: @queue.waiting_count,
88
+ reacquire_waiting_count: @reacquire_waiting_count,
63
89
  timing: @timing.statistics
64
90
  }
65
91
  end
@@ -69,14 +95,23 @@ module Async
69
95
 
70
96
  # Acquire a resource from the queue with optional deadline.
71
97
  def acquire_resource(deadline, reacquire: false, **options)
98
+ @reacquire_waiting_count += 1 if reacquire
99
+
72
100
  @mutex.unlock
73
- return @queue.pop(timeout: deadline&.remaining, **options)
101
+ resource = @queue.pop(timeout: deadline&.remaining, **options)
102
+ return resource
74
103
  ensure
75
104
  @mutex.lock
105
+ @reacquire_waiting_count -= 1 if reacquire
106
+ @acquired_count += 1 if resource
76
107
  end
77
108
 
78
109
  # Release a previously acquired resource back to the queue.
79
110
  def release_resource(value)
111
+ @mutex.synchronize do
112
+ @acquired_count -= 1 if @acquired_count > 0
113
+ end
114
+
80
115
  # Return a default resource to the queue:
81
116
  @queue.push(value)
82
117
  end
@@ -4,6 +4,7 @@
4
4
  # Copyright, 2020, by Bruno Sutic.
5
5
  # Copyright, 2025, by Shopify Inc.
6
6
  # Copyright, 2025, by Samuel Williams.
7
+ # Copyright, 2026, by William T. Nelson.
7
8
 
8
9
  require_relative "sliding_window"
9
10
 
@@ -25,8 +26,6 @@ module Async
25
26
  # Get current timing strategy statistics.
26
27
  # @returns [Hash] Statistics hash with current state.
27
28
  def statistics
28
- current_time = Time.now
29
-
30
29
  {
31
30
  name: "FixedWindow",
32
31
  window_duration: @duration,
@@ -7,6 +7,6 @@
7
7
 
8
8
  module Async
9
9
  module Limiter
10
- VERSION = "2.0.0"
10
+ VERSION = "2.1.0"
11
11
  end
12
12
  end
data/license.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  Copyright, 2020-2021, by Bruno Sutic.
4
4
  Copyright, 2025, by Francisco Mejia.
5
- Copyright, 2025, by Shopify Inc.
6
- Copyright, 2025, by Samuel Williams.
7
- Copyright, 2025, by William T. Nelson.
5
+ Copyright, 2025-2026, by Shopify Inc.
6
+ Copyright, 2025-2026, by Samuel Williams.
7
+ Copyright, 2025-2026, by William T. Nelson.
8
8
 
9
9
  Permission is hereby granted, free of charge, to any person obtaining a copy
10
10
  of this software and associated documentation files (the "Software"), to deal
data/readme.md CHANGED
@@ -20,6 +20,31 @@ Please see the [project documentation](https://socketry.github.io/async-limiter/
20
20
 
21
21
  - [Token Usage](https://socketry.github.io/async-limiter/guides/token-usage/index) - 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.
22
22
 
23
+ ## Releases
24
+
25
+ Please see the [project releases](https://socketry.github.io/async-limiter/releases/index) for all releases.
26
+
27
+ ### v2.1.0
28
+
29
+ - Add telemetry counters to `Async::Limiter::Limited` and `Async::Limiter::Queued`: `acquired_count`, `available_count`, `waiting_count`, and `reacquire_waiting_count` for observability into limiter state.
30
+ - Add `as_json` and `to_json` methods to `Async::Limiter::Generic` for JSON serialization of limiter statistics.
31
+ - Fix unused variable warning in `Async::Limiter::Timing::FixedWindow`.
32
+
33
+ ### v2.0.0
34
+
35
+ The 2.0.x release should be considered somewhat unstable.
36
+
37
+ - **Breaking**: Complete API redesign. The v1.x classes (`Async::Limiter::Concurrent`, `Async::Limiter::Unlimited`, etc.) have been replaced with a new inheritance-based architecture.
38
+ - **Breaking**: Removed `blocking?` method due to inherent race conditions. Use `acquire(timeout: 0)` for non-blocking checks.
39
+ - **Breaking**: Timing strategies now use consumption-only model (no explicit `release` methods).
40
+ - **Breaking**: Window classes moved from `limiter/window/` to `limiter/timing/` with renamed classes.
41
+ - [New Architecture (replaces v1.x classes)](https://socketry.github.io/async-limiter/releases/index#new-architecture-\(replaces-v1.x-classes\))
42
+ - [Advanced Timeout Features](https://socketry.github.io/async-limiter/releases/index#advanced-timeout-features)
43
+ - [Cost-Based Acquisition](https://socketry.github.io/async-limiter/releases/index#cost-based-acquisition)
44
+ - [Enhanced Timing Strategies](https://socketry.github.io/async-limiter/releases/index#enhanced-timing-strategies)
45
+ - [Token-Based Resource Management](https://socketry.github.io/async-limiter/releases/index#token-based-resource-management)
46
+ - [Thread Safety and Performance](https://socketry.github.io/async-limiter/releases/index#thread-safety-and-performance)
47
+
23
48
  ## See Also
24
49
 
25
50
  - [falcon](https://github.com/socketry/falcon) - A high-performance web server
@@ -36,6 +61,22 @@ We welcome contributions to this project.
36
61
  4. Push to the branch (`git push origin my-new-feature`).
37
62
  5. Create new Pull Request.
38
63
 
64
+ ### Running Tests
65
+
66
+ To run the test suite:
67
+
68
+ ``` shell
69
+ bundle exec sus
70
+ ```
71
+
72
+ ### Making Releases
73
+
74
+ To make a new release:
75
+
76
+ ``` shell
77
+ bundle exec bake gem:release:patch # or minor or major
78
+ ```
79
+
39
80
  ### Developer Certificate of Origin
40
81
 
41
82
  In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
data/releases.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Releases
2
2
 
3
+ ## v2.1.0
4
+
5
+ - Add telemetry counters to `Async::Limiter::Limited` and `Async::Limiter::Queued`: `acquired_count`, `available_count`, `waiting_count`, and `reacquire_waiting_count` for observability into limiter state.
6
+ - Add `as_json` and `to_json` methods to `Async::Limiter::Generic` for JSON serialization of limiter statistics.
7
+ - Fix unused variable warning in `Async::Limiter::Timing::FixedWindow`.
8
+
3
9
  ## v2.0.0
4
10
 
5
11
  The 2.0.x release should be considered somewhat unstable.
data.tar.gz.sig CHANGED
Binary file
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: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bruno Sutic
8
8
  - Shopify Inc.
9
9
  - Samuel Williams
10
+ - William T. Nelson
10
11
  - Francisco Mejia
11
- - William
12
12
  bindir: bin
13
13
  cert_chain:
14
14
  - |
@@ -99,14 +99,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - ">="
101
101
  - !ruby/object:Gem::Version
102
- version: '3.2'
102
+ version: '3.3'
103
103
  required_rubygems_version: !ruby/object:Gem::Requirement
104
104
  requirements:
105
105
  - - ">="
106
106
  - !ruby/object:Gem::Version
107
107
  version: '0'
108
108
  requirements: []
109
- rubygems_version: 3.6.9
109
+ rubygems_version: 4.0.6
110
110
  specification_version: 4
111
111
  summary: Execution rate limiting for Async
112
112
  test_files: []
metadata.gz.sig CHANGED
Binary file