async-utilization 0.3.1 → 0.4.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 +48 -45
- data/lib/async/utilization/namespace.rb +51 -0
- data/lib/async/utilization/observer.rb +0 -16
- data/lib/async/utilization/registry.rb +27 -63
- data/lib/async/utilization/version.rb +1 -1
- data/lib/async/utilization.rb +1 -0
- data/readme.md +25 -0
- data/releases.md +9 -0
- data/test/async/utilization/metric.rb +25 -25
- data/test/async/utilization/namespace.rb +53 -0
- data/test/async/utilization/observer.rb +9 -49
- data/test/async/utilization/registry.rb +54 -42
- data.tar.gz.sig +0 -0
- metadata +4 -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: 522f4b5822343c23f14422555147e2159e2bac50afbd230a06e985e2cfe1cee4
|
|
4
|
+
data.tar.gz: caa61db8daf6f65a77e319a210ee0079f9a647deed614e7cc5333507967964fa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ebaabb2e0959e3664e4e2a18812d71efa75d967a2fb01a89f8d63af02eb9e36bc564edfc50c7315b4efbbe8ee5f60e865250cb660daa274fa185e6a538994f6c
|
|
7
|
+
data.tar.gz: 7760f55b1383998120a3f7bf0b65b578c8da04bf648c0407f6b3e57040edec2b1487b82356f6d11eaf3bdb02cab9ddc8797eb3c8ee67c930a03d35fec08a5e60
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -11,16 +11,26 @@ module Async
|
|
|
11
11
|
# including the buffer, offset, and type. When the observer changes, the cache
|
|
12
12
|
# is invalidated and rebuilt on the next access.
|
|
13
13
|
class Metric
|
|
14
|
+
# Create a new metric for the given field and observer.
|
|
15
|
+
#
|
|
16
|
+
# @parameter field [Symbol] The field name for this metric.
|
|
17
|
+
# @parameter observer [Observer | Nil] The observer to associate with this metric.
|
|
18
|
+
# @returns [Metric] A new metric instance.
|
|
19
|
+
def self.for(field, observer)
|
|
20
|
+
self.new(field).tap do |metric|
|
|
21
|
+
metric.observer = observer
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
14
25
|
# Initialize a new metric.
|
|
15
26
|
#
|
|
16
27
|
# @parameter name [Symbol] The field name for this metric.
|
|
17
|
-
|
|
18
|
-
def initialize(name, registry)
|
|
28
|
+
def initialize(name)
|
|
19
29
|
@name = name.to_sym
|
|
20
|
-
@registry = registry
|
|
21
30
|
@value = 0
|
|
22
|
-
|
|
23
|
-
@
|
|
31
|
+
|
|
32
|
+
@observer = nil
|
|
33
|
+
@cached_field = nil
|
|
24
34
|
@cached_buffer = nil
|
|
25
35
|
@guard = Mutex.new
|
|
26
36
|
end
|
|
@@ -28,25 +38,44 @@ module Async
|
|
|
28
38
|
# @attribute [Symbol] The field name for this metric.
|
|
29
39
|
attr :name
|
|
30
40
|
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
36
49
|
|
|
37
|
-
#
|
|
50
|
+
# Set the observer and rebuild cache.
|
|
38
51
|
#
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
52
|
+
# This is called when the registry assigns a new observer (or removes it).
|
|
53
|
+
# The cache is invalidated and then immediately recomputed so that the
|
|
54
|
+
# fast write path doesn't need to re-check the observer on the first write.
|
|
55
|
+
#
|
|
56
|
+
# @parameter observer [Observer | Nil] The new observer (or nil).
|
|
57
|
+
def observer=(observer)
|
|
58
|
+
@guard.synchronize do
|
|
59
|
+
@observer = observer
|
|
60
|
+
|
|
61
|
+
if @observer
|
|
62
|
+
if field = @observer.schema[@name]
|
|
63
|
+
if buffer = @observer.buffer
|
|
64
|
+
@cached_field = field
|
|
65
|
+
@cached_buffer = buffer
|
|
66
|
+
|
|
67
|
+
return write_direct(@value)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
@cached_field = nil
|
|
73
|
+
@cached_buffer = nil
|
|
74
|
+
end
|
|
44
75
|
end
|
|
45
76
|
|
|
46
77
|
# Increment the metric value.
|
|
47
78
|
#
|
|
48
|
-
# Uses the fast path (direct buffer write) when cache is valid and observer is available.
|
|
49
|
-
#
|
|
50
79
|
# @returns [Integer] The new value of the field.
|
|
51
80
|
def increment
|
|
52
81
|
@guard.synchronize do
|
|
@@ -103,28 +132,6 @@ module Async
|
|
|
103
132
|
|
|
104
133
|
protected
|
|
105
134
|
|
|
106
|
-
# Check if the cache is valid and rebuild if necessary.
|
|
107
|
-
#
|
|
108
|
-
# Always attempts to build the cache if it's invalid. Returns true if cache
|
|
109
|
-
# is now valid (observer exists, field is in schema, and buffer is available), false otherwise.
|
|
110
|
-
#
|
|
111
|
-
# @returns [bool] True if cache is valid, false otherwise.
|
|
112
|
-
def ensure_cache_valid!
|
|
113
|
-
unless @cache_valid
|
|
114
|
-
if observer = @registry.observer
|
|
115
|
-
if field = observer.schema[@name]
|
|
116
|
-
if buffer = observer.buffer
|
|
117
|
-
@cached_field_info = field
|
|
118
|
-
@cached_buffer = buffer
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
# Once we've validated the cache, even if there was no observer or buffer, we mark it as valid, so that we don't try to revalidate it again:
|
|
124
|
-
@cache_valid = true
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
135
|
# Write directly to the cached buffer if available.
|
|
129
136
|
#
|
|
130
137
|
# This is the fast path that avoids hash lookups. Always ensures cache is valid
|
|
@@ -133,16 +140,12 @@ module Async
|
|
|
133
140
|
# @parameter value [Numeric] The value to write.
|
|
134
141
|
# @returns [Boolean] Whether the write succeeded.
|
|
135
142
|
def write_direct(value)
|
|
136
|
-
self.ensure_cache_valid!
|
|
137
|
-
|
|
138
143
|
if @cached_buffer
|
|
139
|
-
@cached_buffer.set_value(@
|
|
144
|
+
@cached_buffer.set_value(@cached_field.type, @cached_field.offset, value)
|
|
140
145
|
end
|
|
141
146
|
|
|
142
147
|
return true
|
|
143
148
|
rescue => error
|
|
144
|
-
# If write fails, log warning but don't invalidate cache
|
|
145
|
-
# The error might be transient, and invalidating would force hash lookups
|
|
146
149
|
Console.warn(self, "Failed to write metric value!", metric: {name: @name, value: value}, exception: error)
|
|
147
150
|
|
|
148
151
|
return false
|
|
@@ -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
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2026, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
require "console"
|
|
7
6
|
require_relative "schema"
|
|
8
7
|
|
|
9
8
|
module Async
|
|
@@ -71,21 +70,6 @@ module Async
|
|
|
71
70
|
|
|
72
71
|
# @attribute [IO::Buffer] The mapped buffer for shared memory.
|
|
73
72
|
attr :buffer
|
|
74
|
-
|
|
75
|
-
# Set a field value.
|
|
76
|
-
#
|
|
77
|
-
# Writes the value to shared memory at the offset defined by the schema.
|
|
78
|
-
# Only fields defined in the schema will be written.
|
|
79
|
-
#
|
|
80
|
-
# @parameter field [Symbol] The field name to set.
|
|
81
|
-
# @parameter value [Numeric] The value to set.
|
|
82
|
-
def set(field, value)
|
|
83
|
-
if field = @schema[field]
|
|
84
|
-
@buffer.set_value(field.type, field.offset, value)
|
|
85
|
-
end
|
|
86
|
-
rescue => error
|
|
87
|
-
Console.warn(self, "Failed to set field in shared memory!", field: field, exception: error)
|
|
88
|
-
end
|
|
89
73
|
end
|
|
90
74
|
end
|
|
91
75
|
end
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2026, by Samuel Williams.
|
|
5
5
|
|
|
6
|
+
require "console"
|
|
7
|
+
require_relative "namespace"
|
|
8
|
+
|
|
6
9
|
module Async
|
|
7
10
|
module Utilization
|
|
8
11
|
# Registry for emitting utilization metrics.
|
|
@@ -20,9 +23,11 @@ module Async
|
|
|
20
23
|
# @example Create a registry and emit metrics:
|
|
21
24
|
# registry = Async::Utilization::Registry.new
|
|
22
25
|
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
+
# total_requests = registry.metric(:total_requests)
|
|
27
|
+
# total_requests.increment
|
|
28
|
+
#
|
|
29
|
+
# active_requests = registry.metric(:active_requests)
|
|
30
|
+
# active_requests.track do
|
|
26
31
|
# # Handle request - auto-decrements when block completes
|
|
27
32
|
# end
|
|
28
33
|
#
|
|
@@ -35,7 +40,6 @@ module Async
|
|
|
35
40
|
# observer = Async::Utilization::Observer.open(schema, "/path/to/shm", 4096, 0)
|
|
36
41
|
# registry.observer = observer
|
|
37
42
|
class Registry
|
|
38
|
-
|
|
39
43
|
# Initialize a new registry.
|
|
40
44
|
def initialize
|
|
41
45
|
@observer = nil
|
|
@@ -47,81 +51,33 @@ module Async
|
|
|
47
51
|
# @attribute [Object | Nil] The registered observer.
|
|
48
52
|
attr :observer
|
|
49
53
|
|
|
50
|
-
# @attribute [Mutex] The mutex for thread safety.
|
|
51
|
-
attr :guard
|
|
52
|
-
|
|
53
54
|
# Get the current values for all metrics.
|
|
54
55
|
#
|
|
55
56
|
# @returns [Hash] Hash mapping field names to their current values.
|
|
56
57
|
def values
|
|
57
58
|
@metrics.transform_values do |metric|
|
|
58
|
-
metric.
|
|
59
|
+
metric.value
|
|
59
60
|
end
|
|
60
61
|
end
|
|
61
62
|
|
|
62
63
|
# Set the observer for the registry.
|
|
63
64
|
#
|
|
64
|
-
# When an observer is set,
|
|
65
|
-
#
|
|
66
|
-
#
|
|
65
|
+
# When an observer is set, all cached metrics are updated so they write
|
|
66
|
+
# directly to the observer's buffer. The observer must expose `schema`
|
|
67
|
+
# and `buffer` attributes.
|
|
67
68
|
#
|
|
68
|
-
# @parameter observer [
|
|
69
|
+
# @parameter observer [Observer | Nil] The observer to set.
|
|
69
70
|
def observer=(observer)
|
|
70
71
|
@guard.synchronize do
|
|
71
|
-
|
|
72
|
+
@observer = observer
|
|
73
|
+
|
|
74
|
+
# Invalidate all cached metrics with new observer (or nil)
|
|
72
75
|
@metrics.each_value do |metric|
|
|
73
|
-
metric.
|
|
76
|
+
metric.observer = observer
|
|
74
77
|
end
|
|
75
78
|
|
|
76
|
-
|
|
79
|
+
# Console.info(self, "Observer assigned", observer: observer, metric_count: @metrics.size)
|
|
77
80
|
end
|
|
78
|
-
|
|
79
|
-
# Notify observer of all current metric values (outside guard to avoid deadlock)
|
|
80
|
-
@metrics.each do |name, metric|
|
|
81
|
-
value = metric.guard.synchronize{metric.value}
|
|
82
|
-
observer.set(name, value)
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Set a field value.
|
|
87
|
-
#
|
|
88
|
-
# Delegates to the metric instance for the given field.
|
|
89
|
-
#
|
|
90
|
-
# @parameter field [Symbol] The field name to set.
|
|
91
|
-
# @parameter value [Numeric] The value to set.
|
|
92
|
-
def set(field, value)
|
|
93
|
-
metric(field).set(value)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# Increment a field value.
|
|
97
|
-
#
|
|
98
|
-
# Delegates to the metric instance for the given field.
|
|
99
|
-
#
|
|
100
|
-
# @parameter field [Symbol] The field name to increment.
|
|
101
|
-
# @returns [Integer] The new value of the field.
|
|
102
|
-
def increment(field)
|
|
103
|
-
metric(field).increment
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Track an operation: increment before the block, decrement after it completes.
|
|
107
|
-
#
|
|
108
|
-
# Delegates to the metric instance for the given field.
|
|
109
|
-
#
|
|
110
|
-
# @parameter field [Symbol] The field name to track.
|
|
111
|
-
# @yield The operation to track.
|
|
112
|
-
# @returns [Object] The block's return value.
|
|
113
|
-
def track(field, &block)
|
|
114
|
-
metric(field).track(&block)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Decrement a field value.
|
|
118
|
-
#
|
|
119
|
-
# Delegates to the metric instance for the given field.
|
|
120
|
-
#
|
|
121
|
-
# @parameter field [Symbol] The field name to decrement.
|
|
122
|
-
# @returns [Integer] The new value of the field.
|
|
123
|
-
def decrement(field)
|
|
124
|
-
metric(field).decrement
|
|
125
81
|
end
|
|
126
82
|
|
|
127
83
|
# Get a cached metric reference for a field.
|
|
@@ -135,9 +91,17 @@ module Async
|
|
|
135
91
|
field = field.to_sym
|
|
136
92
|
|
|
137
93
|
@guard.synchronize do
|
|
138
|
-
@metrics[field] ||= Metric.
|
|
94
|
+
@metrics[field] ||= Metric.for(field, @observer)
|
|
139
95
|
end
|
|
140
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
|
|
141
105
|
end
|
|
142
106
|
end
|
|
143
107
|
end
|
data/lib/async/utilization.rb
CHANGED
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.4.0
|
|
18
|
+
|
|
19
|
+
- Add `Async::Utilization::Namespace` for composing registry metric names.
|
|
20
|
+
- `Async::Utilization::Metric` is the primary interface, remove `#set`, `#increment`, `#decrement` and `#track` from `Registry`.
|
|
21
|
+
|
|
22
|
+
### v0.3.2
|
|
23
|
+
|
|
24
|
+
- Better observer state handling.
|
|
25
|
+
|
|
17
26
|
### v0.3.1
|
|
18
27
|
|
|
19
28
|
- Remove unused `thread-local` dependency.
|
|
@@ -36,6 +45,22 @@ We welcome contributions to this project.
|
|
|
36
45
|
4. Push to the branch (`git push origin my-new-feature`).
|
|
37
46
|
5. Create new Pull Request.
|
|
38
47
|
|
|
48
|
+
### Running Tests
|
|
49
|
+
|
|
50
|
+
To run the test suite:
|
|
51
|
+
|
|
52
|
+
``` shell
|
|
53
|
+
bundle exec sus
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Making Releases
|
|
57
|
+
|
|
58
|
+
To make a new release:
|
|
59
|
+
|
|
60
|
+
``` shell
|
|
61
|
+
bundle exec bake gem:release:patch # or minor or major
|
|
62
|
+
```
|
|
63
|
+
|
|
39
64
|
### Developer Certificate of Origin
|
|
40
65
|
|
|
41
66
|
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,14 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.4.0
|
|
4
|
+
|
|
5
|
+
- Add `Async::Utilization::Namespace` for composing registry metric names.
|
|
6
|
+
- `Async::Utilization::Metric` is the primary interface, remove `#set`, `#increment`, `#decrement` and `#track` from `Registry`.
|
|
7
|
+
|
|
8
|
+
## v0.3.2
|
|
9
|
+
|
|
10
|
+
- Better observer state handling.
|
|
11
|
+
|
|
3
12
|
## v0.3.1
|
|
4
13
|
|
|
5
14
|
- Remove unused `thread-local` dependency.
|
|
@@ -154,8 +154,8 @@ describe Async::Utilization::Metric do
|
|
|
154
154
|
end
|
|
155
155
|
|
|
156
156
|
it "writes directly to shared memory when observer is set" do
|
|
157
|
-
registry.observer = observer
|
|
158
157
|
metric = registry.metric(:total_requests)
|
|
158
|
+
registry.observer = observer
|
|
159
159
|
|
|
160
160
|
metric.set(42)
|
|
161
161
|
|
|
@@ -216,22 +216,6 @@ describe Async::Utilization::Metric do
|
|
|
216
216
|
expect(metric1).to be == metric2
|
|
217
217
|
end
|
|
218
218
|
|
|
219
|
-
it "falls back to observer.set when write_direct fails" do
|
|
220
|
-
registry.observer = observer
|
|
221
|
-
metric = registry.metric(:total_requests)
|
|
222
|
-
|
|
223
|
-
# Force cache to be invalid by invalidating it
|
|
224
|
-
metric.invalidate
|
|
225
|
-
|
|
226
|
-
# Set a value - should fall back to observer.set
|
|
227
|
-
metric.set(42)
|
|
228
|
-
expect(metric.value).to be == 42
|
|
229
|
-
|
|
230
|
-
# Verify it was written to shared memory
|
|
231
|
-
buffer = IO::Buffer.map(File.open(shm_path, "r+b"), file_size, 0)
|
|
232
|
-
expect(buffer.get_value(:u64, 0)).to be == 42
|
|
233
|
-
end
|
|
234
|
-
|
|
235
219
|
it "handles write errors gracefully" do
|
|
236
220
|
registry.observer = observer
|
|
237
221
|
metric = registry.metric(:total_requests)
|
|
@@ -239,10 +223,6 @@ describe Async::Utilization::Metric do
|
|
|
239
223
|
# Set a value first to build the cache
|
|
240
224
|
metric.set(10)
|
|
241
225
|
|
|
242
|
-
# Verify cache is built
|
|
243
|
-
expect(metric.instance_variable_get(:@cache_valid)).to be == true
|
|
244
|
-
cached_buffer = metric.instance_variable_get(:@cached_buffer)
|
|
245
|
-
|
|
246
226
|
# Create an invalid buffer that will raise an error
|
|
247
227
|
invalid_buffer = Object.new
|
|
248
228
|
def invalid_buffer.set_value(type, offset, value)
|
|
@@ -251,13 +231,10 @@ describe Async::Utilization::Metric do
|
|
|
251
231
|
|
|
252
232
|
metric.instance_variable_set(:@cached_buffer, invalid_buffer)
|
|
253
233
|
|
|
254
|
-
# Should not raise, but log warning
|
|
234
|
+
# Should not raise, but log warning
|
|
255
235
|
metric.set(42)
|
|
256
236
|
expect(metric.value).to be == 42
|
|
257
237
|
|
|
258
|
-
# Cache should remain valid (not invalidated on error)
|
|
259
|
-
expect(metric.instance_variable_get(:@cache_valid)).to be == true
|
|
260
|
-
|
|
261
238
|
# Assert that a warning was logged
|
|
262
239
|
expect_console.to have_logged(
|
|
263
240
|
severity: be == :warn,
|
|
@@ -265,4 +242,27 @@ describe Async::Utilization::Metric do
|
|
|
265
242
|
message: be == "Failed to write metric value!"
|
|
266
243
|
)
|
|
267
244
|
end
|
|
245
|
+
|
|
246
|
+
it "clears cache when observer is removed" do
|
|
247
|
+
registry.observer = observer
|
|
248
|
+
metric = registry.metric(:total_requests)
|
|
249
|
+
metric.set(10)
|
|
250
|
+
|
|
251
|
+
# Remove observer — cache should be cleared
|
|
252
|
+
registry.observer = nil
|
|
253
|
+
|
|
254
|
+
# Write should not go to the old buffer
|
|
255
|
+
metric.set(99)
|
|
256
|
+
|
|
257
|
+
buffer = IO::Buffer.map(File.open(shm_path, "r+b"), file_size, 0)
|
|
258
|
+
expect(buffer.get_value(:u64, 0)).to be == 10
|
|
259
|
+
|
|
260
|
+
# In-memory value is still updated
|
|
261
|
+
expect(metric.value).to be == 99
|
|
262
|
+
|
|
263
|
+
# Re-attaching observer should sync the current value
|
|
264
|
+
registry.observer = observer
|
|
265
|
+
buffer = IO::Buffer.map(File.open(shm_path, "r+b"), file_size, 0)
|
|
266
|
+
expect(buffer.get_value(:u64, 0)).to be == 99
|
|
267
|
+
end
|
|
268
268
|
end
|
|
@@ -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
|
|
@@ -4,13 +4,11 @@
|
|
|
4
4
|
# Copyright, 2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require "sus"
|
|
7
|
-
require "sus/fixtures/console/captured_logger"
|
|
8
7
|
require "sus/fixtures/temporary_directory_context"
|
|
9
8
|
require "async/utilization"
|
|
10
9
|
require "fileutils"
|
|
11
10
|
|
|
12
11
|
describe Async::Utilization::Observer do
|
|
13
|
-
include Sus::Fixtures::Console::CapturedLogger
|
|
14
12
|
include Sus::Fixtures::TemporaryDirectoryContext
|
|
15
13
|
|
|
16
14
|
let(:shm_path) {File.join(root, "test.shm")}
|
|
@@ -41,68 +39,30 @@ describe Async::Utilization::Observer do
|
|
|
41
39
|
expect(observer.schema).to be == schema
|
|
42
40
|
end
|
|
43
41
|
|
|
44
|
-
it "can write values to shared memory" do
|
|
45
|
-
observer.set(:total_requests, 42)
|
|
46
|
-
observer.set(:active_requests, 5)
|
|
47
|
-
|
|
48
|
-
# Read back from file to verify
|
|
49
|
-
buffer = IO::Buffer.map(File.open(shm_path, "r+b"), file_size, 0)
|
|
50
|
-
expect(buffer.get_value(:u64, 0)).to be == 42
|
|
51
|
-
expect(buffer.get_value(:u32, 8)).to be == 5
|
|
52
|
-
end
|
|
53
|
-
|
|
54
42
|
with "non-page-aligned offsets" do
|
|
55
43
|
let(:file_size) {IO::Buffer::PAGE_SIZE * 2}
|
|
56
44
|
let(:offset) {100} # Not page-aligned
|
|
57
45
|
|
|
58
|
-
it "
|
|
59
|
-
observer.
|
|
60
|
-
observer.
|
|
46
|
+
it "maps values at the correct offset" do
|
|
47
|
+
observer.buffer.set_value(:u64, schema[:total_requests].offset, 100)
|
|
48
|
+
observer.buffer.set_value(:u32, schema[:active_requests].offset, 20)
|
|
61
49
|
|
|
62
|
-
# Read back from file at the correct
|
|
50
|
+
# Read back from file at the correct byte position
|
|
63
51
|
buffer = IO::Buffer.map(File.open(shm_path, "r+b"), file_size, 0)
|
|
64
|
-
expect(buffer.get_value(:u64, offset)).to be == 100
|
|
65
|
-
expect(buffer.get_value(:u32, offset +
|
|
52
|
+
expect(buffer.get_value(:u64, offset + schema[:total_requests].offset)).to be == 100
|
|
53
|
+
expect(buffer.get_value(:u32, offset + schema[:active_requests].offset)).to be == 20
|
|
66
54
|
end
|
|
67
55
|
end
|
|
68
56
|
|
|
69
|
-
it "ignores fields not in schema" do
|
|
70
|
-
# Should not raise an error
|
|
71
|
-
observer.set(:unknown_field, 999)
|
|
72
|
-
|
|
73
|
-
# Verify nothing was written
|
|
74
|
-
buffer = IO::Buffer.map(File.open(shm_path, "r+b"), file_size, 0)
|
|
75
|
-
expect(buffer.get_value(:u64, 0)).to be == 0
|
|
76
|
-
end
|
|
77
|
-
|
|
78
57
|
with "page-aligned offsets" do
|
|
79
58
|
let(:file_size) {page_size * 2}
|
|
80
59
|
let(:segment_size) {page_size}
|
|
81
60
|
|
|
82
|
-
it "
|
|
83
|
-
|
|
84
|
-
observer.set(:total_requests, 123)
|
|
61
|
+
it "maps values at the correct offset" do
|
|
62
|
+
observer.buffer.set_value(:u64, schema[:total_requests].offset, 123)
|
|
85
63
|
|
|
86
|
-
# Verify value was written
|
|
87
64
|
buffer = IO::Buffer.map(File.open(shm_path, "r+b"), file_size, 0)
|
|
88
|
-
expect(buffer.get_value(:u64,
|
|
65
|
+
expect(buffer.get_value(:u64, schema[:total_requests].offset)).to be == 123
|
|
89
66
|
end
|
|
90
67
|
end
|
|
91
|
-
|
|
92
|
-
it "handles errors gracefully when setting values" do
|
|
93
|
-
# Create an invalid buffer that will cause an error
|
|
94
|
-
# We'll mock the buffer to raise an error
|
|
95
|
-
buffer = observer.instance_variable_get(:@buffer)
|
|
96
|
-
expect(buffer).to receive(:set_value).and_raise(IOError, "Buffer error")
|
|
97
|
-
|
|
98
|
-
# Should not raise, but log a warning
|
|
99
|
-
observer.set(:total_requests, 42)
|
|
100
|
-
|
|
101
|
-
# Assert that a warning was logged
|
|
102
|
-
expect_console.to have_logged(
|
|
103
|
-
severity: be == :warn,
|
|
104
|
-
subject: be_a(Async::Utilization::Observer),
|
|
105
|
-
message: be == "Failed to set field in shared memory!"
|
|
106
|
-
)
|
|
107
|
-
end
|
|
108
68
|
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
|
|
@@ -56,44 +57,24 @@ describe Async::Utilization::Registry do
|
|
|
56
57
|
expect(registry.values[:test_field]).to be == 0
|
|
57
58
|
end
|
|
58
59
|
|
|
59
|
-
it "can set an observer" do
|
|
60
|
-
observer = Object.new
|
|
61
|
-
def observer.set(field, value); end
|
|
62
|
-
|
|
63
|
-
registry.set(:test_field, 10)
|
|
64
|
-
registry.observer = observer
|
|
65
|
-
|
|
66
|
-
expect(registry.observer).to be == observer
|
|
67
|
-
end
|
|
68
|
-
|
|
69
60
|
it "notifies observer when values change" do
|
|
70
|
-
values_set = []
|
|
71
|
-
|
|
72
|
-
# Create a proper observer with schema
|
|
73
61
|
schema = Async::Utilization::Schema.build(test_field: :u64)
|
|
74
|
-
|
|
62
|
+
buffer = IO::Buffer.new(8)
|
|
75
63
|
|
|
76
|
-
|
|
77
|
-
observer.define_singleton_method(:set) do |field, value|
|
|
78
|
-
values_set << [field, value]
|
|
79
|
-
end
|
|
64
|
+
observer = Object.new
|
|
80
65
|
observer.define_singleton_method(:schema){schema}
|
|
81
|
-
observer.define_singleton_method(:buffer){
|
|
66
|
+
observer.define_singleton_method(:buffer){buffer}
|
|
82
67
|
|
|
83
|
-
|
|
68
|
+
test_field_metric.set(5)
|
|
84
69
|
registry.observer = observer
|
|
85
70
|
|
|
86
|
-
#
|
|
87
|
-
expect(
|
|
71
|
+
# Buffer should be synced with the existing value on observer assignment
|
|
72
|
+
expect(buffer.get_value(:u64, 0)).to be == 5
|
|
88
73
|
|
|
89
|
-
|
|
90
|
-
# Note: Since observer has no buffer, write_direct will return false
|
|
91
|
-
# and no notification will occur (as per new design)
|
|
92
|
-
values_set.clear
|
|
93
|
-
registry.increment(:test_field)
|
|
74
|
+
test_field_metric.increment
|
|
94
75
|
|
|
95
|
-
#
|
|
96
|
-
expect(
|
|
76
|
+
# Buffer should reflect the incremented value
|
|
77
|
+
expect(buffer.get_value(:u64, 0)).to be == 6
|
|
97
78
|
end
|
|
98
79
|
|
|
99
80
|
it "uses metric method for fast path" do
|
|
@@ -103,6 +84,14 @@ describe Async::Utilization::Registry do
|
|
|
103
84
|
expect(registry.values).to have_keys(module_test: be == 2)
|
|
104
85
|
end
|
|
105
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
|
+
|
|
106
95
|
it "can use metric for decrement" do
|
|
107
96
|
registry.metric(:module_decrement_test).increment
|
|
108
97
|
registry.metric(:module_decrement_test).increment
|
|
@@ -117,12 +106,35 @@ describe Async::Utilization::Registry do
|
|
|
117
106
|
expect(registry.values).to have_keys(module_set_test: be == 99)
|
|
118
107
|
end
|
|
119
108
|
|
|
120
|
-
it "
|
|
109
|
+
it "wires up existing metrics when observer is assigned" do
|
|
110
|
+
schema = Async::Utilization::Schema.build(test_field: :u64)
|
|
111
|
+
buffer = IO::Buffer.new(8)
|
|
112
|
+
observer = Object.new
|
|
113
|
+
observer.define_singleton_method(:schema){schema}
|
|
114
|
+
observer.define_singleton_method(:buffer){buffer}
|
|
115
|
+
|
|
116
|
+
test_field_metric.set(7)
|
|
117
|
+
registry.observer = observer
|
|
118
|
+
|
|
119
|
+
expect(buffer.get_value(:u64, 0)).to be == 7
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "stops writing to buffer when observer is removed" do
|
|
123
|
+
schema = Async::Utilization::Schema.build(test_field: :u64)
|
|
124
|
+
buffer = IO::Buffer.new(8)
|
|
121
125
|
observer = Object.new
|
|
122
|
-
|
|
126
|
+
observer.define_singleton_method(:schema){schema}
|
|
127
|
+
observer.define_singleton_method(:buffer){buffer}
|
|
123
128
|
|
|
124
129
|
registry.observer = observer
|
|
130
|
+
test_field_metric.set(5)
|
|
131
|
+
expect(buffer.get_value(:u64, 0)).to be == 5
|
|
132
|
+
|
|
133
|
+
registry.observer = nil
|
|
134
|
+
test_field_metric.set(99)
|
|
125
135
|
|
|
126
|
-
|
|
136
|
+
# Buffer unchanged, in-memory value updated
|
|
137
|
+
expect(buffer.get_value(:u64, 0)).to be == 5
|
|
138
|
+
expect(registry.values[:test_field]).to be == 99
|
|
127
139
|
end
|
|
128
140
|
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.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -58,6 +58,7 @@ 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
|
|
@@ -67,6 +68,7 @@ files:
|
|
|
67
68
|
- releases.md
|
|
68
69
|
- test/async/utilization.rb
|
|
69
70
|
- test/async/utilization/metric.rb
|
|
71
|
+
- test/async/utilization/namespace.rb
|
|
70
72
|
- test/async/utilization/observer.rb
|
|
71
73
|
- test/async/utilization/registry.rb
|
|
72
74
|
- test/async/utilization/schema.rb
|
|
@@ -90,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
90
92
|
- !ruby/object:Gem::Version
|
|
91
93
|
version: '0'
|
|
92
94
|
requirements: []
|
|
93
|
-
rubygems_version: 4.0.
|
|
95
|
+
rubygems_version: 4.0.6
|
|
94
96
|
specification_version: 4
|
|
95
97
|
summary: High-performance utilization metrics for Async services using shared memory.
|
|
96
98
|
test_files: []
|
metadata.gz.sig
CHANGED
|
Binary file
|