ratomic 0.1.0 → 0.2.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.
@@ -0,0 +1,72 @@
1
+ use std::sync::{Condvar, Mutex};
2
+
3
+ pub(crate) struct Semaphore {
4
+ // Wrap the state in a heap-allocated Box so the raw pointer `self.inner` remains stable
5
+ inner: *mut SemaphoreInner,
6
+ }
7
+
8
+ struct SemaphoreInner {
9
+ lock: Mutex<u32>, // the current count of permits
10
+ cvar: Condvar,
11
+ }
12
+
13
+ impl Semaphore {
14
+ pub(crate) fn alloc() -> Self {
15
+ Self {
16
+ inner: std::ptr::null_mut(),
17
+ }
18
+ }
19
+
20
+ pub(crate) fn init(&mut self, initial: u32) {
21
+ let inner_struct = SemaphoreInner {
22
+ lock: Mutex::new(initial),
23
+ cvar: Condvar::new(),
24
+ };
25
+ // Box into raw pointer to manage memory life manually
26
+ self.inner = Box::into_raw(Box::new(inner_struct));
27
+ }
28
+
29
+ pub(crate) fn post(&self) {
30
+ if self.inner.is_null() {
31
+ return;
32
+ }
33
+
34
+ let inner = unsafe { &*self.inner };
35
+ let mut count = inner.lock.lock().unwrap();
36
+ *count += 1;
37
+
38
+ // notify to wake up a waiting thread
39
+ inner.cvar.notify_one();
40
+ }
41
+
42
+ pub(crate) fn wait(&self) {
43
+ if self.inner.is_null() {
44
+ return;
45
+ }
46
+
47
+ let inner = unsafe { &*self.inner };
48
+ let mut count = inner.lock.lock().unwrap();
49
+
50
+ // Block the thread while there are no available permits
51
+ while *count == 0 {
52
+ count = inner.cvar.wait(count).unwrap();
53
+ }
54
+
55
+ *count -= 1;
56
+ }
57
+ }
58
+
59
+ impl Drop for Semaphore {
60
+ fn drop(&mut self) {
61
+ if !self.inner.is_null() {
62
+ unsafe {
63
+ // Safely reclaim and drop the heap allocated struct
64
+ let _ = Box::from_raw(self.inner);
65
+ }
66
+ self.inner = std::ptr::null_mut();
67
+ }
68
+ }
69
+ }
70
+
71
+ unsafe impl Send for Semaphore {}
72
+ unsafe impl Sync for Semaphore {}
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ratomic
4
+ # Ruby convenience methods for {Counter}.
5
+ module CounterMethods
6
+ # Read the current counter value.
7
+ #
8
+ # @return [Integer]
9
+ def value
10
+ read
11
+ end
12
+
13
+ # Coerce the counter to an Integer snapshot.
14
+ #
15
+ # @return [Integer]
16
+ def to_i
17
+ read
18
+ end
19
+
20
+ # Check whether the current counter value is zero.
21
+ #
22
+ # @return [Boolean]
23
+ def zero?
24
+ read.zero?
25
+ end
26
+
27
+ # Increment the counter.
28
+ #
29
+ # @param amt [Integer] amount to add
30
+ # @raise [ArgumentError] if +amt+ is negative
31
+ # @return [void]
32
+ def inc(amt = 1)
33
+ raise ArgumentError, "amount must be positive: #{amt}" if amt.negative?
34
+
35
+ increment(amt)
36
+ end
37
+
38
+ # Decrement the counter.
39
+ #
40
+ # @param amt [Integer] amount to subtract
41
+ # @raise [ArgumentError] if +amt+ is negative
42
+ # @return [void]
43
+ def dec(amt = 1)
44
+ raise ArgumentError, "amount must be positive: #{amt}" if amt.negative?
45
+
46
+ decrement(amt)
47
+ end
48
+ end
49
+
50
+ Counter.prepend(CounterMethods)
51
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ratomic
4
+ # A Ractor-shareable concurrent map.
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.
9
+ #
10
+ # This is not a full Hash replacement. Iteration and arbitrary mutable object
11
+ # borrowing are intentionally absent.
12
+ #
13
+ # @example Store pipeline offsets
14
+ # OFFSETS = Ratomic::Map.new
15
+ # OFFSETS[:source_a] = 42
16
+ # OFFSETS[:source_a] # => 42
17
+ Map = ConcurrentHashMap
18
+
19
+ # Ruby convenience methods for {Map}.
20
+ module MapMethods
21
+ # Set a value for +key+.
22
+ #
23
+ # @param key [Object]
24
+ # @param value [Object]
25
+ # @return [void]
26
+ def []=(key, value)
27
+ set(key, value)
28
+ end
29
+
30
+ # Read a value by +key+.
31
+ #
32
+ # Missing keys currently return nil, so storing nil is ambiguous.
33
+ #
34
+ # @param key [Object]
35
+ # @return [Object, nil]
36
+ def [](key)
37
+ get(key)
38
+ end
39
+
40
+ # Alias for #size.
41
+ #
42
+ # @return [Integer]
43
+ def length
44
+ size
45
+ end
46
+
47
+ # Check whether the map currently has no entries.
48
+ #
49
+ # @return [Boolean]
50
+ def empty?
51
+ size.zero?
52
+ end
53
+ end
54
+
55
+ ConcurrentHashMap.prepend(MapMethods)
56
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ratomic
4
+ # A Ractor-safe ownership-transfer pool for mutable Ruby objects.
5
+ #
6
+ # Pool follows a Rust-inspired ownership-transfer model: a pooled object has
7
+ # one active owner at a time. #checkout moves ownership from the pool to the
8
+ # caller; #checkin moves ownership back to the pool. Ruby enforces stale
9
+ # caller references dynamically with Ractor::MovedError.
10
+ #
11
+ # This is ownership transfer, not borrowing. Pool never lends shared mutable
12
+ # references across Ractors.
13
+ #
14
+ # Pool uses a private coordinator Ractor and caller-owned Ractor::Port reply
15
+ # ports. Objects are moved to callers on checkout and moved back to the pool
16
+ # on checkin. This is intentionally different from sharing the same mutable
17
+ # object between Ractors: at any instant, exactly one Ractor owns a checked-out
18
+ # object.
19
+ #
20
+ # @example Reuse mutable buffers safely
21
+ # BUFFERS = Ratomic::Pool.new(4, 1.0) { [] }
22
+ # BUFFERS.with do |buffer|
23
+ # buffer.clear
24
+ # buffer << :change
25
+ # end
26
+ class Pool
27
+ # Create a pool and seed it with +size+ objects from the factory block.
28
+ #
29
+ # @param size [Integer] number of pooled objects
30
+ # @param timeout [Numeric, nil] checkout timeout in seconds, or nil to wait indefinitely
31
+ # @yieldreturn [Object] mutable object to store in the pool
32
+ # @raise [ArgumentError] if +size+ is not positive
33
+ # @raise [LocalJumpError] if no factory block is given
34
+ def initialize(size = 5, timeout = 1.0)
35
+ raise ArgumentError, "pool size must be positive" if size <= 0
36
+ raise LocalJumpError, "no block given" unless block_given?
37
+
38
+ @timeout = timeout&.to_f
39
+ @control = self.class.send(:new_control_ractor)
40
+ size.times { @control.send([:checkin, yield], move: true) }
41
+ freeze
42
+ Ractor.make_shareable(self)
43
+ end
44
+
45
+ # Checkout one object from the pool.
46
+ #
47
+ # The returned object has been moved from the pool to the caller. The caller
48
+ # owns it until it is passed to #checkin.
49
+ #
50
+ # @return [Object, nil] pooled object, or nil after timeout
51
+ def checkout
52
+ reply = Ractor::Port.new
53
+ request_id = reply.object_id
54
+ @control << [:checkout, request_id, reply]
55
+ receive_checkout_reply(reply)
56
+ rescue Timeout::Error
57
+ nil
58
+ ensure
59
+ @control << [:cancel, request_id] if request_id
60
+ reply&.close unless reply&.closed?
61
+ end
62
+
63
+ # Return an object to the pool.
64
+ #
65
+ # This moves ownership from the caller back to the pool. The caller must not
66
+ # use the object after calling this method; Ruby raises Ractor::MovedError
67
+ # for stale references.
68
+ #
69
+ # @param object [Object] previously checked-out pooled object
70
+ # @return [nil]
71
+ def checkin(object)
72
+ @control.send([:checkin, object], move: true)
73
+ nil
74
+ end
75
+
76
+ # Checkout an object, yield it, then move it back to the pool.
77
+ #
78
+ # This is the preferred API because it guarantees checkin through an ensure
79
+ # block. If checkout times out, raises Ratomic::Error and does not yield.
80
+ #
81
+ # @yieldparam object [Object] checked-out pooled object
82
+ # @raise [Ratomic::Error] if checkout times out
83
+ # @return [Object] block return value
84
+ def with
85
+ object = checkout
86
+ raise Ratomic::Error, "pool checkout timeout" if object.nil?
87
+
88
+ yield object
89
+ ensure
90
+ checkin(object) unless object.nil?
91
+ end
92
+
93
+ def self.new_control_ractor
94
+ Ractor.new { Ratomic::Pool.send(:run_control_loop) }
95
+ end
96
+ private_class_method :new_control_ractor
97
+
98
+ def self.run_control_loop
99
+ available = []
100
+ waiting = {}
101
+
102
+ loop do
103
+ command, *args = Ractor.receive
104
+ handle_command(command, args, available, waiting)
105
+ end
106
+ end
107
+ private_class_method :run_control_loop
108
+
109
+ def self.handle_command(command, args, available, waiting)
110
+ case command
111
+ when :checkout
112
+ handle_checkout(args, available, waiting)
113
+ when :checkin
114
+ handle_checkin(args.fetch(0), available, waiting)
115
+ when :cancel
116
+ waiting.delete(args.fetch(0))
117
+ end
118
+ end
119
+ private_class_method :handle_command
120
+
121
+ def self.handle_checkout(args, available, waiting)
122
+ request_id, reply = args
123
+ if (object = available.shift)
124
+ reply.send(object, move: true)
125
+ else
126
+ waiting[request_id] = reply
127
+ end
128
+ end
129
+ private_class_method :handle_checkout
130
+
131
+ def self.handle_checkin(object, available, waiting)
132
+ loop do
133
+ _request_id, reply = waiting.shift
134
+ return available << object if reply.nil?
135
+
136
+ begin
137
+ reply.send(object, move: true)
138
+ return
139
+ rescue Ractor::ClosedError
140
+ next
141
+ end
142
+ end
143
+ end
144
+ private_class_method :handle_checkin
145
+
146
+ private
147
+
148
+ def receive_checkout_reply(reply)
149
+ return reply.receive unless @timeout
150
+
151
+ Timeout.timeout(@timeout) { reply.receive }
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ratomic
4
+ # Ruby convenience methods for {Queue}.
5
+ module QueueMethods
6
+ # Push an item and return the queue for chaining.
7
+ #
8
+ # @param item [Object]
9
+ # @return [Ratomic::Queue]
10
+ def <<(item)
11
+ push(item)
12
+ self
13
+ end
14
+ end
15
+
16
+ Queue.prepend(QueueMethods)
17
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ratomic
4
+ # Internal sentinel object for future Hash-like APIs that need to distinguish
5
+ # missing keys from explicit nil values.
6
+ class Undefined
7
+ # @return [String]
8
+ def inspect
9
+ "#<Undefined>"
10
+ end
11
+ end
12
+
13
+ # Internal shareable missing-value sentinel.
14
+ UNDEFINED = Ractor.make_shareable(Undefined.new)
15
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ratomic
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/ratomic.rb CHANGED
@@ -1,68 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "timeout"
4
+
3
5
  require_relative "ratomic/version"
