ratomic 0.2.0 → 0.3.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.
data/lib/ratomic/map.rb CHANGED
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ratomic
4
- # A Ractor-shareable concurrent map.
4
+ # A Ractor-shareable concurrent Hash backed by Rust's DashMap.
5
5
  #
6
- # Map is a public alias for the native ConcurrentHashMap class. It is suitable
7
- # for runtime state with shareable keys and values that are safe to access
8
- # from multiple Ractors, such as counters or immutable offsets.
6
+ # Map gives Ruby code a small, Ruby-shaped API over DashMap's concurrent
7
+ # storage. It is suitable for runtime state with shareable keys and values
8
+ # that are safe to access from multiple Ractors, such as integer counters or
9
+ # immutable offsets.
9
10
  #
10
11
  # This is not a full Hash replacement. Iteration and arbitrary mutable object
11
12
  # borrowing are intentionally absent.
@@ -14,10 +15,171 @@ module Ratomic
14
15
  # OFFSETS = Ratomic::Map.new
15
16
  # OFFSETS[:source_a] = 42
16
17
  # OFFSETS[:source_a] # => 42
17
- Map = ConcurrentHashMap
18
-
19
- # Ruby convenience methods for {Map}.
20
- module MapMethods
18
+ #
19
+ # @!method get(key)
20
+ # Read a value by +key+.
21
+ #
22
+ # Missing keys return nil, so use #key? or #fetch when stored nil values
23
+ # need to be distinguished from missing entries.
24
+ #
25
+ # @param key [Object]
26
+ # @return [Object, nil]
27
+ #
28
+ # @!method set(key, value)
29
+ # Set a value for +key+.
30
+ #
31
+ # @param key [Object]
32
+ # @param value [Object]
33
+ # @return [void]
34
+ #
35
+ # @!method key?(key)
36
+ # Check whether +key+ currently exists in the map.
37
+ #
38
+ # Unlike #get and #[], this distinguishes missing keys from stored nil
39
+ # values.
40
+ #
41
+ # @param key [Object]
42
+ # @return [Boolean]
43
+ #
44
+ # @!method delete(key)
45
+ # Remove +key+ and return its previous value.
46
+ #
47
+ # Missing keys return nil. Stored nil values also return nil; use #key?
48
+ # before deleting if that distinction matters.
49
+ #
50
+ # @param key [Object]
51
+ # @return [Object, nil]
52
+ #
53
+ # @!method clear
54
+ # Remove all entries from the map.
55
+ #
56
+ # @return [void]
57
+ #
58
+ # @!method size
59
+ # Return the current number of entries.
60
+ #
61
+ # Since this is a concurrent map, the value is a moment-in-time observation.
62
+ #
63
+ # @return [Integer]
64
+ #
65
+ # @!method fetch_and_modify(key)
66
+ # Replace the existing value for +key+ with the block return value.
67
+ #
68
+ # TODO: Revisit this name once the Map API settles. Prefer public method
69
+ # names that stay as close as possible to Ruby Hash semantics.
70
+ #
71
+ # The key must already exist. The operation is atomic for the key. The block
72
+ # runs while the map entry is locked, so avoid using this method for
73
+ # Ractor-hot loops or calling back into the same map from inside the block.
74
+ # If the block raises, the previous value is preserved.
75
+ #
76
+ # @param key [Object]
77
+ # @yieldparam value [Object] current value
78
+ # @return [void]
79
+ # @raise [LocalJumpError] if no block is given
80
+ # @raise [Exception] any exception raised by the block
81
+ #
82
+ # @!method compute(key)
83
+ # Atomically compute and store a value for +key+.
84
+ #
85
+ # If +key+ exists, yields the current value. If +key+ is missing, yields
86
+ # nil. The block return value is stored and returned.
87
+ #
88
+ # The operation is atomic for the key. The block runs while the map entry is
89
+ # locked, so avoid using this method for Ractor-hot loops or calling back
90
+ # into the same map from inside the block. Prefer native update helpers such
91
+ # as #increment when they fit the workflow.
92
+ #
93
+ # If the block raises, the previous value is preserved. If the key was
94
+ # missing, no entry is inserted.
95
+ #
96
+ # @param key [Object]
97
+ # @yieldparam value [Object, nil] current value, or nil if missing
98
+ # @return [Object] the newly stored value
99
+ # @raise [LocalJumpError] if no block is given
100
+ # @raise [Exception] any exception raised by the block
101
+ #
102
+ # @!method fetch_or_store(key)
103
+ # Return the existing value for +key+, or atomically store the block result.
104
+ #
105
+ # If +key+ exists, returns the current value and does not yield. If +key+
106
+ # is missing, yields once, stores the block return value, and returns it.
107
+ #
108
+ # The operation is atomic for the key. Under contention, only one stored
109
+ # value wins for a missing key. The block runs while the map entry is locked,
110
+ # so avoid using this method for Ractor-hot loops or calling back into the
111
+ # same map from inside the block.
112
+ #
113
+ # If the block raises, no entry is inserted.
114
+ #
115
+ # @param key [Object]
116
+ # @return [Object] the existing or newly stored value
117
+ # @raise [LocalJumpError] if no block is given
118
+ # @raise [Exception] any exception raised by the block
119
+ #
120
+ # @!method upsert(key, initial)
121
+ # Atomically insert +initial+ for a missing key, or update an existing value.
122
+ #
123
+ # If +key+ is missing, stores and returns +initial+ without yielding. If
124
+ # +key+ exists, yields the current value, stores the block return value, and
125
+ # returns it.
126
+ #
127
+ # The operation is atomic for the key. The block runs while the map entry is
128
+ # locked, so avoid using this method for Ractor-hot loops or calling back
129
+ # into the same map from inside the block. Prefer native update helpers such
130
+ # as #increment when they fit the workflow.
131
+ #
132
+ # If the block raises, the previous value is preserved.
133
+ #
134
+ # @param key [Object]
135
+ # @param initial [Object]
136
+ # @yieldparam value [Object, nil] current value
137
+ # @return [Object] the inserted or newly stored value
138
+ # @raise [LocalJumpError] if no block is given
139
+ # @raise [Exception] any exception raised by the block
140
+ #
141
+ # @!method increment(key, by = 1)
142
+ # Atomically increment the numeric value for +key+.
143
+ #
144
+ # Missing keys start at zero. Existing non-numeric values raise TypeError
145
+ # and are left unchanged. This uses a native update path and is the preferred
146
+ # counter primitive for Ractor-heavy workloads.
147
+ #
148
+ # @param key [Object]
149
+ # @param by [Numeric]
150
+ # @return [Numeric] the newly stored value
151
+ # @raise [TypeError] if +by+ or the existing value is not numeric
152
+ #
153
+ # @!method decrement(key, by = 1)
154
+ # Atomically decrement the numeric value for +key+.
155
+ #
156
+ # Missing keys start at zero.
157
+ #
158
+ # @param key [Object]
159
+ # @param by [Numeric]
160
+ # @return [Numeric] the newly stored value
161
+ # @raise [TypeError] if +by+ or the existing value is not numeric
162
+ #
163
+ # @!method append(key, value)
164
+ # Atomically append +value+ to an Array bucket for +key+.
165
+ #
166
+ # The stored Array is replaced rather than mutated in place.
167
+ #
168
+ # @param key [Object]
169
+ # @param value [Object]
170
+ # @return [Array] the newly stored frozen Array
171
+ # @raise [TypeError] if the existing value is not an Array
172
+ #
173
+ # @!method add_to_set(key, value)
174
+ # Atomically add +value+ to a Set bucket for +key+.
175
+ #
176
+ # The stored Set is replaced rather than mutated in place.
177
+ #
178
+ # @param key [Object]
179
+ # @param value [Object]
180
+ # @return [Set] the newly stored frozen Set
181
+ # @raise [TypeError] if the existing value is not a Set
182
+ class Map
21
183
  # Set a value for +key+.
