async-service-supervisor 0.20.0 → 0.21.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: fb8f5a6980d5ea448b4e4ec8826590167195c4fc31c3d40d2ba3e79e118bc270
4
- data.tar.gz: 47bf75e14cfb55cba7007b50cea7029151bd6c265ebf8d0547340b4e53b58a14
3
+ metadata.gz: 0bad9d9135d925d16cb121c2f935fa99006724d3eea22f8676fbb4c252ff3289
4
+ data.tar.gz: ef02370d8a829606dc0fc1f38f5e56aa152b79d777dd1968542bad4496e6f460
5
5
  SHA512:
6
- metadata.gz: 02b8bb2dada453bc5b96f48ae2608cd655c5eb0cfe22568c9aacd68efe798fcc25a3c652d7cfe94cdcf144676c7aa5d4a12b92f3f91ba6d0d419a2154c296991
7
- data.tar.gz: 547604201a8a872e1498773e3ef6e472ed5a4970b5781c9fd6638802095e732d23bf5049332a8bb9d1cfdab0699421d50a52abff2f3ecc701a1d2366c5acadb8
6
+ metadata.gz: 8278cc7549c4de12409bbe06bc4f99c9e14af56feb6c87fc5202d926c27b7df214a8426514da3cab1a4f048dabd184fb0673e484a2266701c9385cb602780cfb
7
+ data.tar.gz: fdefd78ec63d3afa48fd19204c1ea60fb6526389db774d0b93085a10076010345962c17b1d74e1c2097733ba10ddf5ad5131dee9e7cb6f065827f236392bf6ec
checksums.yaml.gz.sig CHANGED
Binary file
data/context/index.yaml CHANGED
@@ -3,6 +3,8 @@
3
3
  ---
4
4
  description: A supervisor for managing multiple container processes.
5
5
  metadata:
6
+ bug_tracker_uri: https://github.com/socketry/async-service-supervisor/issues
7
+ changelog_uri: https://github.com/socketry/async-service-supervisor/blob/main/releases.md
6
8
  documentation_uri: https://socketry.github.io/async-service-supervisor/
7
9
  source_code_uri: https://github.com/socketry/async-service-supervisor.git
8
10
  files:
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2026, by Samuel Williams.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
5
 
6
6
  require "async/loop"
7
7
 
@@ -3,8 +3,6 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2026, by Samuel Williams.
5
5
 
6
- require "set"
7
-
8
6
  require_relative "monitor"
9
7
  require "async/utilization"
10
8
 
@@ -16,178 +14,9 @@ module Async
16
14
  # Uses shared memory to efficiently collect utilization metrics from workers
17
15
  # and aggregates them by service name for monitoring and reporting.
18
16
  class UtilizationMonitor < Monitor