4
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"
5
12
 
6
13
  module Ratomic
14
+ # Base error for Ratomic-specific runtime failures.
7
15
  class Error < StandardError; end
8
-
9
- ##
10
- # An atomic counter which can be incremented and decremented
11
- # safely by multiple Ractors concurrently.
12
- class Counter
13
- def value
14
- read
15
- end
16
-
17
- def inc(amt = 1)
18
- raise ArgumentError, "amount must be positive: #{amt}" if amt < 0
19
- increment(amt)
20
- end
21
-
22
- def dec(amt = 1)
23
- raise ArgumentError, "amount must be positive: #{amt}" if amt < 0
24
- decrement(amt)
25
- end
26
- end
27
-
28
- class Undefined
29
- def inspect
30
- "#<Undefined>"
31
- end
32
- end
33
- UNDEFINED = Ractor.make_shareable(Undefined.new)
34
-
35
- class Pool < FixedSizeObjectPool
36
- def initialize(size = 5, timeout = 1.0)
37
- super(size, (timeout * 1000).to_i)
38
- end
39
-
40
- def with
41
- obj_and_idx = checkout
42
- if obj_and_idx.nil?
43
- raise Ratomic::Error, "pool checkout timeout"
44
- else
45
- yield obj_and_idx[0]
46
- end
47
- ensure
48
- unless obj_and_idx.nil?
49
- checkin(obj_and_idx[1])
50
- end
51
- end
52
- end
53
-
54
- class Map < ConcurrentHashMap
55
- def []=(key, value)
56
- set(key, value)
57
- end
58
-
59
- def [](key)
60
- get(key)
61
- end
62
-
63
- # TODO add as much of the Hash API as possible.
64
- # Stretch goal? Support Enumerable if DashMap can safely
65
- # iterate.
66
- end
67
-
68
16
  end