22
184
  #
23
185
  # @param key [Object]
@@ -37,6 +199,101 @@ module Ratomic
37
199
  get(key)
38
200
  end
39
201
 
202
+ # Fetch a value by +key+.
203
+ #
204
+ # Unlike #[], this distinguishes missing keys from explicit nil values.
205
+ #
206
+ # @param key [Object]
207
+ # @param default [Object]
208
+ # @yieldparam key [Object]
209
+ # @return [Object]
210
+ # @raise [KeyError] if +key+ is missing and no default or block is provided
211
+ def fetch(key, default = UNDEFINED)
212
+ return get(key) if key?(key)
213
+ return yield key if block_given?
214
+ return default unless default.equal?(UNDEFINED)
215
+
216
+ raise KeyError, "key not found: #{key.inspect}"
217
+ end
218
+
219
+ # Atomically increment the numeric value for +key+.
220
+ #
221
+ # Missing keys start at zero. Existing non-numeric values raise TypeError
222
+ # and are left unchanged. This uses a native update path and is the preferred
223
+ # counter primitive for Ractor-heavy workloads.
224
+ #
225
+ # @param key [Object]
226
+ # @param by [Numeric]
227
+ # @return [Numeric] the newly stored value
228
+ # @raise [TypeError] if +by+ or the existing value is not numeric
229
+ def increment(key, by = 1)
230
+ raise TypeError, "amount must be numeric: #{by.inspect}" unless by.is_a?(Numeric)
231
+
232
+ __increment_numeric(key, by)
233
+ end
234
+
235
+ # Atomically decrement the numeric value for +key+.
236
+ #
237
+ # Missing keys start at zero.
238
+ #
239
+ # @param key [Object]
240
+ # @param by [Numeric]
241
+ # @return [Numeric] the newly stored value
242
+ # @raise [TypeError] if +by+ or the existing value is not numeric
243
+ def decrement(key, by = 1)
244
+ raise TypeError, "amount must be numeric: #{by.inspect}" unless by.is_a?(Numeric)
245
+
246
+ increment(key, -by)
247
+ end
248
+
249
+ # Atomically append +value+ to an Array bucket for +key+.
250
+ #
251
+ # The stored Array is replaced rather than mutated in place.
252
+ #
253
+ # @param key [Object]
254
+ # @param value [Object]
255
+ # @return [Array] the newly stored frozen Array
256
+ # @raise [TypeError] if the existing value is not an Array
257
+ def append(key, value)
258
+ missing = !key?(key)
259
+ compute(key) do |old_value|
260
+ if old_value.nil? && missing
261
+ [value].freeze
262
+ else
263
+ unless old_value.is_a?(Array)
264
+ raise TypeError,
265
+ "existing value for #{key.inspect} must be an Array: #{old_value.inspect}"
266
+ end
267
+
268
+ (old_value + [value]).freeze
269
+ end
270
+ end
271
+ end
272
+
273
+ # Atomically add +value+ to a Set bucket for +key+.
274
+ #
275
+ # The stored Set is replaced rather than mutated in place.
276
+ #
277
+ # @param key [Object]
278
+ # @param value [Object]
279
+ # @return [Set] the newly stored frozen Set
280
+ # @raise [TypeError] if the existing value is not a Set
281
+ def add_to_set(key, value)
282
+ missing = !key?(key)
283
+ compute(key) do |old_value|
284
+ if old_value.nil? && missing
285
+ Set[value].freeze
286
+ else
287
+ unless old_value.is_a?(Set)
288
+ raise TypeError,
289
+ "existing value for #{key.inspect} must be a Set: #{old_value.inspect}"
290
+ end
291
+
292
+ (old_value | [value]).freeze
293
+ end
294
+ end
295
+ end
296
+
40
297
  # Alias for #size.
