async-limiter 2.1.0 → 2.3.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: d9b6e7e0d162f4068ced173efa8f365f53e809bf36b80fb41c21988865ea558b
4
- data.tar.gz: d6ed3ed9482da942bf2fdec9403d2c842174eaa090cf0ea61036cf839c731d9c
3
+ metadata.gz: 20ea7fcd7cb0c04777113d99878872f59f2275d7df572555eb23e5176173103a
4
+ data.tar.gz: 65bd9ebbc338f9cb786adb711f01085b8e3fdade9ed5d8a5614a3710023a5863
5
5
  SHA512:
6
- metadata.gz: 496cd5748700aa4c7eb897fc2c47720ea10c7650ab2407e0d15394ce56d44c1d7bd82a31f4138b9437d1e0e474449defe2a5f190d2140f41bc44afafa68bfe8b
7
- data.tar.gz: 8733a7138915188e423fee9f9b4213cc087d93b86f69a43e3ac7954272e94eb15ef99e2ea100ccfd621aebc6d4f292267c298d609f559448f92e45c7c8ffb0dd
6
+ metadata.gz: dbd5950c897046843d3cd9da278843aab29a46d5a4e6f2915dc971596353e97edeec4da100553f243eb31661243a51a05fbc3232758521fdf8840ddcddd7fd58
7
+ data.tar.gz: 4e1a47617c6239470ba15351d22f310d082062358159e7c9595f42a160ea87a694f9cd52a25daaaa19a6d42983c0869dd5580e31f97a7dcd3603de4e056d84cf
checksums.yaml.gz.sig CHANGED
Binary file
@@ -6,6 +6,7 @@
6
6
 
7
7
  require "async/task"
8
8
  require "async/deadline"
9
+ require "async/utilization"
9
10
  require "json"
10
11
  require_relative "timing/none"
11
12
  require_relative "timing/sliding_window"
@@ -24,17 +25,25 @@ module Async
24
25
  # Initialize a new generic limiter.
25
26
  # @parameter timing [#acquire, #wait, #maximum_cost] Strategy for timing constraints.
26
27
  # @parameter parent [Async::Task, nil] Parent task for creating child tasks.
27
- def initialize(timing: Timing::None, parent: nil, tags: nil)
28
+ # @parameter utilization [#metric] Registry-like object for utilization metrics.
29
+ def initialize(timing: Timing::None, parent: nil, tags: nil, utilization: Async::Utilization::Registry.new)
28
30
  @timing = timing
29
31
  @parent = parent
30
32
  @tags = tags
33
+ @utilization = utilization
31
34
 
32
35
  @mutex = Mutex.new
33
36
  end
34
37
 
38
+ # @attribute [Timing] Strategy for timing constraints.
39
+ attr :timing
40
+
35
41
  # @attribute [Array(String)] Tags associated with this limiter for identification or categorization.
36
42
  attr :tags
37
43
 
44
+ # @attribute [#metric] Registry-like object for utilization metrics.
45
+ attr :utilization
46
+
38
47
  # @returns [Boolean] Whether this limiter is currently limiting concurrency.
39
48
  def limited?
40
49
  false
@@ -67,7 +76,7 @@ module Async
67
76
  end
68
77
 
69
78
  # Manually acquire a resource with timing and concurrency coordination.
70
- #
79
+ #
71
80
  # This method provides the core acquisition logic with support for:
72
81
  # - Flexible timeout handling (blocking, non-blocking, timed)
73
82
  # - Cost-based consumption for timing strategies
@@ -20,18 +20,22 @@ module Async
20
20
  class Limited < Generic
21
21
  # Initialize a limited concurrency limiter.
22
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.
23
+ # @parameter options [Hash] Options passed to {Generic#initialize}.
25
24
  # @raises [ArgumentError] If limit is not positive.
26
- def initialize(limit = 1, timing: Timing::None, parent: nil)
27
- super(timing: timing, parent: parent)
25
+ def initialize(limit = 1, **options)
26
+ super(**options)
28
27
 
29
28
  @limit = limit
30
29
  @count = 0
31
- @waiting_count = 0
32
- @reacquire_waiting_count = 0
33
30
 
34
31
  @available = ConditionVariable.new