19
- # Allocates and manages shared memory segments for worker utilization data.
20
- #
21
- # Manages a shared memory file that workers can write utilization metrics to.
22
- # Allocates segments to workers and maintains a free list for reuse.
23
- # Each process (supervisor and workers) maps the shared memory file independently.
24
- class SegmentAllocator
25
- # Initialize a new shared memory manager.
26
- #
27
- # Creates and maps the shared memory file. Workers will map the same file
28
- # independently using the provided path.
29
- #
30
- # @parameter path [String] Path to the shared memory file.
31
- # @parameter size [Integer] Total size of the shared memory buffer.
32
- # @parameter segment_size [Integer] Size of each allocation segment (default: 512 bytes).
33
- # @parameter growth_factor [Integer, Float] Factor to grow by when resizing (default: 2, doubles the size).
34
- # Can be less than 2 or a floating point value; the result will be page-aligned to an integer.
35
- def initialize(path, size: IO::Buffer::PAGE_SIZE * 8, segment_size: 512, growth_factor: 2)
36
- @path = path
37
- @size = size
38
- @segment_size = segment_size
39
- @growth_factor = growth_factor
40
-
41
- File.unlink(path) rescue nil
42
- @file = File.open(path, "w+b")
43
- @file.truncate(size)
44
- # Supervisor maps the file for reading worker data
45
- @buffer = IO::Buffer.map(@file, size)
46
-
47
- # Track allocated segments: worker_id => {offset: Integer, schema: Array}
48
- @allocations = {}
49
-
50
- # Free list of segment offsets
51
- @free_list = []
52
-
53
- # Initialize free list with all segments
54
- (0...(@size / @segment_size)).each do |segment_index|
55
- @free_list << (segment_index * @segment_size)
56
- end
57
- end
58
-
59
- # Allocate a segment for a worker.
60
- #
61
- # Automatically resizes the shared memory file if no segments are available.
62
- #
63
- # @parameter worker_id [Integer] The ID of the worker.
64
- # @parameter schema [Array] Array of [key, type, offset] tuples describing the data layout.
65
- # @returns [Integer] The offset into the shared memory buffer, or nil if allocation fails.
66
- def allocate(worker_id, schema)
67
- # Try to resize if we're out of segments
68
- if @free_list.empty?
69
- unless resize(@size * @growth_factor)
70
- return nil
71
- end
72
- end
73
-
74
- offset = @free_list.shift
75
- @allocations[worker_id] = {offset: offset, schema: schema}
76
-
77
- return offset
78
- end
79
-
80
- # Free a segment allocated to a worker.
81
- #
82
- # @parameter worker_id [Integer] The ID of the worker.
83
- def free(worker_id)
84
- if allocation = @allocations.delete(worker_id)
85
- @free_list << allocation[:offset]
86
- end
87
- end
88
-
89
- # Get the allocation information for a worker.
90
- #
91
- # @parameter worker_id [Integer] The ID of the worker.
92
- # @returns [Hash] Allocation info with :offset and :schema, or nil if not allocated.
93
- def allocation(worker_id)
94
- @allocations[worker_id]
95
- end
96
-
97
- # Get the current size of the shared memory file.
98
- #
99
- # @returns [Integer] The current size of the shared memory file.
100
- def size
101
- @size
102
- end
103
-
104
- # Update the schema for an existing allocation.
105
- #
106
- # @parameter worker_id [Integer] The ID of the worker.
107
- # @parameter schema [Array] Array of [key, type, offset] tuples describing the data layout.
108
- def update_schema(worker_id, schema)
109
- if allocation = @allocations[worker_id]
110
- allocation[:schema] = schema
111
- end
112
- end
113
-
114
- # Read utilization data from a worker's allocated segment.
115
- #
116
- # @parameter worker_id [Integer] The ID of the worker.
117
- # @returns [Hash] Hash mapping keys to their values, or nil if not allocated.
118
- def read(worker_id)
119
- allocation = @allocations[worker_id]
120
- return nil unless allocation
121
-
122
- offset = allocation[:offset]
123
- schema = allocation[:schema]
124
-
125
- result = {}
126
- schema.each do |key, type, field_offset|
127
- absolute_offset = offset + field_offset
128
-
129
- # Use IO::Buffer type symbols directly (i32, u32, i64, u64, f32, f64)
130
- # IO::Buffer accepts both lowercase and uppercase versions
131
- begin
132
- result[key] = @buffer.get_value(type, absolute_offset)
133
- rescue => error
134
- Console.warn(self, "Failed to read value", type: type, key: key, offset: absolute_offset, exception: error)
135
- end
136
- end
137
-
138
- return result
139
- end
140
-
141
- # Resize the shared memory file.
142
- #
143
- # Extends the file to the new size, remaps the buffer, and adds new segments
144
- # to the free list. The new size must be larger than the current size and should
145
- # be page-aligned for optimal performance.
146
- #
147
- # @parameter new_size [Integer] The new size for the shared memory file.
148
- # @returns [Boolean] True if resize was successful, false otherwise.
149
- def resize(new_size)
150
- old_size = @size
151
- return false if new_size <= old_size
152
-
153
- # Ensure new size is page-aligned (rounds up to nearest page boundary)
154
- page_size = IO::Buffer::PAGE_SIZE
155
- new_size = (((new_size + page_size - 1) / page_size) * page_size).to_i
156
-
157
- begin
158
- # Extend the file:
159
- @file.truncate(new_size)
160
-
161
- # Remap the buffer to the new size:
162
- @buffer&.free
163
- @buffer = IO::Buffer.map(@file, new_size)
164
-
165
- # Calculate new segments to add to free list:
166
- old_segment_count = old_size / @segment_size
167
- new_segment_count = new_size / @segment_size
168
-
169
- # Add new segments to free list:
170
- (old_segment_count...new_segment_count).each do |segment_index|
171
- @free_list << (segment_index * @segment_size)
172
- end
173
-
174
- @size = new_size
175
-
176
- Console.info(self, "Resized shared memory", old_size: old_size, new_size: new_size, segments_added: new_segment_count - old_segment_count)
177
-
178
- return true
179
- rescue => error
180
- Console.error(self, "Failed to resize shared memory", old_size: old_size, new_size: new_size, exception: error)
181
- return false
182
- end
183
- end
184
-
185
- # Close the shared memory file.
186
- def close
187
- @file&.close
188
- @buffer = nil
189
- end
190
- end
17
+ # @deprecated Use {Async::Utilization::SegmentStore} instead.
18
+ SegmentAllocator = Async::Utilization::SegmentStore
19
+
191
20
  # Initialize a new utilization monitor.