41
298
  #
42
299
  # @return [Integer]
@@ -50,7 +307,14 @@ module Ratomic
50
307
  def empty?
51
308
  size.zero?
52
309
  end
53
- end
54
310
 
55
- ConcurrentHashMap.prepend(MapMethods)
311
+ # Alias for #key?.
312
+ #
313
+ # @param key [Object]
314
+ # @return [Boolean]
315
+ def include?(key)
316
+ key?(key)
317
+ end
318
+ alias member? include?
319
+ end
56
320
  end
data/lib/ratomic/pool.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "timeout"
4
+
3
5
  module Ratomic
4
6
  # A Ractor-safe ownership-transfer pool for mutable Ruby objects.
5
7
  #
@@ -73,6 +75,20 @@ module Ratomic
73
75
  nil
74
76
  end
75
77
 
78
+ # Stop the private coordinator Ractor.
79
+ #
80
+ # This is primarily useful for tests and short-lived scripts. A closed pool
81
+ # should not be used for further checkout/checkin operations.
82
+ #
83
+ # @return [nil]
84
+ def close
85
+ @control << [:shutdown]
86
+ @control.value
87
+ nil
88
+ rescue Ractor::ClosedError, Ractor::Error
89
+ nil
90
+ end
91
+
76
92
  # Checkout an object, yield it, then move it back to the pool.