32
+
33
+ @acquired_count_metric = @utilization.metric(:acquired_count)
34
+ @available_count_metric = @utilization.metric(:available_count)
35
+ @waiting_count_metric = @utilization.metric(:waiting_count)
36
+ @reacquire_waiting_count_metric = @utilization.metric(:reacquire_waiting_count)
37
+
38
+ update_utilization_metrics
35
39
  end
36
40
 
37
41
  # @attribute [Integer] The maximum number of concurrent tasks.
@@ -52,12 +56,12 @@ module Async
52
56
 
53
57
  # @returns [Integer] Current count of tasks waiting for capacity.
54
58
  def waiting_count
55
- @mutex.synchronize{@waiting_count}
59
+ @waiting_count_metric.value
56
60
  end
57
61
 
58
62
  # @returns [Integer] Current count of reacquiring tasks waiting for capacity.
59
63
  def reacquire_waiting_count
60
- @mutex.synchronize{@reacquire_waiting_count}
64
+ @reacquire_waiting_count_metric.value
61
65
  end
62
66
 
63
67
  # Check if a new task can be acquired.
@@ -73,6 +77,7 @@ module Async
73
77
  @mutex.synchronize do
74
78
  old_limit = @limit
75
79
  @limit = new_limit
80
+ update_utilization_metrics
76
81
 
77
82
  # Wake up waiting tasks if limit increased:
78
83
  @available.broadcast if new_limit > old_limit
@@ -88,8 +93,8 @@ module Async
88
93
  count: @count,
89
94
  acquired_count: @count,
90
95
  available_count: @limit - @count,
91
- waiting_count: @waiting_count,
92
- reacquire_waiting_count: @reacquire_waiting_count,
96
+ waiting_count: @waiting_count_metric.value,
97
+ reacquire_waiting_count: @reacquire_waiting_count_metric.value,
93
98
  timing: @timing.statistics
94
99
  }
95
100
  end
@@ -103,6 +108,7 @@ module Async
103
108
  return nil if deadline&.expired? && @count >= @limit
104
109
 
105
110
  waiting = false
111
+ acquired = false
106
112
 
107
113
  # Wait for capacity with deadline tracking
108
114
  while @count >= @limit
@@ -110,8 +116,8 @@ module Async
110
116
  return nil if remaining && remaining <= 0
111
117
 
112
118
  unless waiting
113
- @waiting_count += 1
114
- @reacquire_waiting_count += 1 if reacquire
119
+ @waiting_count_metric.increment
120
+ @reacquire_waiting_count_metric.increment if reacquire
115
121
  waiting = true
116
122
  end
117
123
 
@@ -121,22 +127,33 @@ module Async
121
127
  end
122
128
 
123
129
  @count += 1
130
+ acquired = true
124
131
 
125
132
  return true
126
133
  ensure
127
134
  if waiting
128
- @waiting_count -= 1
129
- @reacquire_waiting_count -= 1 if reacquire
135
+ @waiting_count_metric.decrement
136
+ @reacquire_waiting_count_metric.decrement if reacquire
130
137
  end
138
+
139
+ update_utilization_metrics if acquired
131
140
  end
132
141
 
133
142
  # Release resource.
134
143
  def release_resource(resource)
135
144
  @mutex.synchronize do
136
145
  @count -= 1
146
+ update_utilization_metrics
137
147
  @available.signal
138
148
  end
139
149
  end
150
+
151
+ private
152
+
153
+ def update_utilization_metrics
154
+ @acquired_count_metric.set(@count)
155
+ @available_count_metric.set(@limit - @count)
156
+ end
140
157
  end
141
158
  end
142
159
  end
@@ -28,13 +28,17 @@ module Async
28
28
 
29
29
  # Initialize a queue-based limiter.
30
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)
31
+ # @parameter options [Hash] Options passed to {Generic#initialize}.
32
+ def initialize(queue = self.class.default_queue, **options)
33
+ super(**options)
35
34
  @queue = queue
36
- @acquired_count = 0
37
- @reacquire_waiting_count = 0
35
+
36
+ @acquired_count_metric = @utilization.metric(:acquired_count)
37
+ @available_count_metric = @utilization.metric(:available_count)
38
+ @waiting_count_metric = @utilization.metric(:waiting_count)
39
+ @reacquire_waiting_count_metric = @utilization.metric(:reacquire_waiting_count)
40
+
41
+ update_utilization_metrics
38
42
  end