192
21
  #
193
22
  # @parameter path [String] Path to the shared memory file.
@@ -199,7 +28,7 @@ module Async
199
28
  @path = path
200
29
  @segment_size = segment_size
201
30
 
202
- @allocator = SegmentAllocator.new(path, size: size, segment_size: segment_size)
31
+ @store = Async::Utilization::SegmentStore.open(path, size: size, segment_size: segment_size, replace: true)
203
32
 
204
33
  # Track workers: worker_id => supervisor_controller
205
34
  @workers = {}
@@ -207,6 +36,9 @@ module Async
207
36
  @guard = Mutex.new
208
37
  end
209
38
 
39
+ # @attribute [Async::Utilization::SegmentStore] The shared utilization segment store.
40
+ attr :store
41
+
210
42
  # Register a worker with the utilization monitor.
211
43
  #
212
44
  # Allocates a segment of shared memory and instructs the worker
@@ -220,7 +52,7 @@ module Async
220
52
  return unless worker_id
221
53
 
222
54
  # Allocate a segment first (we'll get schema from worker)
223
- offset = @allocator.allocate(worker_id, [])
55
+ offset = @store.allocate(worker_id, [])
224
56
 
225
57
  unless offset
226
58
  Console.warn(self, "Failed to allocate utilization segment", worker_id: worker_id)
@@ -238,19 +70,19 @@ module Async
238
70
 
239
71
  # Update the allocation with the actual schema
240
72
  if schema && !schema.empty?
241
- @allocator.update_schema(worker_id, schema)
73
+ @store.update_schema(worker_id, schema)
242
74
  @workers[worker_id] = supervisor_controller
243
75
 
244
76
  Console.info(self, "Registered worker utilization", worker_id: worker_id, offset: offset, schema: schema)
245
77
  else
246
78
  # Worker didn't provide schema, free the allocation
247
- @allocator.free(worker_id)
79
+ @store.free(worker_id)
248
80
  Console.info(self, "Worker did not provide utilization schema", worker_id: worker_id)
249
81
  end
250
82
  end
251
83
  rescue => error
252
84
  Console.error(self, "Error setting up worker utilization", worker_id: worker_id, exception: error)
253
- @allocator.free(worker_id)
85
+ @store.free(worker_id)
254
86
  end
255
87
  end
256
88
  end
@@ -266,7 +98,7 @@ module Async
266
98
  return unless worker_id
267
99
 
268
100
  @workers.delete(worker_id)
269
- @allocator.free(worker_id)
101
+ @store.free(worker_id)
270
102
 
271
103
  Console.debug(self, "Freed utilization segment", worker_id: worker_id)
272
104
  end
@@ -316,7 +148,7 @@ module Async
316
148
  def sample_by_worker
317
149
  @guard.synchronize do
318
150
  @workers.each_with_object({}) do |(worker_id, supervisor_controller), workers|