77
93
  #
78
94
  # This is the preferred API because it guarantees checkin through an ensure
@@ -101,7 +117,7 @@ module Ratomic
101
117
 
102
118
  loop do
103
119
  command, *args = Ractor.receive
104
- handle_command(command, args, available, waiting)
120
+ break if handle_command(command, args, available, waiting) == :shutdown
105
121
  end
106
122
  end
107
123
  private_class_method :run_control_loop
@@ -114,6 +130,8 @@ module Ratomic
114
130
  handle_checkin(args.fetch(0), available, waiting)
115
131
  when :cancel
116
132
  waiting.delete(args.fetch(0))
133
+ when :shutdown
134
+ :shutdown
117
135
  end
118
136
  end
119
137
  private_class_method :handle_command
data/lib/ratomic/queue.rb CHANGED
@@ -1,8 +1,64 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ratomic
4
- # Ruby convenience methods for {Queue}.
5
- module QueueMethods
4
+ # A Ractor-shareable multi-producer, multi-consumer queue.
5
+ #
6
+ # Queue stores Ruby objects in a fixed-size native ring buffer. Push blocks
7
+ # when the queue is full; pop blocks when the queue is empty.
8
+ #
9
+ # @example Push and pop work
10
+ # queue = Ratomic::Queue.new(128)
11
+ # queue << "job"
12
+ # queue.pop # => "job"
13
+ #
14
+ # @!method self.new(capacity)
15
+ # Create a queue with a fixed capacity.
16
+ #
17
+ # @param capacity [Integer]
18
+ # @return [Ratomic::Queue]
19
+ # @raise [ArgumentError] if capacity is outside the supported range
20
+ # @raise [TypeError] if capacity is not an Integer
21
+ #
22
+ # @!method push(item)
23
+ # Push an item, blocking until space is available.
24
+ #
25
+ # @param item [Object]
26
+ # @return [void]
27
+ #
28
+ # @!method pop
29
+ # Pop an item, blocking until one is available.
30
+ #
31
+ # @return [Object]
32
+ #
33
+ # @!method peek
34
+ # Return the next item without removing it.
35
+ #
36
+ # Since this is a concurrent queue, the value is a moment-in-time
37
+ # observation.
38
+ #
39
+ # @return [Object, nil]
40
+ #
41
+ # @!method empty?
42
+ # Check whether the queue currently appears empty.
43
+ #
44
+ # Since this is a concurrent queue, the value is a moment-in-time
45
+ # observation.
46
+ #
47
+ # @return [Boolean]
48
+ #
49
+ # @!method size
50
+ # Return the current queue size.
51
+ #
52
+ # Since this is a concurrent queue, the value is a moment-in-time
53
+ # observation.
54
+ #
55
+ # @return [Integer]
56
+ #
57
+ # @!method length
58
+ # Alias for #size.
59
+ #
60
+ # @return [Integer]
61
+ class Queue
6
62
  # Push an item and return the queue for chaining.
7
63
  #
8
64
  # @param item [Object]
@@ -12,6 +68,4 @@ module Ratomic
12
68
  self
13
69
  end
14
70
  end
15
-
16
- Queue.prepend(QueueMethods)
17
71
  end
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Ratomic provides Ractor-friendly mutable data structures backed by native
4
+ # Rust concurrency primitives.
5
+ #
6
+ # The public API currently includes {Counter}, {Map}, {Queue}, and {Pool}.
3
7
  module Ratomic
4
- VERSION = "0.2.0"
8
+ # Current gem version.
9
+ VERSION = "0.3.0"
5
10
  end