39
43
 
40
44
  # @attribute [Queue] The queue managing resources.
@@ -42,7 +46,7 @@ module Async
42
46
 
43
47
  # @returns [Integer] Current count of acquired resources.
44
48
  def acquired_count
45
- @mutex.synchronize{@acquired_count}
49
+ @acquired_count_metric.value
46
50
  end
47
51
 
48
52
  # @returns [Integer] Current count of available resources.
@@ -57,7 +61,7 @@ module Async
57
61
 
58
62
  # @returns [Integer] Current count of reacquiring tasks waiting for resources.
59
63
  def reacquire_waiting_count
60
- @mutex.synchronize{@reacquire_waiting_count}
64
+ @reacquire_waiting_count_metric.value
61
65
  end
62
66
 
63
67
  # Check if a new task can be acquired.
@@ -73,6 +77,8 @@ module Async
73
77
  count.times do
74
78
  @queue.push(value)
75
79
  end
80
+
81
+ update_utilization_metrics
76
82
  end
77
83
 
78
84
  # Get current limiter statistics.
@@ -82,10 +88,10 @@ module Async
82
88
  {
83
89
  waiting: @queue.waiting_count,
84
90
  available: @queue.size,
85
- acquired_count: @acquired_count,
91
+ acquired_count: @acquired_count_metric.value,
86
92
  available_count: @queue.size,
87
93
  waiting_count: @queue.waiting_count,
88
- reacquire_waiting_count: @reacquire_waiting_count,
94
+ reacquire_waiting_count: @reacquire_waiting_count_metric.value,
89
95
  timing: @timing.statistics
90
96
  }
91
97
  end
@@ -95,25 +101,36 @@ module Async
95
101
 
96
102
  # Acquire a resource from the queue with optional deadline.
97
103
  def acquire_resource(deadline, reacquire: false, **options)
98
- @reacquire_waiting_count += 1 if reacquire
104
+ @reacquire_waiting_count_metric.increment if reacquire
105
+ update_utilization_metrics if reacquire
99
106
 
100
107
  @mutex.unlock
101
108
  resource = @queue.pop(timeout: deadline&.remaining, **options)
102
109
  return resource
103
110
  ensure
104
111
  @mutex.lock
105
- @reacquire_waiting_count -= 1 if reacquire
106
- @acquired_count += 1 if resource
112
+ @reacquire_waiting_count_metric.decrement if reacquire
113
+ @acquired_count_metric.increment if resource
114
+ update_utilization_metrics if reacquire || resource
107
115
  end
108
116
 
109
117
  # Release a previously acquired resource back to the queue.
110
118
  def release_resource(value)
111
119
  @mutex.synchronize do
112
- @acquired_count -= 1 if @acquired_count > 0
120
+ @acquired_count_metric.decrement if @acquired_count_metric.value > 0
121
+ update_utilization_metrics
113
122
  end
114
123
 
115
124
  # Return a default resource to the queue:
116
125
  @queue.push(value)
126
+ update_utilization_metrics
127
+ end
128
+
129
+ private
130
+
131
+ def update_utilization_metrics
132
+ @available_count_metric.set(@queue.size)
133
+ @waiting_count_metric.set(@queue.waiting_count)
117
134
  end
118
135
  end
119
136
  end
@@ -14,7 +14,8 @@ module Async
14
14
  # re-acquisition with modified parameters while maintaining the original context.
15
15
  #
16
16
  # The token automatically tracks release state using the resource itself as the
17
- # state indicator (nil = released, non-nil = acquired).
17
+ # state indicator (nil = released, non-nil = acquired). A closed token also
18
+ # clears its limiter reference, which prevents future re-acquisition.
18
19
  class Token
19
20
  # Acquire a token from a limiter.
20
21
  #
@@ -61,6 +62,12 @@ module Async
61
62
  end
62
63
  end
63
64
 
65
+ # Close the token and prevent future re-acquisition.
66
+ def close
67
+ self.release
68
+ @limiter = nil
69
+ end
70
+
64
71
  # Re-acquire the resource with modified options.
65
72
  #
