ractor-server 0.1.1 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +1 -1
- data/.rubocop.yml +5 -0
- data/README.md +53 -6
- data/lib/ractor/server/client.rb +20 -6
- data/lib/ractor/server/request.rb +57 -9
- data/lib/ractor/server/server.rb +21 -12
- data/lib/ractor/server/talk.rb +2 -21
- data/lib/ractor/server/version.rb +1 -1
- data/ractor-server.gemspec +1 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e363bbd188b2a662459734263104b8e18a6fc31984a4ba76d258b8bc49c7ae07
|
4
|
+
data.tar.gz: daf7ade9149e2ee7ff3e39d71def5516566e5329751caef920d2ae0cd0e25fe1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6395fd4a6db49def30c07f3031f6ffc3fb68b2110c51d14dc2c4d6bf0eea5b5bf16ba424207b39d469a45bcd30e8a4387dd841d81884840e5c53078445444061
|
7
|
+
data.tar.gz: 87ce7921d6973573f6a7b281a5e97234ceab96b50dc75388527e09e1380bb274b3d5a71b5de2409499cea8732188f978f7ad82646cdcd4a5b6bebdfdce89ed4f
|
data/.github/workflows/main.yml
CHANGED
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
@@ -15,13 +15,13 @@ class RactorHash < Hash
|
|
15
15
|
include Ractor::Server
|
16
16
|
end
|
17
17
|
|
18
|
-
H = RactorHash.start
|
18
|
+
H = RactorHash.start # => starts Server, returns instance of a Client
|
19
19
|
|
20
|
-
Ractor.new { H[:example] = 42 }.take
|
20
|
+
Ractor.new { H[:example] = 42 }.take # => 42
|
21
21
|
puts Ractor.new { H[:example] }.take # => 42
|
22
22
|
```
|
23
23
|
|
24
|
-
Calls
|
24
|
+
Calls are atomic but also allow reentrant calls from blocks:
|
25
25
|
|
26
26
|
```ruby
|
27
27
|
ractors = 3.times.map do |i|
|
@@ -38,6 +38,34 @@ puts H # => {:example => 42, :foo => 0, :bar => 0}
|
|
38
38
|
|
39
39
|
The first ractor to call `fetch_values` will have its block called twice; only the `fetch_values` has completed will the other Ractors have their calls to `fetch_values` run. The block is reentrant as it calls `[]=`; that call will not wait.
|
40
40
|
|
41
|
+
Moreover, exceptions are propagated transparently between Client and Server.
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
H.fetch_values(:z) { raise ArgumentError } # => ArgumentError
|
45
|
+
# (raised by Client, propagated to the Server, then back to the Client)
|
46
|
+
|
47
|
+
H.fetch_values(:z) # => KeyError
|
48
|
+
# (raised by Hash#fetch_values) on the Server, propagated to the Client)
|
49
|
+
```
|
50
|
+
|
51
|
+
Block can also `return`, `break` or `throw` on the client-side and the server will be aware of that
|
52
|
+
so that any `ensure` blocks are called and server doesn't hang expecting a return value.
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
class RactorHash
|
56
|
+
def fetch_values(*)
|
57
|
+
super
|
58
|
+
ensure
|
59
|
+
puts 'here'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def foo
|
64
|
+
H.fetch_values(:z) { return 42 }
|
65
|
+
end
|
66
|
+
foo # => 42, prints 'here'
|
67
|
+
```
|
68
|
+
|
41
69
|
The implementation relies on three layers of functionality.
|
42
70
|
|
43
71
|
### Low-level API: `Request`
|
@@ -103,6 +131,25 @@ The method `receive_request` will only receive a `Request` that was sent with `s
|
|
103
131
|
|
104
132
|
The method `Request#receive` will only receive a `Request` that is a direct response to the receiver.
|
105
133
|
|
134
|
+
#### Exceptions
|
135
|
+
|
136
|
+
Instead of responsing with data, it is possible to respond by raising (on the remote side) an error with `send_exception`.
|
137
|
+
|
138
|
+
Calling `send_exception` wraps the original exception in a `Ractor::Remote`:
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
ractor = Ractor.new do
|
142
|
+
request, data = receive_request
|
143
|
+
rescue ArgumentError => e
|
144
|
+
puts e # => 'example' (ArgumentError)
|
145
|
+
end
|
146
|
+
|
147
|
+
ractor.send_exception(ArgumentError.new('example'))
|
148
|
+
ractor.take
|
149
|
+
```
|
150
|
+
|
151
|
+
Calling `send_exception` again on the wrapped `Ractor::Remote` will unwrap it. This way, if an exception travels from the client to the server and back to the client, this voyage will be transparent to the client.
|
152
|
+
|
106
153
|
#### Implementation
|
107
154
|
|
108
155
|
`send_request` / `receive_request` use `Ractor#send` and `Ractor#receive_if` with the following layout:
|
@@ -120,7 +167,8 @@ One may specify the expected syncing for a `Request`:
|
|
120
167
|
* `:tell`: receiver may not reply ("do this, I'm assuming it will get done")
|
121
168
|
* `:ask`: receiver must reply exactly once with sync type `:conclude` ("do this, let me know when done, and don't me ask questions")
|
122
169
|
* `:conclude`: as with `:tell`, receiver may not reply. Must be in response of `ask` or `converse`
|
123
|
-
* `:converse`: receiver may reply has many times as desired (with sync type `:tell`, `:ask`, or `:converse`) and must then reply exactly once with sync type `:conclude`. ("do this, ask questions if need be, and let me know when done")
|
170
|
+
* `:converse`: receiver may reply has many times as desired (with sync type `:tell`, `:ask`, or `:converse`) and must then reply exactly once with sync type `:conclude`. ("do this, ask questions if need be, and let me know when done"). Exception: if the receiver replies with a `converse`, both `converse` requests may be interrupted with a response of sync `interrupt`; the receiver may no longer reply, not even for the final `:conclude`.
|
171
|
+
* `:interrupt`: acts as a kind of "double conclude". Not only should receiver not reply, but outer `conclude`.
|
124
172
|
|
125
173
|
The API uses `send_request`/`send` with a `sync:` named argument:
|
126
174
|
|
@@ -252,7 +300,7 @@ end
|
|
252
300
|
|
253
301
|
It may be necessary to customize the `Client` interface.
|
254
302
|
|
255
|
-
For example in the `SharedObject` example above, it may be more efficient if the
|
303
|
+
For example in the `SharedObject` example above, it may be more efficient if the held object is always shareable. This can be done by customizing the client:
|
256
304
|
|
257
305
|
```ruby
|
258
306
|
class SharedObject
|
@@ -295,7 +343,6 @@ end
|
|
295
343
|
|
296
344
|
## To do
|
297
345
|
|
298
|
-
* Exception rescuing and propagation
|
299
346
|
* API to pass block via makeshareable
|
300
347
|
* Monitoring
|
301
348
|
* Promise-style communication
|
data/lib/ractor/server/client.rb
CHANGED
@@ -165,18 +165,32 @@ class Ractor
|
|
165
165
|
loop do
|
166
166
|
response, result = rq.receive
|
167
167
|
case response.sync
|
168
|
-
in :converse
|
169
|
-
|
170
|
-
Ractor.make_shareable(block_result) if share_inputs?(method)
|
171
|
-
response.conclude block_result
|
172
|
-
in :conclude
|
173
|
-
return result
|
168
|
+
in :converse then handle_yield(method, response) { yield result }
|
169
|
+
in :conclude then return result
|
174
170
|
end
|
175
171
|
end
|
176
172
|
ensure
|
177
173
|
debug(:await) { "Finished waiting for #{rq}" }
|
178
174
|
end
|
179
175
|
|
176
|
+
private def handle_yield(method, response)
|
177
|
+
begin
|
178
|
+
status, result = with_requests_nested(response) do
|
179
|
+
[:ok, yield]
|
180
|
+
rescue Exception => e
|
181
|
+
[:exception, e]
|
182
|
+
end
|
183
|
+
ensure
|
184
|
+
response.interrupt unless status # throw/return/...
|
185
|
+
end
|
186
|
+
if status == :exception
|
187
|
+
response.send_exception(result)
|
188
|
+
else
|
189
|
+
Ractor.make_shareable(result) if share_inputs?(method)
|
190
|
+
response.conclude result
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
180
194
|
private def with_requests_nested(context)
|
181
195
|
store = Thread.current
|
182
196
|
prev = store[@nest_request_key]
|
@@ -5,6 +5,8 @@ using Ractor::Server::Talk
|
|
5
5
|
|
6
6
|
class Ractor
|
7
7
|
module Server
|
8
|
+
WRAP_IN_REMOTE_ERROR = false # unsure what's best
|
9
|
+
|
8
10
|
class Request
|
9
11
|
include Debugging
|
10
12
|
attr_reader :response_to, :initiating_ractor, :sync, :info
|
@@ -38,7 +40,7 @@ class Ractor
|
|
38
40
|
Request.send(initiating_ractor, *args, **options, response_to: self)
|
39
41
|
end
|
40
42
|
|
41
|
-
%i[tell ask converse conclude].each do |sync|
|
43
|
+
%i[tell ask converse conclude interrupt].each do |sync|
|
42
44
|
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
43
45
|
def #{sync}(*args, **options) # def tell(*args, **options)
|
44
46
|
send(*args, **options, sync: :#{sync}) # send(*args, **options, sync: :tell)
|
@@ -50,9 +52,25 @@ class Ractor
|
|
50
52
|
RUBY
|
51
53
|
end
|
52
54
|
|
55
|
+
class WrappedException
|
56
|
+
# Use Marshal to circumvent https://bugs.ruby-lang.org/issues/17577
|
57
|
+
def initialize(exception)
|
58
|
+
@exception = Marshal.dump(exception)
|
59
|
+
end
|
60
|
+
|
61
|
+
def exception
|
62
|
+
Marshal.load(@exception)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
private_constant :WrappedException
|
66
|
+
|
67
|
+
def send_exception(exception)
|
68
|
+
send(WrappedException.new(exception), sync: sync && :conclude)
|
69
|
+
end
|
70
|
+
|
53
71
|
def receive
|
54
72
|
enforce_sync_when_receiving!
|
55
|
-
Request.receive_if(&self)
|
73
|
+
unwrap(Request.receive_if(&self))
|
56
74
|
end
|
57
75
|
|
58
76
|
def inspect
|
@@ -103,7 +121,7 @@ class Ractor
|
|
103
121
|
request
|
104
122
|
end
|
105
123
|
|
106
|
-
%i[tell ask converse conclude].each do |sync|
|
124
|
+
%i[tell ask converse conclude interrupt].each do |sync|
|
107
125
|
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
108
126
|
def #{sync}(r, *args, **options) # def tell(r, *args, **options)
|
109
127
|
send(r, *args, **options, sync: :#{sync}) # send(r, *args, **options, sync: :tell)
|
@@ -112,15 +130,39 @@ class Ractor
|
|
112
130
|
end
|
113
131
|
end
|
114
132
|
|
133
|
+
private def unwrap(message)
|
134
|
+
_rq, arg = message
|
135
|
+
raise_exception(arg) if arg.is_a?(WrappedException)
|
136
|
+
|
137
|
+
message
|
138
|
+
end
|
139
|
+
|
140
|
+
private def raise_exception(exc)
|
141
|
+
raise exc unless WRAP_IN_REMOTE_ERROR
|
142
|
+
|
143
|
+
if exc.exception.is_a?(Ractor::RemoteError)
|
144
|
+
debug(:exception) { 'Received RemoteError, raising original cause' }
|
145
|
+
raise exc.exception.cause
|
146
|
+
else
|
147
|
+
debug(:exception) { 'Received exception, raising RemoveError' }
|
148
|
+
begin
|
149
|
+
raise exc.exception
|
150
|
+
rescue Exception
|
151
|
+
raise Ractor::RemoteError # => sets `cause` to exc.exception
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
115
156
|
# @api private
|
116
157
|
def enforce_sync_when_sending!
|
117
158
|
# Only dynamic checks are done here; static validity checked in constructor
|
118
159
|
case sync
|
119
|
-
when :conclude
|
160
|
+
when :conclude, :interrupt
|
120
161
|
registry = Request.pending_send_conclusion
|
121
162
|
raise Talk::Error, "Request #{response_to} already answered" unless registry[response_to]
|
122
163
|
|
123
164
|
registry[response_to] = false
|
165
|
+
Request.pending_receive_conclusion[response_to.response_to] = false if sync == :interrupt
|
124
166
|
when :ask, :converse
|
125
167
|
Request.pending_receive_conclusion[self] = true
|
126
168
|
end
|
@@ -130,8 +172,9 @@ class Ractor
|
|
130
172
|
def sync_after_receiving
|
131
173
|
# Only dynamic checks are done here; static validity checked in constructor
|
132
174
|
case sync
|
133
|
-
when :conclude
|
175
|
+
when :conclude, :interrupt
|
134
176
|
Request.pending_receive_conclusion[response_to] = false
|
177
|
+
Request.pending_send_conclusion[response_to.response_to] = false if sync == :interrupt
|
135
178
|
when :ask, :converse
|
136
179
|
Request.pending_send_conclusion[self] = true
|
137
180
|
end
|
@@ -140,7 +183,7 @@ class Ractor
|
|
140
183
|
# Receiver is request to receive a reply from
|
141
184
|
private def enforce_sync_when_receiving!
|
142
185
|
case sync
|
143
|
-
when :tell, :conclude
|
186
|
+
when :tell, :conclude, :interrupt
|
144
187
|
raise Talk::Error, "Can not receive from a Request for a `#{sync}` sync: #{self}"
|
145
188
|
when :ask, :converse
|
146
189
|
return :ok if Request.pending_receive_conclusion[self]
|
@@ -153,7 +196,7 @@ class Ractor
|
|
153
196
|
ractor.name || "##{ractor.to_s.match(/#(\d+) /)[1]}"
|
154
197
|
end
|
155
198
|
|
156
|
-
private def enforce_valid_sync!
|
199
|
+
private def enforce_valid_sync! # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
157
200
|
case [response_to&.sync, sync]
|
158
201
|
in [nil, nil]
|
159
202
|
:ok_unsynchronized
|
@@ -161,10 +204,15 @@ class Ractor
|
|
161
204
|
:ok_talk
|
162
205
|
in [:ask | :converse, :conclude]
|
163
206
|
:ok_concluding
|
164
|
-
in [:
|
207
|
+
in [:ask | :converse, :interrupt]
|
208
|
+
raise Talk::Error, 'Can only interrupt a 2-level conversation' unless response_to.response_to&.converse?
|
209
|
+
|
210
|
+
:ok_interrupting
|
211
|
+
in [:tell | :conclude | :interrupt => from, _]
|
165
212
|
raise Talk::Error, "Can not respond to a Request with `#{from.inspect}` sync"
|
166
213
|
in [:ask, _]
|
167
|
-
raise Talk::Error,
|
214
|
+
raise Talk::Error, 'Request with `ask` sync must be responded with a `conclude`' \
|
215
|
+
"or `interrupt` sync, got #{sync.inspect}"
|
168
216
|
in [_, nil]
|
169
217
|
raise Talk::Error, "Specify sync to respond to a Request with #{sync.inspect}"
|
170
218
|
else
|
data/lib/ractor/server/server.rb
CHANGED
@@ -17,26 +17,35 @@ class Ractor
|
|
17
17
|
:done
|
18
18
|
end
|
19
19
|
|
20
|
+
INTERRUPT = Object.new.freeze
|
21
|
+
private_constant :INTERRUPT
|
22
|
+
|
20
23
|
private def process(rq, method_name, args, options, block = nil)
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
24
|
+
catch(INTERRUPT) do
|
25
|
+
if rq.converse?
|
26
|
+
public_send(method_name, *args, **options) do |yield_arg|
|
27
|
+
yield_client(rq, yield_arg)
|
28
|
+
end
|
29
|
+
else
|
30
|
+
public_send(method_name, *args, **options, &block)
|
31
|
+
end => result
|
32
|
+
rescue Exception => e
|
33
|
+
rq.send_exception(e) unless rq.tell?
|
25
34
|
else
|
26
|
-
|
27
|
-
end
|
28
|
-
|
29
|
-
rq.conclude(result) unless rq.tell?
|
35
|
+
rq.conclude(result) unless rq.tell?
|
36
|
+
end
|
30
37
|
end
|
31
38
|
|
32
39
|
private def yield_client(rq, arg)
|
33
40
|
yield_request = rq.converse(arg)
|
34
41
|
loop do
|
35
42
|
rq, *data = yield_request.receive
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
43
|
+
case rq.sync
|
44
|
+
when :conclude then return data.first
|
45
|
+
when :interrupt then throw INTERRUPT
|
46
|
+
else # Reentrant request
|
47
|
+
process(rq, *data)
|
48
|
+
end
|
40
49
|
end
|
41
50
|
end
|
42
51
|
|
data/lib/ractor/server/talk.rb
CHANGED
@@ -1,27 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
# shareable_constant_value: literal
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
# See https://bugs.ruby-lang.org/issues/17374#note-8
|
7
|
-
def refine(what, export: false)
|
8
|
-
mod = super(what)
|
9
|
-
return mod unless export
|
10
|
-
|
11
|
-
export = self if export == true
|
12
|
-
export.class_eval do
|
13
|
-
mod.instance_methods(false).each do |method|
|
14
|
-
define_method(method, mod.instance_method(method))
|
15
|
-
end
|
16
|
-
mod.private_instance_methods(false).each do |method|
|
17
|
-
private define_method(method, mod.instance_method(method))
|
18
|
-
end
|
19
|
-
end
|
20
|
-
mod
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
using RefinementExporter
|
4
|
+
require 'refine_export'
|
5
|
+
using RefineExport
|
25
6
|
|
26
7
|
class Ractor
|
27
8
|
module Server
|
data/ractor-server.gemspec
CHANGED
@@ -28,6 +28,7 @@ Gem::Specification.new do |spec|
|
|
28
28
|
spec.require_paths = ['lib']
|
29
29
|
|
30
30
|
# Uncomment to register a new dependency of your gem
|
31
|
+
spec.add_dependency 'refine_export'
|
31
32
|
spec.add_dependency 'require_relative_dir', '>= 1.1.0'
|
32
33
|
|
33
34
|
# For more information and examples about making a new gem, checkout our
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ractor-server
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marc-Andre Lafortune
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-01-
|
11
|
+
date: 2021-01-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: refine_export
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: require_relative_dir
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|