data/ratomic.gemspec CHANGED
@@ -5,35 +5,41 @@ require_relative "lib/ratomic/version"
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "ratomic"
7
7
  spec.version = Ratomic::VERSION
8
- spec.authors = ["Mike Perham"]
8
+ spec.authors = ["Mike Perham", "Ken C. Demanawa"]
9
9
  spec.email = ["mike@perham.net"]
10
+ spec.metadata["maintainers"] = "Ken C. Demanawa"
10
11
 
11
12
  spec.summary = "Mutable data structures for Ractors"
12
13
  spec.description = spec.summary
13
14
  spec.homepage = "https://github.com/mperham/ratomic"
14
15
  spec.license = "MIT"
15
- spec.required_ruby_version = ">= 3.4.0"
16
+ spec.required_ruby_version = ">= 4.0.0"
16
17
 
17
18
  spec.metadata["homepage_uri"] = spec.homepage
18
19
  spec.metadata["source_code_uri"] = "https://github.com/mperham/ratomic"
19
20
  spec.metadata["changelog_uri"] = "https://github.com/mperham/ratomic"
21
+ spec.metadata["rubygems_mfa_required"] = "true"
20
22
 
21
23
  # Specify which files should be added to the gem when it is released.
22
24
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
- gemspec = File.basename(__FILE__)
24
- spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
25
- ls.readlines("\x0", chomp: true).select do |f|
26
- (f == gemspec) || f.start_with?(*%w[lib/ ext/ rs/])
27
- end
28
- end
29
- %w(lib/ratomic/ratomic.bundle lib/ratomic/ratomic.so).each do |file|
30
- File.exist?(file) && spec.files << file
31
- end
25
+ spec.files = [
26
+ "Cargo.lock",
27
+ "Cargo.toml",
28
+ "CHANGELOG.md",
29
+ "LICENSE.txt",
30
+ "README.md",
31
+ "ratomic.gemspec"
32
+ ] + Dir[
33
+ "lib/**/*.rb",
34
+ "ext/ratomic/Cargo.toml",
35
+ "ext/ratomic/build.rs",
36
+ "ext/ratomic/extconf.rb",
37
+ "ext/ratomic/src/**/*.rs"
38
+ ]
32
39
  spec.require_paths = ["lib"]