66
73
  # This allows changing acquisition parameters (timeout, cost, priority, etc.)
@@ -74,6 +81,7 @@ module Async
74
81
  # @asynchronous
75
82
  def acquire(**options, &block)
76
83
  raise "Token already acquired!" if @resource
84
+ return nil unless @limiter
77
85
 
78
86
  @resource = @limiter.acquire(reacquire: true, **options)
79
87
 
@@ -97,6 +105,12 @@ module Async
97
105
  def released?
98
106
  !@resource
99
107
  end
108
+
109
+ # Check if the token has been closed.
110
+ # @returns [Boolean] True if the token has been closed.
111
+ def closed?
112
+ !@limiter
113
+ end
100
114
  end
101
115
  end
102
116
  end
@@ -7,6 +7,6 @@
7
7
 
8
8
  module Async
9
9
  module Limiter
10
- VERSION = "2.1.0"
10
+ VERSION = "2.3.0"
11
11
  end
12
12
  end
data/readme.md CHANGED
@@ -24,6 +24,15 @@ Please see the [project documentation](https://socketry.github.io/async-limiter/
24
24
 
25
25
  Please see the [project releases](https://socketry.github.io/async-limiter/releases/index) for all releases.
26
26
 
27
+ ### v2.3.0
28
+
29
+ - Add `Async::Limiter::Token#close` to release and permanently invalidate a token.
30
+ - Expose `Async::Limiter::Generic#timing`.
31
+
32
+ ### v2.2.0
33
+
34
+ - Add `async-utilization` metrics for limiter telemetry counters.
35
+
27
36
  ### v2.1.0
28
37
 
29
38
  - 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.
data/releases.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Releases
2
2
 
3
+ ## v2.3.0
4
+
5
+ - Add `Async::Limiter::Token#close` to release and permanently invalidate a token.
6
+ - Expose `Async::Limiter::Generic#timing`.
7
+
8
+ ## v2.2.0
9
+
10
+ - Add `async-utilization` metrics for limiter telemetry counters.
11
+
3
12
  ## v2.1.0
4
13
 
5
14
  - 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.
data.tar.gz.sig CHANGED
@@ -1,5 +1 @@
1
- d���U��=���>jZ"8UM1j_#��� �s2)����l��ٱ1�4�$ob@�/��6cf;"�Q�}fDhvת�^gn[\W�_m�к̔EM�޾��i�
2
- ���A�V3�\�|�v����w]1�u�'���U��;x�6�b���
3
- ��St�B74,�>�����NaZC�pmC���ȴHN�/cG"�'��e�*
4
- t��m�L2�� ��#��/q��!@E��EYg]
5
- ;L`�E�)�J��{�5����tD�J��ҍ�q��m�o���=:&#ݗ��M��x��>{�d���˅e�uh�. ��~(�d�1-R�hM:;�{- e�ȭ�c�QX��q���G�
1
+ _��rG�� 1K���S~��nw7`I)Q����6Z
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-limiter
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bruno Sutic
@@ -56,6 +56,20 @@ dependencies:
56
56
  - - ">="
57
57
  - !ruby/object:Gem::Version
58
58
  version: '2.31'
59
+ - !ruby/object:Gem::Dependency
60
+ name: async-utilization
61
+ requirement: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - "~>"
64
+ - !ruby/object:Gem::Version
65
+ version: '0.4'
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - "~>"
71
+ - !ruby/object:Gem::Version
72
+ version: '0.4'
59
73
  executables: []
60
74
  extensions: []
61
75
  extra_rdoc_files: []
@@ -86,7 +100,7 @@ files:
86
100
  - license.md
87
101
  - readme.md
88
102
  - releases.md
89
- homepage: https://github.com/bruno-/async-limiter
103
+ homepage: https://github.com/socketry/async-limiter
90
104
  licenses:
91
105
  - MIT
92
106
  metadata:
@@ -106,7 +120,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
106
120
  - !ruby/object:Gem::Version
107
121
  version: '0'
108
122
  requirements: []
109
- rubygems_version: 4.0.6
123
+ rubygems_version: 4.0.3
110
124
  specification_version: 4
111
125
  summary: Execution rate limiting for Async
112
126
  test_files: []
metadata.gz.sig CHANGED
Binary file