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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/index.yaml +2 -0
- data/lib/async/service/supervisor/monitor.rb +1 -1
- data/lib/async/service/supervisor/utilization_monitor.rb +13 -181
- data/lib/async/service/supervisor/version.rb +1 -1
- data/lib/metrics/provider/async/service/supervisor/utilization_monitor.rb +6 -6
- data/readme.md +23 -9
- data/releases.md +4 -0
- data.tar.gz.sig +0 -0
- metadata +5 -3
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0bad9d9135d925d16cb121c2f935fa99006724d3eea22f8676fbb4c252ff3289
|
|
4
|
+
data.tar.gz: ef02370d8a829606dc0fc1f38f5e56aa152b79d777dd1968542bad4496e6f460
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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:
|
|
@@ -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
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
@
|
|
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 = @
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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 = @
|
|
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,
|
|
@@ -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.
|
|
15
|
-
UTILIZATION_CONNECTIONS_TOTAL = Metrics.metric("async.utilization.
|
|
16
|
-
UTILIZATION_REQUESTS_ACTIVE = Metrics.metric("async.utilization.
|
|
17
|
-
UTILIZATION_REQUESTS_TOTAL = Metrics.metric("async.utilization.
|
|
18
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
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.
|
|
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.
|
|
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.
|
|
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
|