33
40
  spec.extensions = ["ext/ratomic/extconf.rb"]
34
41
 
35
- # Uncomment to register a new dependency of your gem
36
- # spec.add_dependency "example-gem", "~> 1.0"
42
+ spec.add_dependency "rb_sys", "~> 0.9.128"
37
43
 
38
44
  # For more information and examples about making a new gem, check out our
39
45
  # guide at: https://bundler.io/guides/creating_gem.html
metadata CHANGED
@@ -1,14 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ratomic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Perham
8
+ - Ken C. Demanawa
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 2025-03-22 00:00:00.000000000 Z
11
- dependencies: []
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rb_sys
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.9.128
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.9.128
12
27
  description: Mutable data structures for Ractors
13
28
  email:
14
29
  - mike@perham.net
@@ -17,35 +32,38 @@ extensions:
17
32
  - ext/ratomic/extconf.rb
18
33
  extra_rdoc_files: []
19
34
  files:
20
- - ext/ratomic/counter.h
35
+ - CHANGELOG.md
36
+ - Cargo.lock
37
+ - Cargo.toml
38
+ - LICENSE.txt
39
+ - README.md
40
+ - ext/ratomic/Cargo.toml
41
+ - ext/ratomic/build.rs
21
42
  - ext/ratomic/extconf.rb
22
- - ext/ratomic/fixed-size-object-pool.h
23
- - ext/ratomic/hashmap.h
24
- - ext/ratomic/mpmc-queue.h
25
- - ext/ratomic/ratomic.c
43
+ - ext/ratomic/src/counter.rs
44
+ - ext/ratomic/src/fixed_size_object_pool.rs
45
+ - ext/ratomic/src/gc_guard.rs
46
+ - ext/ratomic/src/hashmap.rs
47
+ - ext/ratomic/src/lib.rs
48
+ - ext/ratomic/src/mpmc_queue.rs
49
+ - ext/ratomic/src/sem.rs
26
50
  - lib/ratomic.rb