data/lib/ratomic.rb CHANGED
@@ -1,16 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "timeout"
4
-
5
- require_relative "ratomic/version"
6
- require_relative "ratomic/ratomic"
7
- require_relative "ratomic/counter"
8
- require_relative "ratomic/undefined"
9
- require_relative "ratomic/pool"
10
- require_relative "ratomic/map"
11
- require_relative "ratomic/queue"
3
+ require "ratomic/ratomic"
4
+ require "ratomic/version"
12
5
 
13
6
  module Ratomic
14
- # Base error for Ratomic-specific runtime failures.
15
7
  class Error < StandardError; end
16
8
  end
9
+
10
+ require "ratomic/undefined"
11
+ require "ratomic/counter"
12
+ require "ratomic/map"
13
+ require "ratomic/queue"
14
+ require "ratomic/pool"
data/ratomic.gemspec CHANGED
@@ -9,15 +9,16 @@ Gem::Specification.new do |spec|
9
9
  spec.email = ["mike@perham.net"]
10
10
  spec.metadata["maintainers"] = "Ken C. Demanawa"
11
11
 
12
- spec.summary = "Mutable data structures for Ractors"
13
- spec.description = spec.summary
14
- spec.homepage = "https://github.com/mperham/ratomic"
12
+ spec.summary = "Ractor-safe concurrent data structures for Ruby"
13
+ spec.description = "Ractor-safe counters, maps, queues, and ownership-transfer pools " \
14
+ "backed by native Rust concurrency primitives."
15
+ spec.homepage = "https://mperham.github.io/ratomic"
15
16
  spec.license = "MIT"
16
- spec.required_ruby_version = ">= 4.0.0"
17
+ spec.required_ruby_version = [">= 4.0", "< 4.1.dev"]
17
18
 
18
19
  spec.metadata["homepage_uri"] = spec.homepage
19
20
  spec.metadata["source_code_uri"] = "https://github.com/mperham/ratomic"
20
- spec.metadata["changelog_uri"] = "https://github.com/mperham/ratomic"
21
+ spec.metadata["changelog_uri"] = "https://github.com/mperham/ratomic/blob/trunk/CHANGELOG.md"
21
22
  spec.metadata["rubygems_mfa_required"] = "true"
22
23
 
23
24
  # Specify which files should be added to the gem when it is released.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ratomic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Perham
@@ -24,7 +24,8 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: 0.9.128
27
- description: Mutable data structures for Ractors
27
+ description: Ractor-safe counters, maps, queues, and ownership-transfer pools backed
28
+ by native Rust concurrency primitives.
28
29
  email:
29
30
  - mike@perham.net
30
31
  executables: []
@@ -55,14 +56,14 @@ files:
55
56
  - lib/ratomic/undefined.rb
56
57
  - lib/ratomic/version.rb
57
58
  - ratomic.gemspec
58
- homepage: https://github.com/mperham/ratomic
59
+ homepage: https://mperham.github.io/ratomic
59
60
  licenses:
60
61
  - MIT
61
62
  metadata:
62
63
  maintainers: Ken C. Demanawa
63
- homepage_uri: https://github.com/mperham/ratomic
64
+ homepage_uri: https://mperham.github.io/ratomic
64
65
  source_code_uri: https://github.com/mperham/ratomic
65
- changelog_uri: https://github.com/mperham/ratomic
66
+ changelog_uri: https://github.com/mperham/ratomic/blob/trunk/CHANGELOG.md
66
67
  rubygems_mfa_required: 'true'
67
68
  rdoc_options: []
68
69
  require_paths:
@@ -71,7 +72,10 @@ required_ruby_version: !ruby/object:Gem::Requirement
71
72
  requirements:
72
73
  - - ">="
73
74
  - !ruby/object:Gem::Version
74
- version: 4.0.0
75
+ version: '4.0'
76
+ - - "<"
77
+ - !ruby/object:Gem::Version
78
+ version: 4.1.dev
75
79
  required_rubygems_version: !ruby/object:Gem::Requirement
76
80
  requirements:
77
81
  - - ">="
@@ -80,5 +84,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
80
84
  requirements: []
81
85
  rubygems_version: 4.0.10
82
86
  specification_version: 4
83
- summary: Mutable data structures for Ractors
87
+ summary: Ractor-safe concurrent data structures for Ruby
84
88
  test_files: []