async-utilization 0.3.2 → 0.5.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/lib/async/utilization/metric.rb +8 -5
- data/lib/async/utilization/namespace.rb +51 -0
- data/lib/async/utilization/registry.rb +15 -50
- data/lib/async/utilization/segment_store.rb +208 -0
- data/lib/async/utilization/version.rb +1 -1
- data/lib/async/utilization.rb +2 -0
- data/readme.md +28 -3
- data/releases.md +9 -0
- data/test/async/utilization/namespace.rb +53 -0
- data/test/async/utilization/registry.rb +24 -15
- data/test/async/utilization/segment_store.rb +175 -0
- data.tar.gz.sig +0 -0
- metadata +8 -2
- 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: 64e873daf011f6226b574b609cecde9b402edb2a2ff5144f68526195575492ea
|
|
4
|
+
data.tar.gz: b5e25e805f9e8b359af97653bc1496706c9ac81d8c8c2ea42c1bbd1680be271b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e486c1307fead9186ae112f2c258ec4e5ed25b7c6cb8d49d56974634ed2e250400ccb8a57caccb736871df8cd592e5be5379bfd9ac738cf9ace363812e651a90
|
|
7
|
+
data.tar.gz: 4c0380449a03ea7b1ba5c42cc0b70e86cbe76fb360e0bdccaf7f3b5477dccd26d0171921e91f2e535433a236c60c8abb8a97f3dcaf1c6e34b17d7e8c87a72a17
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -38,11 +38,14 @@ module Async
|
|
|
38
38
|
# @attribute [Symbol] The field name for this metric.
|
|
39
39
|
attr :name
|
|
40
40
|
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
# Get the current in-memory metric value.
|
|
42
|
+
#
|
|
43
|
+
# @returns [Numeric] The last value written to this metric.
|
|
44
|
+
def value
|
|
45
|
+
@guard.synchronize do
|
|
46
|
+
@value
|
|
47
|
+
end
|
|
48
|
+
end
|
|
46
49
|
|
|
47
50
|
# Set the observer and rebuild cache.
|
|
48
51
|
#
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Async
|
|
7
|
+
module Utilization
|
|
8
|
+
# A registry-like view that prefixes metric names.
|
|
9
|
+
#
|
|
10
|
+
# Namespaces let components use generic metric names while applications decide
|
|
11
|
+
# how those names are composed in the shared registry.
|
|
12
|
+
class Namespace
|
|
13
|
+
# Initialize a new namespace.
|
|
14
|
+
#
|
|
15
|
+
# @parameter registry [Registry] The underlying registry.
|
|
16
|
+
# @parameter name [Symbol] The namespace name.
|
|
17
|
+
def initialize(registry, name)
|
|
18
|
+
@registry = registry
|
|
19
|
+
@name = name.to_sym
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @attribute [Registry] The underlying registry.
|
|
23
|
+
attr :registry
|
|
24
|
+
|
|
25
|
+
# @attribute [Symbol] The namespace name.
|
|
26
|
+
attr :name
|
|
27
|
+
|
|
28
|
+
# Get a metric in this namespace.
|
|
29
|
+
#
|
|
30
|
+
# @parameter name [Symbol] The metric name.
|
|
31
|
+
# @returns [Metric] A metric instance for the namespaced field.
|
|
32
|
+
def metric(name)
|
|
33
|
+
@registry.metric(metric_name(name))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get a nested namespace.
|
|
37
|
+
#
|
|
38
|
+
# @parameter name [Symbol] The nested namespace name.
|
|
39
|
+
# @returns [Namespace] A namespace view with the composed name.
|
|
40
|
+
def namespace(name)
|
|
41
|
+
self.class.new(@registry, metric_name(name))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def metric_name(name)
|
|
47
|
+
:"#{@name}_#{name}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# Copyright, 2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require "console"
|
|
7
|
+
require_relative "namespace"
|
|
7
8
|
|
|
8
9
|
module Async
|
|
9
10
|
module Utilization
|
|
@@ -22,9 +23,11 @@ module Async
|
|
|
22
23
|
# @example Create a registry and emit metrics:
|
|
23
24
|
# registry = Async::Utilization::Registry.new
|
|
24
25
|
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
26
|
+
# total_requests = registry.metric(:total_requests)
|
|
27
|
+
# total_requests.increment
|
|
28
|
+
#
|
|
29
|
+
# active_requests = registry.metric(:active_requests)
|
|
30
|
+
# active_requests.track do
|
|
28
31
|
# # Handle request - auto-decrements when block completes
|
|
29
32
|
# end
|
|
30
33
|
#
|
|
@@ -37,7 +40,6 @@ module Async
|
|
|
37
40
|
# observer = Async::Utilization::Observer.open(schema, "/path/to/shm", 4096, 0)
|
|
38
41
|
# registry.observer = observer
|
|
39
42
|
class Registry
|
|
40
|
-
|
|
41
43
|
# Initialize a new registry.
|
|
42
44
|
def initialize
|
|
43
45
|
@observer = nil
|
|
@@ -49,15 +51,12 @@ module Async
|
|
|
49
51
|
# @attribute [Object | Nil] The registered observer.
|
|
50
52
|
attr :observer
|
|
51
53
|
|
|
52
|
-
# @attribute [Mutex] The mutex for thread safety.
|
|
53
|
-
attr :guard
|
|
54
|
-
|
|
55
54
|
# Get the current values for all metrics.
|
|
56
55
|
#
|
|
57
56
|
# @returns [Hash] Hash mapping field names to their current values.
|
|
58
57
|
def values
|
|
59
58
|
@metrics.transform_values do |metric|
|
|
60
|
-
metric.
|
|
59
|
+
metric.value
|
|
61
60
|
end
|
|
62
61
|
end
|
|
63
62
|
|
|
@@ -79,48 +78,6 @@ module Async
|
|
|
79
78
|
|
|
80
79
|
# Console.info(self, "Observer assigned", observer: observer, metric_count: @metrics.size)
|
|
81
80
|
end
|
|
82
|
-
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Set a field value.
|
|
86
|
-
#
|
|
87
|
-
# Delegates to the metric instance for the given field.
|
|
88
|
-
#
|
|
89
|
-
# @parameter field [Symbol] The field name to set.
|
|
90
|
-
# @parameter value [Numeric] The value to set.
|
|
91
|
-
def set(field, value)
|
|
92
|
-
metric(field).set(value)
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Increment a field value.
|
|
96
|
-
#
|
|
97
|
-
# Delegates to the metric instance for the given field.
|
|
98
|
-
#
|
|
99
|
-
# @parameter field [Symbol] The field name to increment.
|
|
100
|
-
# @returns [Integer] The new value of the field.
|
|
101
|
-
def increment(field)
|
|
102
|
-
metric(field).increment
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# Track an operation: increment before the block, decrement after it completes.
|
|
106
|
-
#
|
|
107
|
-
# Delegates to the metric instance for the given field.
|
|
108
|
-
#
|
|
109
|
-
# @parameter field [Symbol] The field name to track.
|
|
110
|
-
# @yield The operation to track.
|
|
111
|
-
# @returns [Object] The block's return value.
|
|
112
|
-
def track(field, &block)
|
|
113
|
-
metric(field).track(&block)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# Decrement a field value.
|
|
117
|
-
#
|
|
118
|
-
# Delegates to the metric instance for the given field.
|
|
119
|
-
#
|
|
120
|
-
# @parameter field [Symbol] The field name to decrement.
|
|
121
|
-
# @returns [Integer] The new value of the field.
|
|
122
|
-
def decrement(field)
|
|
123
|
-
metric(field).decrement
|
|
124
81
|
end
|
|
125
82
|
|
|
126
83
|
# Get a cached metric reference for a field.
|
|
@@ -137,6 +94,14 @@ module Async
|
|
|
137
94
|
@metrics[field] ||= Metric.for(field, @observer)
|
|
138
95
|
end
|
|
139
96
|
end
|
|
97
|
+
|
|
98
|
+
# Get a namespace view of this registry.
|
|
99
|
+
#
|
|
100
|
+
# @parameter name [Symbol] The namespace name.
|
|
101
|
+
# @returns [Namespace] A registry-like namespace view.
|
|
102
|
+
def namespace(name)
|
|
103
|
+
Namespace.new(self, name)
|
|
104
|
+
end
|
|
140
105
|
end
|
|
141
106
|
end
|
|
142
107
|
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "console"
|
|
7
|
+
|
|
8
|
+
module Async
|
|
9
|
+
module Utilization
|
|
10
|
+
# Represents a shared memory segment store for utilization data.
|
|
11
|
+
#
|
|
12
|
+
# Stores fixed-size segments in a shared memory file, associates each
|
|
13
|
+
# segment with a utilization schema, and reads the resulting values.
|
|
14
|
+
class SegmentStore
|
|
15
|
+
# Open a shared memory segment store.
|
|
16
|
+
#
|
|
17
|
+
# @parameter path [String] The path to the shared memory file.
|
|
18
|
+
# @parameter size [Integer] The initial size of the shared memory file.
|
|
19
|
+
# @parameter segment_size [Integer] The size of each allocation segment.
|
|
20
|
+
# @parameter growth_factor [Integer | Float] The factor used to grow the file when all segments are allocated.
|
|
21
|
+
# @parameter replace [Boolean] Whether to replace an existing file at the given path.
|
|
22
|
+
# @yields {|store| ...} The store, which is closed after the block completes.
|
|
23
|
+
# @parameter store [SegmentStore] The opened store.
|
|
24
|
+
# @returns [SegmentStore | Object] The store, or the value returned by the block.
|
|
25
|
+
# @raises [ArgumentError] If the store configuration is invalid.
|
|
26
|
+
# @raises [Errno::EEXIST] If the path already exists and `replace` is `false`.
|
|
27
|
+
def self.open(path, size: IO::Buffer::PAGE_SIZE * 8, segment_size: 512, growth_factor: 2, replace: false)
|
|
28
|
+
raise ArgumentError, "Size must be a positive integer!" unless size.is_a?(Integer) && size > 0
|
|
29
|
+
raise ArgumentError, "Segment size must be a positive integer!" unless segment_size.is_a?(Integer) && segment_size > 0
|
|
30
|
+
raise ArgumentError, "Segment size must not exceed size!" if segment_size > size
|
|
31
|
+
raise ArgumentError, "Growth factor must be greater than 1!" unless growth_factor.is_a?(Numeric) && growth_factor.real? && growth_factor > 1
|
|
32
|
+
|
|
33
|
+
if replace
|
|
34
|
+
begin
|
|
35
|
+
File.unlink(path)
|
|
36
|
+
rescue Errno::ENOENT
|
|
37
|
+
# The file does not need to be replaced:
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
file = File.open(path, "w+bx")
|
|
42
|
+
buffer = nil
|
|
43
|
+
|
|
44
|
+
begin
|
|
45
|
+
file.truncate(size)
|
|
46
|
+
buffer = IO::Buffer.map(file, size)
|
|
47
|
+
store = new(file, buffer, size: size, segment_size: segment_size, growth_factor: growth_factor)
|
|
48
|
+
rescue
|
|
49
|
+
buffer&.free
|
|
50
|
+
file.close
|
|
51
|
+
raise
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
return store unless block_given?
|
|
55
|
+
|
|
56
|
+
begin
|
|
57
|
+
yield store
|
|
58
|
+
ensure
|
|
59
|
+
store.close
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Initialize the shared memory segment store.
|
|
64
|
+
#
|
|
65
|
+
# @parameter file [File] The open shared memory file.
|
|
66
|
+
# @parameter buffer [IO::Buffer] The mapped shared memory buffer.
|
|
67
|
+
# @parameter size [Integer] The initial size of the shared memory file.
|
|
68
|
+
# @parameter segment_size [Integer] The size of each allocation segment.
|
|
69
|
+
# @parameter growth_factor [Integer | Float] The factor used to grow the file when all segments are allocated.
|
|
70
|
+
def initialize(file, buffer, size:, segment_size:, growth_factor:)
|
|
71
|
+
@file = file
|
|
72
|
+
@buffer = buffer
|
|
73
|
+
@size = size
|
|
74
|
+
@segment_size = segment_size
|
|
75
|
+
@growth_factor = growth_factor
|
|
76
|
+
|
|
77
|
+
@allocations = {}
|
|
78
|
+
@free_list = []
|
|
79
|
+
|
|
80
|
+
(0...(@size / @segment_size)).each do |segment_index|
|
|
81
|
+
@free_list << (segment_index * @segment_size)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Allocate a segment for the given key.
|
|
86
|
+
#
|
|
87
|
+
# The shared memory file is automatically resized if no segments are available.
|
|
88
|
+
#
|
|
89
|
+
# @parameter key [Object] The key used to identify the allocation.
|
|
90
|
+
# @parameter schema [Array] The `[key, type, offset]` tuples describing the data layout.
|
|
91
|
+
# @returns [Integer | Nil] The offset into the shared memory file, or `nil` if allocation fails.
|
|
92
|
+
def allocate(key, schema)
|
|
93
|
+
if @free_list.empty?
|
|
94
|
+
unless resize(@size * @growth_factor)
|
|
95
|
+
return nil
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
offset = @free_list.shift
|
|
100
|
+
@allocations[key] = {offset: offset, schema: schema}
|
|
101
|
+
|
|
102
|
+
return offset
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Free the segment allocated to the given key.
|
|
106
|
+
#
|
|
107
|
+
# @parameter key [Object] The key used to identify the allocation.
|
|
108
|
+
def free(key)
|
|
109
|
+
if allocation = @allocations.delete(key)
|
|
110
|
+
@free_list << allocation[:offset]
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Get the allocation information for the given key.
|
|
115
|
+
#
|
|
116
|
+
# @parameter key [Object] The key used to identify the allocation.
|
|
117
|
+
# @returns [Hash | Nil] The allocation offset and schema, or `nil` if the key is not allocated.
|
|
118
|
+
def allocation(key)
|
|
119
|
+
@allocations[key]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @attribute [Integer] The current size of the shared memory file.
|
|
123
|
+
attr :size
|
|
124
|
+
|
|
125
|
+
# Update the schema for an existing allocation.
|
|
126
|
+
#
|
|
127
|
+
# @parameter key [Object] The key used to identify the allocation.
|
|
128
|
+
# @parameter schema [Array] The `[key, type, offset]` tuples describing the data layout.
|
|
129
|
+
def update_schema(key, schema)
|
|
130
|
+
if allocation = @allocations[key]
|
|
131
|
+
allocation[:schema] = schema
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Read utilization data from an allocated segment.
|
|
136
|
+
#
|
|
137
|
+
# @parameter key [Object] The key used to identify the allocation.
|
|
138
|
+
# @returns [Hash | Nil] The utilization values, or `nil` if the key is not allocated.
|
|
139
|
+
def read(key)
|
|
140
|
+
allocation = @allocations[key]
|
|
141
|
+
return nil unless allocation
|
|
142
|
+
|
|
143
|
+
offset = allocation[:offset]
|
|
144
|
+
schema = allocation[:schema]
|
|
145
|
+
|
|
146
|
+
result = {}
|
|
147
|
+
schema.each do |field_key, type, field_offset|
|
|
148
|
+
absolute_offset = offset + field_offset
|
|
149
|
+
|
|
150
|
+
begin
|
|
151
|
+
result[field_key] = @buffer.get_value(type, absolute_offset)
|
|
152
|
+
rescue => error
|
|
153
|
+
Console.warn(self, "Failed to read value", type: type, key: field_key, offset: absolute_offset, exception: error)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
return result
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Resize the shared memory file.
|
|
161
|
+
#
|
|
162
|
+
# The new size is rounded up to the nearest page boundary.
|
|
163
|
+
#
|
|
164
|
+
# @parameter new_size [Integer] The requested new size of the shared memory file.
|
|
165
|
+
# @returns [Boolean] Whether the file was resized successfully.
|
|
166
|
+
def resize(new_size)
|
|
167
|
+
old_size = @size
|
|
168
|
+
return false if new_size <= old_size
|
|
169
|
+
|
|
170
|
+
page_size = IO::Buffer::PAGE_SIZE
|
|
171
|
+
new_size = (((new_size + page_size - 1) / page_size) * page_size).to_i
|
|
172
|
+
|
|
173
|
+
begin
|
|
174
|
+
@file.truncate(new_size)
|
|
175
|
+
buffer = IO::Buffer.map(@file, new_size)
|
|
176
|
+
|
|
177
|
+
@buffer&.free
|
|
178
|
+
@buffer = buffer
|
|
179
|
+
|
|
180
|
+
old_segment_count = old_size / @segment_size
|
|
181
|
+
new_segment_count = new_size / @segment_size
|
|
182
|
+
|
|
183
|
+
(old_segment_count...new_segment_count).each do |segment_index|
|
|
184
|
+
@free_list << (segment_index * @segment_size)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
@size = new_size
|
|
188
|
+
|
|
189
|
+
Console.info(self, "Resized shared memory", old_size: old_size, new_size: new_size, segments_added: new_segment_count - old_segment_count)
|
|
190
|
+
|
|
191
|
+
return true
|
|
192
|
+
rescue => error
|
|
193
|
+
Console.error(self, "Failed to resize shared memory", old_size: old_size, new_size: new_size, exception: error)
|
|
194
|
+
return false
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Close the shared memory file.
|
|
199
|
+
def close
|
|
200
|
+
@buffer&.free
|
|
201
|
+
@buffer = nil
|
|
202
|
+
|
|
203
|
+
@file&.close
|
|
204
|
+
@file = nil
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
data/lib/async/utilization.rb
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
require_relative "utilization/version"
|
|
7
7
|
require_relative "utilization/schema"
|
|
8
|
+
require_relative "utilization/namespace"
|
|
8
9
|
require_relative "utilization/registry"
|
|
9
10
|
require_relative "utilization/observer"
|
|
10
11
|
require_relative "utilization/metric"
|
|
12
|
+
require_relative "utilization/segment_store"
|
|
11
13
|
|
|
12
14
|
# @namespace
|
|
13
15
|
module Async
|
data/readme.md
CHANGED
|
@@ -14,6 +14,15 @@ Please see the [project documentation](https://socketry.github.io/async-utilizat
|
|
|
14
14
|
|
|
15
15
|
Please see the [project releases](https://socketry.github.io/async-utilization/releases/index) for all releases.
|
|
16
16
|
|
|
17
|
+
### v0.5.0
|
|
18
|
+
|
|
19
|
+
- Add `Async::Utilization::SegmentStore` for allocating and reading utilization data in shared memory.
|
|
20
|
+
|
|
21
|
+
### v0.4.0
|
|
22
|
+
|
|
23
|
+
- Add `Async::Utilization::Namespace` for composing registry metric names.
|
|
24
|
+
- `Async::Utilization::Metric` is the primary interface, remove `#set`, `#increment`, `#decrement` and `#track` from `Registry`.
|
|
25
|
+
|
|
17
26
|
### v0.3.2
|
|
18
27
|
|
|
19
28
|
- Better observer state handling.
|
|
@@ -34,11 +43,27 @@ Please see the [project releases](https://socketry.github.io/async-utilization/r
|
|
|
34
43
|
|
|
35
44
|
We welcome contributions to this project.
|
|
36
45
|
|
|
37
|
-
1. Fork
|
|
46
|
+
1. Fork the repository.
|
|
38
47
|
2. Create your feature branch (`git checkout -b my-new-feature`).
|
|
39
|
-
3. Commit your changes (`git commit -am 'Add some feature'`).
|
|
48
|
+
3. Commit your changes (`git commit -am 'Add some feature.'`).
|
|
40
49
|
4. Push to the branch (`git push origin my-new-feature`).
|
|
41
|
-
5. Create new
|
|
50
|
+
5. Create a new pull request.
|
|
51
|
+
|
|
52
|
+
### Running Tests
|
|
53
|
+
|
|
54
|
+
To run the test suite:
|
|
55
|
+
|
|
56
|
+
``` bash
|
|
57
|
+
$ bundle exec sus
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Making Releases
|
|
61
|
+
|
|
62
|
+
To make a new release:
|
|
63
|
+
|
|
64
|
+
``` bash
|
|
65
|
+
$ bundle exec bake gem:release:patch # or minor or major
|
|
66
|
+
```
|
|
42
67
|
|
|
43
68
|
### Developer Certificate of Origin
|
|
44
69
|
|
data/releases.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.5.0
|
|
4
|
+
|
|
5
|
+
- Add `Async::Utilization::SegmentStore` for allocating and reading utilization data in shared memory.
|
|
6
|
+
|
|
7
|
+
## v0.4.0
|
|
8
|
+
|
|
9
|
+
- Add `Async::Utilization::Namespace` for composing registry metric names.
|
|
10
|
+
- `Async::Utilization::Metric` is the primary interface, remove `#set`, `#increment`, `#decrement` and `#track` from `Registry`.
|
|
11
|
+
|
|
3
12
|
## v0.3.2
|
|
4
13
|
|
|
5
14
|
- Better observer state handling.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "sus"
|
|
7
|
+
require "async/utilization"
|
|
8
|
+
|
|
9
|
+
describe Async::Utilization::Namespace do
|
|
10
|
+
let(:registry) {Async::Utilization::Registry.new}
|
|
11
|
+
let(:namespace) {registry.namespace(:socket_accept)}
|
|
12
|
+
|
|
13
|
+
it "uses namespaced metric names" do
|
|
14
|
+
metric = namespace.metric(:acquired_count)
|
|
15
|
+
|
|
16
|
+
expect(metric).to be_a(Async::Utilization::Metric)
|
|
17
|
+
expect(metric.name).to be == :socket_accept_acquired_count
|
|
18
|
+
|
|
19
|
+
metric.set(2)
|
|
20
|
+
expect(registry.values).to have_keys(socket_accept_acquired_count: be == 2)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "returns the same metric instance for the same namespaced field" do
|
|
24
|
+
metric1 = namespace.metric(:waiting_count)
|
|
25
|
+
metric2 = namespace.metric(:waiting_count)
|
|
26
|
+
|
|
27
|
+
expect(metric1).to be == metric2
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "supports nested namespaces" do
|
|
31
|
+
metric = namespace.namespace(:long_task).metric(:waiting_count)
|
|
32
|
+
|
|
33
|
+
expect(metric.name).to be == :socket_accept_long_task_waiting_count
|
|
34
|
+
|
|
35
|
+
metric.increment
|
|
36
|
+
expect(registry.values).to have_keys(socket_accept_long_task_waiting_count: be == 1)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "writes namespaced metrics to an observer" do
|
|
40
|
+
schema = Async::Utilization::Schema.build(socket_accept_acquired_count: :u64)
|
|
41
|
+
buffer = IO::Buffer.new(8)
|
|
42
|
+
|
|
43
|
+
observer = Object.new
|
|
44
|
+
observer.define_singleton_method(:schema){schema}
|
|
45
|
+
observer.define_singleton_method(:buffer){buffer}
|
|
46
|
+
|
|
47
|
+
registry.observer = observer
|
|
48
|
+
|
|
49
|
+
namespace.metric(:acquired_count).set(5)
|
|
50
|
+
|
|
51
|
+
expect(buffer.get_value(:u64, 0)).to be == 5
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -8,37 +8,38 @@ require "async/utilization"
|
|
|
8
8
|
|
|
9
9
|
describe Async::Utilization::Registry do
|
|
10
10
|
let(:registry) {Async::Utilization::Registry.new}
|
|
11
|
+
let(:test_field_metric) {registry.metric(:test_field)}
|
|
11
12
|
|
|
12
13
|
it "can increment a value" do
|
|
13
|
-
value =
|
|
14
|
+
value = test_field_metric.increment
|
|
14
15
|
expect(value).to be == 1
|
|
15
16
|
expect(registry.values[:test_field]).to be == 1
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
it "can increment multiple times" do
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
test_field_metric.increment
|
|
21
|
+
test_field_metric.increment
|
|
22
|
+
test_field_metric.increment
|
|
22
23
|
|
|
23
24
|
expect(registry.values[:test_field]).to be == 3
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
it "can decrement a value" do
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
test_field_metric.increment
|
|
29
|
+
test_field_metric.increment
|
|
29
30
|
|
|
30
|
-
value =
|
|
31
|
+
value = test_field_metric.decrement
|
|
31
32
|
expect(value).to be == 1
|
|
32
33
|
expect(registry.values[:test_field]).to be == 1
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
it "can set a value directly" do
|
|
36
|
-
|
|
37
|
+
test_field_metric.set(42)
|
|
37
38
|
expect(registry.values[:test_field]).to be == 42
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
it "can track an operation with auto-decrement" do
|
|
41
|
-
|
|
42
|
+
test_field_metric.track do
|
|
42
43
|
expect(registry.values[:test_field]).to be == 1
|
|
43
44
|
end
|
|
44
45
|
|
|
@@ -47,7 +48,7 @@ describe Async::Utilization::Registry do
|
|
|
47
48
|
|
|
48
49
|
it "decrements even if track block raises an error" do
|
|
49
50
|
begin
|
|
50
|
-
|
|
51
|
+
test_field_metric.track do
|
|
51
52
|
raise "Error!"
|
|
52
53
|
end
|
|
53
54
|
rescue
|
|
@@ -64,13 +65,13 @@ describe Async::Utilization::Registry do
|
|
|
64
65
|
observer.define_singleton_method(:schema){schema}
|
|
65
66
|
observer.define_singleton_method(:buffer){buffer}
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
test_field_metric.set(5)
|
|
68
69
|
registry.observer = observer
|
|
69
70
|
|
|
70
71
|
# Buffer should be synced with the existing value on observer assignment
|
|
71
72
|
expect(buffer.get_value(:u64, 0)).to be == 5
|
|
72
73
|
|
|
73
|
-
|
|
74
|
+
test_field_metric.increment
|
|
74
75
|
|
|
75
76
|
# Buffer should reflect the incremented value
|
|
76
77
|
expect(buffer.get_value(:u64, 0)).to be == 6
|
|
@@ -83,6 +84,14 @@ describe Async::Utilization::Registry do
|
|
|
83
84
|
expect(registry.values).to have_keys(module_test: be == 2)
|
|
84
85
|
end
|
|
85
86
|
|
|
87
|
+
it "can create a namespace" do
|
|
88
|
+
namespace = registry.namespace(:socket_accept)
|
|
89
|
+
|
|
90
|
+
expect(namespace).to be_a(Async::Utilization::Namespace)
|
|
91
|
+
expect(namespace.registry).to be == registry
|
|
92
|
+
expect(namespace.name).to be == :socket_accept
|
|
93
|
+
end
|
|
94
|
+
|
|
86
95
|
it "can use metric for decrement" do
|
|
87
96
|
registry.metric(:module_decrement_test).increment
|
|
88
97
|
registry.metric(:module_decrement_test).increment
|
|
@@ -104,7 +113,7 @@ describe Async::Utilization::Registry do
|
|
|
104
113
|
observer.define_singleton_method(:schema){schema}
|
|
105
114
|
observer.define_singleton_method(:buffer){buffer}
|
|
106
115
|
|
|
107
|
-
|
|
116
|
+
test_field_metric.set(7)
|
|
108
117
|
registry.observer = observer
|
|
109
118
|
|
|
110
119
|
expect(buffer.get_value(:u64, 0)).to be == 7
|
|
@@ -118,11 +127,11 @@ describe Async::Utilization::Registry do
|
|
|
118
127
|
observer.define_singleton_method(:buffer){buffer}
|
|
119
128
|
|
|
120
129
|
registry.observer = observer
|
|
121
|
-
|
|
130
|
+
test_field_metric.set(5)
|
|
122
131
|
expect(buffer.get_value(:u64, 0)).to be == 5
|
|
123
132
|
|
|
124
133
|
registry.observer = nil
|
|
125
|
-
|
|
134
|
+
test_field_metric.set(99)
|
|
126
135
|
|
|
127
136
|
# Buffer unchanged, in-memory value updated
|
|
128
137
|
expect(buffer.get_value(:u64, 0)).to be == 5
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "sus"
|
|
7
|
+
require "sus/fixtures/console/null_logger"
|
|
8
|
+
require "sus/fixtures/temporary_directory_context"
|
|
9
|
+
require "async/utilization"
|
|
10
|
+
|
|
11
|
+
describe Async::Utilization::SegmentStore do
|
|
12
|
+
include Sus::Fixtures::Console::NullLogger
|
|
13
|
+
include Sus::Fixtures::TemporaryDirectoryContext
|
|
14
|
+
|
|
15
|
+
let(:path) {File.join(root, "utilization.shm")}
|
|
16
|
+
let(:page_size) {IO::Buffer::PAGE_SIZE}
|
|
17
|
+
let(:schema) do
|
|
18
|
+
Async::Utilization::Schema.build(
|
|
19
|
+
requests_total: :u64,
|
|
20
|
+
requests_active: :u32,
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "allocates, reads, and reuses segments" do
|
|
25
|
+
store = subject.open(path, size: page_size, segment_size: page_size)
|
|
26
|
+
|
|
27
|
+
first_offset = store.allocate(:first, [])
|
|
28
|
+
expect(first_offset).to be == 0
|
|
29
|
+
expect(store.allocation(:first)).to have_keys(offset: be == 0, schema: be == [])
|
|
30
|
+
|
|
31
|
+
store.update_schema(:first, schema.to_a)
|
|
32
|
+
observer = Async::Utilization::Observer.open(schema, path, page_size, first_offset)
|
|
33
|
+
observer.buffer.set_value(:u64, 0, 12)
|
|
34
|
+
observer.buffer.set_value(:u32, 8, 3)
|
|
35
|
+
|
|
36
|
+
expect(store.read(:first)).to be == {requests_total: 12, requests_active: 3}
|
|
37
|
+
|
|
38
|
+
store.free(:first)
|
|
39
|
+
expect(store.read(:first)).to be_nil
|
|
40
|
+
expect(store.allocate(:second, schema.to_a)).to be == first_offset
|
|
41
|
+
ensure
|
|
42
|
+
observer&.buffer&.free
|
|
43
|
+
store&.close
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "preserves existing observer mappings when resizing" do
|
|
47
|
+
store = subject.open(path, size: page_size, segment_size: page_size)
|
|
48
|
+
first_offset = store.allocate(:first, schema.to_a)
|
|
49
|
+
observer = Async::Utilization::Observer.open(schema, path, page_size, first_offset)
|
|
50
|
+
|
|
51
|
+
observer.buffer.set_value(:u64, 0, 42)
|
|
52
|
+
expect(store.read(:first)[:requests_total]).to be == 42
|
|
53
|
+
|
|
54
|
+
second_offset = store.allocate(:second, schema.to_a)
|
|
55
|
+
expect(second_offset).to be == page_size
|
|
56
|
+
expect(store.size).to be == page_size * 2
|
|
57
|
+
|
|
58
|
+
observer.buffer.set_value(:u64, 0, 99)
|
|
59
|
+
expect(store.read(:first)[:requests_total]).to be == 99
|
|
60
|
+
ensure
|
|
61
|
+
observer&.buffer&.free
|
|
62
|
+
store&.close
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "returns nil when automatic resizing fails" do
|
|
66
|
+
store = subject.open(path, size: page_size, segment_size: page_size)
|
|
67
|
+
store.allocate(:first, schema.to_a)
|
|
68
|
+
|
|
69
|
+
expect(store).to receive(:resize).and_return(false)
|
|
70
|
+
expect(store.allocate(:second, schema.to_a)).to be_nil
|
|
71
|
+
ensure
|
|
72
|
+
store&.close
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "skips fields that cannot be read" do
|
|
76
|
+
store = subject.open(path, size: page_size, segment_size: page_size)
|
|
77
|
+
store.allocate(:worker, [[:invalid, :invalid, 0]])
|
|
78
|
+
|
|
79
|
+
expect(store.read(:worker)).to be == {}
|
|
80
|
+
ensure
|
|
81
|
+
store&.close
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "reports resize failures" do
|
|
85
|
+
store = subject.open(path, size: page_size, segment_size: page_size)
|
|
86
|
+
file = store.instance_variable_get(:@file)
|
|
87
|
+
|
|
88
|
+
expect(file).to receive(:truncate).and_raise(IOError, "Failed to resize")
|
|
89
|
+
expect(store.resize(page_size * 2)).to be_falsey
|
|
90
|
+
ensure
|
|
91
|
+
store&.close
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "only replaces an existing file when requested" do
|
|
95
|
+
original = subject.open(path, size: page_size, segment_size: page_size)
|
|
96
|
+
original.resize(page_size * 2)
|
|
97
|
+
|
|
98
|
+
existing_file = File.open(path, "rb")
|
|
99
|
+
original_size = existing_file.size
|
|
100
|
+
original.close
|
|
101
|
+
|
|
102
|
+
expect do
|
|
103
|
+
subject.open(path, size: page_size, segment_size: page_size)
|
|
104
|
+
end.to raise_exception(Errno::EEXIST)
|
|
105
|
+
|
|
106
|
+
replacement = subject.open(path, size: page_size, segment_size: page_size, replace: true)
|
|
107
|
+
expect(replacement.size).to be == page_size
|
|
108
|
+
expect(existing_file.size).to be == original_size
|
|
109
|
+
ensure
|
|
110
|
+
original&.close
|
|
111
|
+
replacement&.close
|
|
112
|
+
existing_file&.close
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "validates configuration before replacing an existing file" do
|
|
116
|
+
File.write(path, "existing")
|
|
117
|
+
|
|
118
|
+
[
|
|
119
|
+
[{size: 0}, "Size must be a positive integer!"],
|
|
120
|
+
[{segment_size: 0}, "Segment size must be a positive integer!"],
|
|
121
|
+
[{size: page_size, segment_size: page_size * 2}, "Segment size must not exceed size!"],
|
|
122
|
+
[{growth_factor: 1}, "Growth factor must be greater than 1!"],
|
|
123
|
+
].each do |options, message|
|
|
124
|
+
expect do
|
|
125
|
+
subject.open(path, replace: true, **options)
|
|
126
|
+
end.to raise_exception(ArgumentError, message: be == message)
|
|
127
|
+
|
|
128
|
+
expect(File.read(path)).to be == "existing"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "closes the store after yielding it" do
|
|
133
|
+
file = nil
|
|
134
|
+
|
|
135
|
+
result = subject.open(path, size: page_size, segment_size: page_size) do |store|
|
|
136
|
+
file = store.instance_variable_get(:@file)
|
|
137
|
+
expect(file.closed?).to be_falsey
|
|
138
|
+
:result
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
expect(result).to be == :result
|
|
142
|
+
expect(file.closed?).to be_truthy
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it "closes the file when mapping fails" do
|
|
146
|
+
file = File.open(path, "w+bx")
|
|
147
|
+
File.unlink(path)
|
|
148
|
+
|
|
149
|
+
expect(File).to receive(:open).with(path, "w+bx").and_return(file)
|
|
150
|
+
expect(IO::Buffer).to receive(:map).with(file, page_size).and_raise(IOError, "Failed to map")
|
|
151
|
+
|
|
152
|
+
expect do
|
|
153
|
+
subject.open(path, size: page_size, segment_size: page_size)
|
|
154
|
+
end.to raise_exception(IOError, message: be == "Failed to map")
|
|
155
|
+
|
|
156
|
+
expect(file.closed?).to be_truthy
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it "releases acquired resources when initialization fails" do
|
|
160
|
+
file = File.open(path, "w+bx")
|
|
161
|
+
File.unlink(path)
|
|
162
|
+
buffer = IO::Buffer.new(page_size)
|
|
163
|
+
|
|
164
|
+
expect(File).to receive(:open).with(path, "w+bx").and_return(file)
|
|
165
|
+
expect(IO::Buffer).to receive(:map).with(file, page_size).and_return(buffer)
|
|
166
|
+
expect(subject).to receive(:new).and_raise(IOError, "Failed to initialize")
|
|
167
|
+
|
|
168
|
+
expect do
|
|
169
|
+
subject.open(path, size: page_size, segment_size: page_size)
|
|
170
|
+
end.to raise_exception(IOError, message: be == "Failed to initialize")
|
|
171
|
+
|
|
172
|
+
expect(file.closed?).to be_truthy
|
|
173
|
+
expect(buffer.null?).to be_truthy
|
|
174
|
+
end
|
|
175
|
+
end
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: async-utilization
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -58,22 +58,28 @@ extra_rdoc_files: []
|
|
|
58
58
|
files:
|
|
59
59
|
- lib/async/utilization.rb
|
|
60
60
|
- lib/async/utilization/metric.rb
|
|
61
|
+
- lib/async/utilization/namespace.rb
|
|
61
62
|
- lib/async/utilization/observer.rb
|
|
62
63
|
- lib/async/utilization/registry.rb
|
|
63
64
|
- lib/async/utilization/schema.rb
|
|
65
|
+
- lib/async/utilization/segment_store.rb
|
|
64
66
|
- lib/async/utilization/version.rb
|
|
65
67
|
- license.md
|
|
66
68
|
- readme.md
|
|
67
69
|
- releases.md
|
|
68
70
|
- test/async/utilization.rb
|
|
69
71
|
- test/async/utilization/metric.rb
|
|
72
|
+
- test/async/utilization/namespace.rb
|
|
70
73
|
- test/async/utilization/observer.rb
|
|
71
74
|
- test/async/utilization/registry.rb
|
|
72
75
|
- test/async/utilization/schema.rb
|
|
76
|
+
- test/async/utilization/segment_store.rb
|
|
73
77
|
homepage: https://github.com/socketry/async-utilization
|
|
74
78
|
licenses:
|
|
75
79
|
- MIT
|
|
76
80
|
metadata:
|
|
81
|
+
bug_tracker_uri: https://github.com/socketry/async-utilization/issues
|
|
82
|
+
changelog_uri: https://github.com/socketry/async-utilization/blob/main/releases.md
|
|
77
83
|
documentation_uri: https://socketry.github.io/async-utilization/
|
|
78
84
|
source_code_uri: https://github.com/socketry/async-utilization.git
|
|
79
85
|
rdoc_options: []
|
|
@@ -90,7 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
90
96
|
- !ruby/object:Gem::Version
|
|
91
97
|
version: '0'
|
|
92
98
|
requirements: []
|
|
93
|
-
rubygems_version: 4.0.
|
|
99
|
+
rubygems_version: 4.0.10
|
|
94
100
|
specification_version: 4
|
|
95
101
|
summary: High-performance utilization metrics for Async services using shared memory.
|
|
96
102
|
test_files: []
|
metadata.gz.sig
CHANGED
|
Binary file
|