27
- - lib/ratomic/ratomic.bundle
51
+ - lib/ratomic/counter.rb
52
+ - lib/ratomic/map.rb
53
+ - lib/ratomic/pool.rb
54
+ - lib/ratomic/queue.rb
55
+ - lib/ratomic/undefined.rb
28
56
  - lib/ratomic/version.rb
29
57
  - ratomic.gemspec
30
- - rs/Cargo.lock
31
- - rs/Cargo.toml
32
- - rs/cbindgen.toml
33
- - rs/rust-atomics.h
34
- - rs/src/bin/mpmc_queue.rs
35
- - rs/src/counter.rs
36
- - rs/src/fixed_size_object_pool.rs
37
- - rs/src/gc_guard.rs
38
- - rs/src/hashmap.rs
39
- - rs/src/lib.rs
40
- - rs/src/mpmc_queue.rs
41
- - rs/src/sem.rs
42
58
  homepage: https://github.com/mperham/ratomic
43
59
  licenses:
44
60
  - MIT
45
61
  metadata:
62
+ maintainers: Ken C. Demanawa
46
63
  homepage_uri: https://github.com/mperham/ratomic
47
64
  source_code_uri: https://github.com/mperham/ratomic
48
65
  changelog_uri: https://github.com/mperham/ratomic
66
+ rubygems_mfa_required: 'true'
49
67
  rdoc_options: []
50
68
  require_paths:
51
69
  - lib
@@ -53,14 +71,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
53
71
  requirements:
54
72
  - - ">="
55
73
  - !ruby/object:Gem::Version
56
- version: 3.4.0
74
+ version: 4.0.0
57
75
  required_rubygems_version: !ruby/object:Gem::Requirement
58
76
  requirements:
59
77
  - - ">="
60
78
  - !ruby/object:Gem::Version
61
79
  version: '0'
62
80
  requirements: []
63
- rubygems_version: 3.6.2
81
+ rubygems_version: 4.0.10
64
82
  specification_version: 4
65
83
  summary: Mutable data structures for Ractors
66
84
  test_files: []
@@ -1,45 +0,0 @@
1
- #include "rust-atomics.h"
2
- #include <ruby.h>
3
-
4
- const rb_data_type_t atomic_counter_data = {
5
- .function = {.dfree = RUBY_DEFAULT_FREE},
6
- .flags = RUBY_TYPED_FROZEN_SHAREABLE};
7
-
8
- VALUE rb_atomic_counter_alloc(VALUE klass) {
9
- atomic_counter_t *counter;
10
- TypedData_Make_Struct0(obj, klass, atomic_counter_t, ATOMIC_COUNTER_SIZE,
11
- &atomic_counter_data, counter);
12
- atomic_counter_init(counter, 0);
13
- VALUE rb_cRactor = rb_const_get(rb_cObject, rb_intern("Ractor"));
14
- rb_funcall(rb_cRactor, rb_intern("make_shareable"), 1, obj);
15
- return obj;
16
- }
17
-
18
- VALUE rb_atomic_counter_increment(VALUE self, VALUE amt) {
19
- atomic_counter_t *counter;
20
- TypedData_Get_Struct(self, atomic_counter_t, &atomic_counter_data, counter);
21
- atomic_counter_increment(counter, FIX2LONG(amt));
22
- return Qnil;
23
- }
24
-
25
- VALUE rb_atomic_counter_decrement(VALUE self, VALUE amt) {
26
- atomic_counter_t *counter;
27
- TypedData_Get_Struct(self, atomic_counter_t, &atomic_counter_data, counter);
28
- atomic_counter_decrement(counter, FIX2LONG(amt));
29
- return Qnil;
30
- }
31
-
32
- VALUE rb_atomic_counter_read(VALUE self) {
33
- atomic_counter_t *counter;
34
- TypedData_Get_Struct(self, atomic_counter_t, &atomic_counter_data, counter);
35
- return LONG2FIX(atomic_counter_read(counter));
36
- }
37
-
38
- static void init_counter(VALUE rb_mRoot) {
39
- VALUE rb_cAtomicCounter =
40
- rb_define_class_under(rb_mRoot, "Counter", rb_cObject);
41
- rb_define_alloc_func(rb_cAtomicCounter, rb_atomic_counter_alloc);
42
- rb_define_method(rb_cAtomicCounter, "increment", rb_atomic_counter_increment, 1);
43
- rb_define_method(rb_cAtomicCounter, "decrement", rb_atomic_counter_decrement, 1);
44
- rb_define_method(rb_cAtomicCounter, "read", rb_atomic_counter_read, 0);
45
- }