319
- if utilization = @allocator.read(worker_id)
151
+ if utilization = @store.read(worker_id)
320
152
  workers[worker_id] = {
321
153
  state: supervisor_controller.state.dup.freeze,
322
154
  utilization: utilization.freeze,
@@ -9,7 +9,7 @@ module Async
9
9
  module Service
10
10
  # @namespace
11
11
  module Supervisor
12
- VERSION = "0.20.0"
12
+ VERSION = "0.21.0"
13
13
  end
14
14
  end
15
15
  end
@@ -11,11 +11,11 @@ require_relative "../../../../../async/service/supervisor/utilization_monitor"
11
11
  class Async::Service::Supervisor::UtilizationMonitor
12
12
  Metrics::Provider(self) do
13
13
  UTILIZATION = Metrics.metric("async.utilization", :gauge, description: "Active requests per worker.")
14
- UTILIZATION_CONNECTIONS_ACTIVE = Metrics.metric("async.utilization.connections.active", :gauge, description: "The number of active connections.")
15
- UTILIZATION_CONNECTIONS_TOTAL = Metrics.metric("async.utilization.connections.total", :gauge, description: "The total number of connections.")
16
- UTILIZATION_REQUESTS_ACTIVE = Metrics.metric("async.utilization.requests.active", :gauge, description: "The number of active requests.")
17
- UTILIZATION_REQUESTS_TOTAL = Metrics.metric("async.utilization.requests.total", :gauge, description: "The total number of requests.")
18
- UTILIZATION_WORKERS = Metrics.metric("async.utilization.workers", :gauge, description: "The number of workers contributing utilization metrics.")
14
+ UTILIZATION_CONNECTIONS_ACTIVE = Metrics.metric("async.utilization.connections_active", :gauge, description: "The number of active connections.")
15
+ UTILIZATION_CONNECTIONS_TOTAL = Metrics.metric("async.utilization.connections_total", :gauge, description: "The total number of connections.")
16
+ UTILIZATION_REQUESTS_ACTIVE = Metrics.metric("async.utilization.requests_active", :gauge, description: "The number of active requests.")
17
+ UTILIZATION_REQUESTS_TOTAL = Metrics.metric("async.utilization.requests_total", :gauge, description: "The total number of requests.")
18
+ UTILIZATION_WORKER_COUNT = Metrics.metric("async.utilization.worker_count", :gauge, description: "The number of workers contributing utilization metrics.")
19
19
 
20
20
  def emit(metrics)
21
21
  metrics.each do |service_name, fields|
@@ -38,7 +38,7 @@ class Async::Service::Supervisor::UtilizationMonitor
38
38
  end
39
39
 
40
40
  if worker_count = fields[:worker_count]
41
- UTILIZATION_WORKERS.emit(worker_count, tags: tags)
41
+ UTILIZATION_WORKER_COUNT.emit(worker_count, tags: tags)
42
42
 
43
43
  if worker_count > 0 and requests_active = fields[:requests_active]
44
44
  UTILIZATION.emit(requests_active.to_f / worker_count, tags: tags)
data/readme.md CHANGED
@@ -30,6 +30,10 @@ Please see the [project documentation](https://socketry.github.io/async-service-
30
30
 
31
31
  Please see the [project releases](https://socketry.github.io/async-service-supervisor/releases/index) for all releases.
32
32
 
33
+ ### v0.21.0
34
+
35
+ - Use `Async::Utilization::SegmentStore` to manage utilization shared memory.
36
+
33
37
  ### v0.20.0
34
38
 
35
39
  - Add per-worker snapshots to `Async::Service::Supervisor::UtilizationMonitor`.
@@ -66,21 +70,31 @@ Please see the [project releases](https://socketry.github.io/async-service-super
66
70
 
67
71
  - Introduce `UtilizationMonitor`, that uses shared memory to track worker utilization metrics, like total and active requests, connections, etc.
68
72
 
69
- ### v0.11.0
70
-
71
- - Add `state` attribute to `SupervisorController` to store per-worker metadata (e.g., service name).
72
- - Add `state` parameter to `Worker#initialize` to allow workers to provide state during registration.
73
- - State is now accessible via `supervisor_controller.state` instead of `connection.state` (as it was in `Async::Container::Supervisor`).
74
-
75
73
  ## Contributing
76
74
 
77
75
  We welcome contributions to this project.
78
76
 
79
- 1. Fork it.
77
+ 1. Fork the repository.
80
78
  2. Create your feature branch (`git checkout -b my-new-feature`).
81
- 3. Commit your changes (`git commit -am 'Add some feature'`).
79
+ 3. Commit your changes (`git commit -am 'Add some feature.'`).
82
80
  4. Push to the branch (`git push origin my-new-feature`).
83
- 5. Create new Pull Request.
81
+ 5. Create a new pull request.
82
+
83
+ ### Running Tests
84
+
85
+ To run the test suite:
86
+
87
+ ``` bash
88
+ $ bundle exec sus
89
+ ```
90
+
91
+ ### Making Releases
92
+
93
+ To make a new release:
94
+
95
+ ``` bash
96
+ $ bundle exec bake gem:release:patch # or minor or major
97
+ ```
84
98
 
85
99
  ### Developer Certificate of Origin
86
100
 
data/releases.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Releases
2
2
 
3
+ ## v0.21.0
4
+
5
+ - Use `Async::Utilization::SegmentStore` to manage utilization shared memory.
6
+
3
7
  ## v0.20.0
4
8
 
5
9
  - Add per-worker snapshots to `Async::Service::Supervisor::UtilizationMonitor`.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-service-supervisor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.0
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -86,14 +86,14 @@ dependencies:
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '0.3'
89
+ version: '0.5'
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '0.3'
96
+ version: '0.5'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: io-endpoint
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -190,6 +190,8 @@ homepage: https://github.com/socketry/async-service-supervisor
190
190
  licenses:
191
191
  - MIT
192
192
  metadata:
193
+ bug_tracker_uri: https://github.com/socketry/async-service-supervisor/issues
194
+ changelog_uri: https://github.com/socketry/async-service-supervisor/blob/main/releases.md
193
195
  documentation_uri: https://socketry.github.io/async-service-supervisor/
194
196
  source_code_uri: https://github.com/socketry/async-service-supervisor.git
195
197
  rdoc_options: []
metadata.gz.sig CHANGED
Binary file