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 +7 -0
- data/.ruby-version +1 -0
- data/README.md +323 -0
- data/Rakefile +12 -0
- data/lib/shadow_link/error.rb +15 -0
- data/lib/shadow_link/mirror.rb +9 -0
- data/lib/shadow_link/shadow.rb +69 -0
- data/lib/shadow_link/version.rb +5 -0
- data/lib/shadow_link.rb +83 -0
- data/sig/shadow_link.rbs +20 -0
- metadata +51 -0
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,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
|
data/lib/shadow_link.rb
ADDED
|
@@ -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
|
data/sig/shadow_link.rbs
ADDED
|
@@ -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: []
|