shadowlink 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1e8bd23384ce00d9ad3fd1569c81ec058e7524af4690757092b0468eb81ad5f7
4
+ data.tar.gz: c0afe6c5a5bd2542dd030af24496c04b6711846351c0f5bb9f12ba90eab3de2a
5
+ SHA512:
6
+ metadata.gz: 56ad8994440d44294b8667a98099de9521fd2b4976a204b31bf403a6150284692cb08ad6a1190984bf1b699dd18d2ea89c625f3436e4573ba928183a524754e3
7
+ data.tar.gz: d87e979252dcbe48228219a1ca13a989b966989ad1d7943a8b7db8dee1d01c9889e4e3267481aca1c325b103bdda28bfea6ad75a4db096c3a06dfdfb779fef75
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 4.0.5
data/README.md ADDED
@@ -0,0 +1,323 @@
1
+ # ShadowLink
2
+
3
+ ShadowLink is a Ruby gem that enables the exchange of objects which typically cannot be shared between Ractors by emulating them via proxies and converting them into a shareable data format.
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to the application's Gemfile by executing:
8
+
9
+ ```bash
10
+ bundle add shadowlink
11
+ ```
12
+
13
+ If bundler is not being used to manage dependencies, install the gem by executing:
14
+
15
+ ```bash
16
+ gem install shadowlink
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ Objects such as ActiveRecord typically cannot be used within a Ractor.
22
+
23
+ ```ruby
24
+ Ractor.new(Monster.all) do |monsters|
25
+ monsters.first.id # An error occurs.
26
+ end
27
+ ```
28
+
29
+ Passing an object that cannot be shared to ShadowLink converts it into a shareable proxy object.
30
+
31
+ ```ruby
32
+ ShadowLink.lurk(Monster.all) do |shadow_monsters|
33
+ Ractor.new(shadow_monsters) do |monsters|
34
+ monsters.first.id
35
+ end.value
36
+ end
37
+ ```
38
+
39
+ You can pass multiple objects.
40
+
41
+ ```ruby
42
+ ShadowLink.lurk(Fairy.new, [Octorok.new, Keese.new]) do |shadow_fairy, shadow_monsters|
43
+ Ractor.new(shadow_fairy, shadow_monsters) do |fairy, monsters|
44
+ # anything
45
+ end.value
46
+ end
47
+ ```
48
+
49
+ The last argument of the `ShadowLink.lurk` block is a Shadow object.
50
+ By using the Shadow object's `#sink` method, you can shadow an object even after `ShadowLink.lurk` has executed.
51
+
52
+ ```ruby
53
+ ShadowLink.lurk(Fairy.new) do |shadow_fairy, shadow|
54
+ shadow_octorok = shadow.sink(Octorok.new)
55
+ Ractor.new(shadow_fairy, shadow_octorok, shadow.sink(Keese.all)) do |fairy, octorok, keese|
56
+ # anything
57
+ end.value
58
+ end
59
+ ```
60
+
61
+ Exiting `ShadowLink.lurk` before the processing inside the Ractor completes causes the port to close, resulting in an error.
62
+ Be sure to wait for the operation to complete using `Ractor#join` or `Ractor#value`.
63
+
64
+ ```ruby
65
+ ShadowLink.lurk(Fairy.new) do |shadow_fairy|
66
+ Ractor.new(shadow_fairy) do |fairy|
67
+ sleep 1
68
+ fairy.id # raise 'Ractor::Port#send': The port was already closed (Ractor::ClosedError)
69
+ end # non wait
70
+ end
71
+ ```
72
+
73
+ If the result of the ShadowLink.lurk block is a shadowed object, the result of ShadowLink.lurk is converted to the original object and returned.
74
+
75
+ ```ruby
76
+ ShadowLink.lurk(Fairy.new) do |shadow_fairy|
77
+ Ractor.new(shadow_fairy) do |fairy|
78
+ fairy # ShadowLink<Fairy> Object
79
+ end.value # ShadowLink<Fairy> Object
80
+ end # Fairy Object
81
+ ```
82
+
83
+ You can also retrieve the original object by using `#seek`.
84
+
85
+ ```ruby
86
+ ShadowLink.lurk(Fairy.new) do |shadow_fairy, shadow|
87
+ result_fairy = Ractor.new(shadow_fairy) do |fairy|
88
+ fairy # ShadowLink<Fairy> Object
89
+ end.value # ShadowLink<Fairy> Object
90
+ shadow.seek(result_fairy) # Fairy Object
91
+ end
92
+ ```
93
+
94
+ When using ShadowLink, an error occurring at the source becomes a `Ractor::RuntimeError` within `ShadowLink.lurk`, but upon exiting `ShadowLink.lurk`, it is converted back into the original error and raised as an exception.
95
+
96
+ ```ruby
97
+ begin
98
+ ShadowLink.lurk(-> { raise 'Gameover' }) do |shadow_process|
99
+ Ractor.new(shadow_process) do |process|
100
+ process.call
101
+ end.value
102
+ rescue
103
+ raise # raise Ractor::RuntimeError
104
+ end
105
+ rescue
106
+ raise # raise Gameover (RuntimeError)
107
+ end
108
+ ```
109
+
110
+ Typically, the main process runs on a single thread, but you can increase the number of processing threads by specifying the thread count via a keyword argument.
111
+ If you run multiple Ractors within `ShadowLink.lurk` and share tasks that involve significant I/O waiting, increasing the number of threads should improve processing efficiency.
112
+
113
+ ```ruby
114
+ ShadowLink.lurk(Fairy.new, thread: 5) do |shadow_fairy|
115
+ 5.times.map do
116
+ Ractor.new(shadow_fairy) do |fairy|
117
+ # anything
118
+ end
119
+ end.each(:join)
120
+ end
121
+ ```
122
+
123
+ ShadowLink does not convert shareable objects; it passes the data through as-is.
124
+ Objects that cannot be shared directly are shared via proxy objects.
125
+ As an exception, String objects are by default converted into shareable objects via `Ractor.make_shareable`.
126
+ If there are objects other than Strings that you wish to make shareable using `Ractor.make_shareable`, you can specify them using keyword arguments.
127
+
128
+ ```ruby
129
+ ShadowLink.lurk(any, make_shareables: [String, Array, Hash]) do |shadow_any|
130
+ Ractor.new(shadow_any) do |obj|
131
+ obj.to_s # Frozen original String object
132
+ obj.to_a # Frozen original Array object
133
+ obj.to_h # Frozen original Hash object
134
+ end.value
135
+ end
136
+ ```
137
+
138
+ If you do not want a String to be converted into a shareable object, you can prevent the conversion by specifying an array that does not contain the String.
139
+ I wouldn't recommend it, as it causes various things to stop working.
140
+
141
+ ```ruby
142
+ string = 'zelda'.dup
143
+ ShadowLink.lurk(string, make_shareables: []) do |shadow_string|
144
+ Ractor.new(shadow_string) do |str|
145
+ str.gsub!('zelda', 'sheik')
146
+ # However, you cannot do `puts str`
147
+ puts str == 'sheik' # false
148
+ end.join
149
+ end
150
+ puts string == 'sheik' # true
151
+ ```
152
+
153
+ ## Performance
154
+
155
+ Performance verification is conducted in the following environment.
156
+
157
+ ```zsh
158
+ % system_profiler SPHardwareDataType
159
+ Hardware:
160
+
161
+ Hardware Overview:
162
+
163
+ Model Name: MacBook Pro
164
+ Model Identifier: Mac16,8
165
+ Model Number: Z1FE000FHJ/A
166
+ Chip: Apple M4 Pro
167
+ Total Number of Cores: 12 (8 Performance and 4 Efficiency)
168
+ Memory: 48 GB
169
+ System Firmware Version: 18000.120.36
170
+ OS Loader Version: 18000.120.36
171
+
172
+ % ruby -v
173
+ ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [arm64-darwin25]
174
+ ```
175
+
176
+ For method calls without arguments, execution is possible with the following performance difference.
177
+
178
+ ```ruby
179
+ require 'benchmark'
180
+
181
+ Benchmark.bm do |x|
182
+ array = (1..10000).to_a
183
+ x.report('non ractor') do
184
+ array.each { array.sum }
185
+ end
186
+ x.report('use frozen object') do
187
+ frozen_array = array.dup.freeze
188
+ array.each do
189
+ Ractor.new(frozen_array) { |arr| arr.sum }.join
190
+ end
191
+ end
192
+ x.report('use shadowlink') do
193
+ ShadowLink.lurk(array) do |shadow_array|
194
+ array.each do
195
+ Ractor.new(shadow_array) { |arr| arr.sum }.join
196
+ end
197
+ end
198
+ end
199
+ end
200
+ ```
201
+
202
+ ```
203
+ user system total real
204
+ non ractor 0.084500 0.000373 0.084873 ( 0.085017)
205
+ use frozen object 0.131565 0.071487 0.203052 ( 0.203015)
206
+ use shadowlink 0.224832 0.189211 0.414043 ( 0.379295)
207
+ ```
208
+
209
+ Methods that accept only shareable objects as arguments can be called with the following performance differences.
210
+
211
+ ```ruby
212
+ require 'benchmark'
213
+
214
+ Benchmark.bm do |x|
215
+ array = (1..10000).to_a
216
+ x.report('non ractor') do
217
+ array.each { array.sum(0) }
218
+ end
219
+ x.report('use frozen object') do
220
+ frozen_array = array.dup.freeze
221
+ array.each do
222
+ Ractor.new(frozen_array) { |arr| arr.sum(0) }.join
223
+ end
224
+ end
225
+ x.report('use shadowlink') do
226
+ ShadowLink.lurk(array) do |shadow_array|
227
+ array.each do
228
+ Ractor.new(shadow_array) { |arr| arr.sum(0) }.join
229
+ end
230
+ end
231
+ end
232
+ end
233
+ ```
234
+
235
+ ```
236
+ user system total real
237
+ non ractor 0.082363 0.000367 0.082730 ( 0.082839)
238
+ use frozen object 0.128988 0.073280 0.202268 ( 0.202182)
239
+ use shadowlink 0.229528 0.192023 0.421551 ( 0.385374)
240
+ ```
241
+
242
+ Methods containing objects that cannot be shared can be called with the following performance differences.
243
+
244
+ ```ruby
245
+ require 'benchmark'
246
+
247
+ Benchmark.bm do |x|
248
+ array = (1..100).to_a
249
+ x.report('non ractor') do
250
+ array.each { array.sum { |i| i * 2 } }
251
+ end
252
+ x.report('use frozen object') do
253
+ frozen_array = array.dup.freeze
254
+ array.each do
255
+ Ractor.new(frozen_array) { |arr| arr.sum { |i| i * 2 } }.join
256
+ end
257
+ end
258
+ x.report('use shadowlink') do
259
+ ShadowLink.lurk(array) do |shadow_array|
260
+ array.each do
261
+ Ractor.new(shadow_array) { |arr| arr.sum { |i| i * 2 } }.join
262
+ end
263
+ end
264
+ end
265
+ end
266
+ ```
267
+
268
+ ```
269
+ user system total real
270
+ non ractor 0.000415 0.000014 0.000429 ( 0.000426)
271
+ use frozen object 0.002826 0.002211 0.005037 ( 0.004948)
272
+ use shadowlink 0.079253 0.076243 0.155496 ( 0.149635)
273
+ ```
274
+
275
+ In practice, parallelization can reduce processing time.
276
+ For example, if you need to read characters 1,000 times with each read taking 0.001 seconds processing them in 10 parallel threads can reduce the execution time to nearly one-tenth of the original.
277
+
278
+ ```ruby
279
+ require 'benchmark'
280
+
281
+ Benchmark.bm do |x|
282
+ process = -> { sleep 0.001; 'result' }
283
+ x.report('non ractor') do
284
+ 1000.times.each { process.call }
285
+ end
286
+ x.report('use thread') do
287
+ 10.times.map do
288
+ Thread.new do
289
+ 100.times.each { process.call }
290
+ end
291
+ end.each(&:join)
292
+ end
293
+ x.report('use shadowlink') do
294
+ ShadowLink.lurk(process, threads: 10) do |shadow_process|
295
+ 10.times.map do
296
+ Ractor.new(shadow_process) do |p|
297
+ 100.times.map { p.call }
298
+ end
299
+ end.each(&:join)
300
+ end
301
+ end
302
+ end
303
+ ```
304
+
305
+ ```
306
+ user system total real
307
+ non ractor 0.003898 0.010412 0.014310 ( 1.269621)
308
+ use thread 0.003713 0.013605 0.017318 ( 0.127920)
309
+ use shadowlink 0.026961 0.033084 0.060045 ( 0.140258)
310
+ ```
311
+
312
+ In this example, threads are faster; however, the difference lies in the ability to process conditional branching and basic arithmetic operations in parallel.
313
+ In cases where calculations are performed simultaneously with data input and output, extremely high-speed data processing becomes possible.
314
+
315
+ ## Development
316
+
317
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
318
+
319
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
320
+
321
+ ## Contributing
322
+
323
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ucks/shadowlink-rb.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ShadowLink
4
+ # Error is a custom error class that wraps an error raised in a shadow.
5
+ # It provides access to the error's shadow object.
6
+ class Error < StandardError
7
+ attr_reader :shadow
8
+
9
+ def initialize(error)
10
+ @shadow = error
11
+ super(error.message)
12
+ freeze
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ShadowLink
4
+ # Mirror is a simple data structure that holds a reference to the original object and its corresponding shadow.
5
+ # It is used internally by the Shadow class to manage the mapping between original objects and their shadows.
6
+ Mirror = Struct.new(:original, :shadow)
7
+
8
+ private_constant :Mirror
9
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ShadowLink
4
+ # Shadow is a class that manages the resources required to use ShadowLink from other Ractors.
5
+ # It provides methods to shadowing processes and manage objects between original and shadow.
6
+ # ShadowLink manages resources in units of the Shadow.
7
+ class Shadow
8
+ def initialize(threads:, make_shareables:)
9
+ @threads = threads
10
+ @make_shareables = make_shareables.freeze
11
+ end
12
+
13
+ def start
14
+ raise 'Shadow is already illuminated' if @port
15
+
16
+ @port = Ractor::Port.new
17
+ @object_map = {}
18
+ @thread_pool = @threads.times.map { Thread.new { observe } }
19
+
20
+ self
21
+ end
22
+
23
+ def close
24
+ raise 'Shadow is not illuminated' unless @port
25
+
26
+ @port.close
27
+ @thread_pool.each(&:exit)
28
+ @object_map = nil
29
+
30
+ self
31
+ end
32
+
33
+ def sink(original)
34
+ raise 'Shadow is not illuminated' unless @port
35
+
36
+ return original if Ractor.shareable?(original)
37
+ return Ractor.make_shareable(original) if ShadowLink.make_shareable?(original, make_shareables: @make_shareables)
38
+
39
+ object_id = original.object_id
40
+ mirror = @object_map[object_id] ||= Mirror.new(original, ShadowLink.new(@port, object_id, make_shareables: @make_shareables))
41
+ mirror.shadow
42
+ end
43
+
44
+ def seek(shadow)
45
+ scoop(shadow.instance_variable_get(:@object_id))
46
+ end
47
+
48
+ def scoop(object_id)
49
+ @object_map[object_id].original
50
+ end
51
+
52
+ private
53
+
54
+ def observe
55
+ loop do
56
+ message = @port.receive
57
+ begin
58
+ block = ->(*args) { message[:block].call(*args.map { |arg| sink(arg) }) } if message[:block]
59
+ value = sink(scoop(message[:object_id])
60
+ .public_send(message[:name], *message[:args], **message[:kwargs], &block))
61
+ message[:port].send({ type: :success, value: })
62
+ rescue StandardError => e
63
+ value = sink(e)
64
+ message[:port].send({ type: :error, value: })
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ShadowLink
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'shadow_link/error'
4
+ require_relative 'shadow_link/mirror'
5
+ require_relative 'shadow_link/shadow'
6
+ require_relative 'shadow_link/version'
7
+
8
+ # ShadowLink is a library that allows you to create a "shadow" of an object in a separate Ractor, enabling concurrent processing and communication between the original object and its shadow. It provides a way to offload work to a separate thread while maintaining a reference to the original object.
9
+ class ShadowLink
10
+ DEFAULT_MAKE_SHAREABLES = [String].freeze
11
+
12
+ def initialize(port, object_id, make_shareables:)
13
+ @port = port
14
+ @object_id = object_id
15
+ @make_shareables = make_shareables
16
+ freeze
17
+ end
18
+
19
+ %i[to_s inspect respond_to_missing?].each do |method_name|
20
+ define_method(method_name) do |*args, **kwargs, &block|
21
+ method_missing(method_name, *args, **kwargs, &block)
22
+ end
23
+ end
24
+
25
+ def method_missing(name, *args, **kwargs, &block) # rubocop:disable Style/MissingRespondToMissing
26
+ process = lambda do |shadow = nil|
27
+ if shadow
28
+ args = args.map { |arg| shadow.sink(arg) }
29
+ kwargs = kwargs.transform_values { |value| shadow.sink(value) }
30
+ block = shadow.sink(block) if block
31
+ end
32
+
33
+ port = Ractor::Port.new
34
+ @port.send({ object_id: @object_id, port:, name:, args:, kwargs:, block: })
35
+
36
+ case port.receive
37
+ in { type: :success, value: }
38
+ value
39
+ in { type: :error, value: }
40
+ raise ShadowLink::Error, value
41
+ in message
42
+ raise "Unexpected message: #{message.inspect}"
43
+ end
44
+ ensure
45
+ port.close
46
+ end
47
+
48
+ convert = ->(value) { ShadowLink.make_shareable?(value, make_shareables: @make_shareables) ? Ractor.make_shareable(value) : value }
49
+ args = args.map(&convert)
50
+ kwargs = kwargs.transform_values(&convert)
51
+
52
+ has_unshareable_args = [*args, *kwargs.values, block].any? { |arg| !Ractor.shareable?(arg) }
53
+ has_unshareable_args ? ShadowLink.lurk(&process) : process.call
54
+ end
55
+
56
+ class << self
57
+ def lurk(*objects, threads: 1, make_shareables: DEFAULT_MAKE_SHAREABLES, &block)
58
+ shadow = ShadowLink::Shadow.new(threads:, make_shareables:).start
59
+
60
+ begin
61
+ result = yield(*objects.map { |object| shadow.sink(object) }, shadow)
62
+ result.is_a?(ShadowLink) ? shadow.seek(result) : result
63
+ rescue Ractor::RemoteError => e
64
+ raise (shadow.seek(e.cause.shadow) if e.cause.is_a?(ShadowLink::Error)) || e
65
+ rescue ShadowLink::Error => e
66
+ raise shadow.seek(e.shadow) || e
67
+ ensure
68
+ shadow.close
69
+ end
70
+ end
71
+
72
+ def make_shareable?(object, make_shareables: DEFAULT_MAKE_SHAREABLES)
73
+ return false if Ractor.shareable?(object)
74
+
75
+ case object
76
+ when *make_shareables
77
+ true
78
+ else
79
+ false
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,20 @@
1
+ # ShadowLink is a library that allows you to create a "shadow" of an object in a separate Ractor, enabling concurrent processing and communication between the original object and its shadow. It provides a way to offload work to a separate thread while maintaining a reference to the original object.
2
+ class ShadowLink
3
+ @port: ::Ractor::Port?
4
+
5
+ @object_id: ::Integer
6
+
7
+ @make_shareables: ::Array[Class]
8
+
9
+ DEFAULT_MAKE_SHAREABLES: ::Array[Class]
10
+
11
+ VERSION: String
12
+
13
+ def initialize: (::Ractor::Port port, ::Integer object_id, make_shareables: ::Array[Class]) -> void
14
+
15
+ def method_missing: (::Symbol name, *untyped args, **untyped kwargs) { (?) -> untyped } -> untyped
16
+
17
+ def self.lurk: (*untyped objects, ?threads: ::Integer, ?make_shareables: ::Array[Class]) { (untyped, untyped) -> untyped } -> untyped
18
+
19
+ def self.make_shareable?: (untyped object, ?make_shareables: ::Array[Class]) -> (false | untyped)
20
+ end
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shadowlink
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - ucks
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: ShadowLink is a Ruby gem that enables the exchange of objects which typically
13
+ cannot be shared between Ractors by emulating them via proxies and converting them
14
+ into a shareable data format.
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - ".ruby-version"
20
+ - README.md
21
+ - Rakefile
22
+ - lib/shadow_link.rb
23
+ - lib/shadow_link/error.rb
24
+ - lib/shadow_link/mirror.rb
25
+ - lib/shadow_link/shadow.rb
26
+ - lib/shadow_link/version.rb
27
+ - sig/shadow_link.rbs
28
+ homepage: https://github.com/ucks/shadowlink-rb
29
+ licenses: []
30
+ metadata:
31
+ homepage_uri: https://github.com/ucks/shadowlink-rb
32
+ source_code_uri: https://github.com/ucks/shadowlink-rb
33
+ rubygems_mfa_required: 'true'
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: 3.2.0
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubygems_version: 4.0.10
49
+ specification_version: 4
50
+ summary: This gem simplifies object sharing between Ractors.
51
+ test_files: []