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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4114f3169e309e3062d153202d83d3eb7b38031dc47a5fbb0df0e3cf63296e9d
4
- data.tar.gz: 68eca39b660a7d6414e12bb79ee464d3267d62ec4a1a5ab39c23742a6bb645a4
3
+ metadata.gz: 522f4b5822343c23f14422555147e2159e2bac50afbd230a06e985e2cfe1cee4
4
+ data.tar.gz: caa61db8daf6f65a77e319a210ee0079f9a647deed614e7cc5333507967964fa
5
5
  SHA512:
6
- metadata.gz: 45cd92a3a8ae94aa2c2d6847cff090c7e5a1fadd96cd43dad062c2cf1384d9c1743926311151386a32517d41757e267fb35c5e7e276a4f7cead08468a83c9b68
7
- data.tar.gz: 901a1771f0116652411a7cdbb5122669e814b4dc3594646d764179851ba0cf1ef7e8800838191bf2a67e2db3c877513ced9f3df4f58adedc8b4a84f4a0800890
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
- # @parameter registry [Registry] The registry instance to use.
18
- def initialize(name, registry)
28
+ def initialize(name)
19
29
  @name = name.to_sym
20
- @registry = registry
21
30
  @value = 0
22
- @cache_valid = false
23
- @cached_field_info = nil
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
- # @attribute [Numeric] The current value of this metric.
32
- attr :value
33
-
34
- # @attribute [Mutex] The mutex for thread safety.
35
- attr :guard
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
- # Invalidate the cached field information.
50
+ # Set the observer and rebuild cache.
38
51
  #
39
- # Called when the observer changes to force cache rebuild.
40
- def invalidate
41
- @cache_valid = false
42
- @cached_field_info = nil
43
- @cached_buffer = nil
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(@cached_field_info.type, @cached_field_info.offset, 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
- # # Emit metrics - values tracked in registry
24
- # registry.increment(:total_requests)
25
- # registry.track(:active_requests) do
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.guard.synchronize{metric.value}
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, it is notified of all current metric values
65
- # so it can sync its state. The observer must implement `set(field, value)`.
66
- # All cached metrics are invalidated when the observer changes.
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 [#set] The observer to set.
69
+ # @parameter observer [Observer | Nil] The observer to set.
69
70
  def observer=(observer)
70
71
  @guard.synchronize do
71
- # Invalidate all cached metrics
72
+ @observer = observer
73
+
74
+ # Invalidate all cached metrics with new observer (or nil)
72
75
  @metrics.each_value do |metric|
73
- metric.invalidate
76
+ metric.observer = observer
74
77
  end
75
78
 
76
- @observer = observer
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.new(field, self)
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
@@ -7,6 +7,6 @@
7
7
  module Async
8
8
  # @namespace
9
9
  module Utilization
10
- VERSION = "0.3.1"
10
+ VERSION = "0.4.0"
11
11
  end
12
12
  end
@@ -5,6 +5,7 @@
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"
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 and keep cache valid
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 "handles non-page-aligned offsets" do
59
- observer.set(:total_requests, 100)
60
- observer.set(:active_requests, 20)
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 offset
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 + 8)).to be == 20
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 "handles page-aligned offsets without slicing" do
83
- expect(observer).to be_a(Async::Utilization::Observer)
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, 0)).to be == 123
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 = registry.increment(:test_field)
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
- registry.increment(:test_field)
20
- registry.increment(:test_field)
21
- registry.increment(:test_field)
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
- registry.increment(:test_field)
28
- registry.increment(:test_field)
28
+ test_field_metric.increment
29
+ test_field_metric.increment
29
30
 
30
- value = registry.decrement(:test_field)
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
- registry.set(:test_field, 42)
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
- registry.track(:test_field) do
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
- registry.track(:test_field) do
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
- observer = Object.new
62
+ buffer = IO::Buffer.new(8)
75
63
 
76
- # Define methods on observer
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){nil} # No buffer, so write_direct will return false
66
+ observer.define_singleton_method(:buffer){buffer}
82
67
 
83
- registry.set(:test_field, 5)
68
+ test_field_metric.set(5)
84
69
  registry.observer = observer
85
70
 
86
- # Observer should be notified of existing values
87
- expect(values_set).to be(:include?, [:test_field, 5])
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
- # Clear and test new changes
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
- # With no buffer, write_direct fails silently, so no notification
96
- expect(values_set).to be == []
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 "can set observer" do
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
- def observer.set(field, value); end
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
- expect(registry.observer).to be == observer
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.3.1
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.3
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