ractor-server 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '029685fbbe91ed839a800417dfe53141a7bbce3fb5058575f4607a7956872ccc'
4
- data.tar.gz: 71376351108500ba31e6161ed4229fc836bc90b49d1992c8e0d9cbe4a07a8678
3
+ metadata.gz: e363bbd188b2a662459734263104b8e18a6fc31984a4ba76d258b8bc49c7ae07
4
+ data.tar.gz: daf7ade9149e2ee7ff3e39d71def5516566e5329751caef920d2ae0cd0e25fe1
5
5
  SHA512:
6
- metadata.gz: f728a4a7862ed48732aaf688480e0f5ae64af885a02175b34bbfdec6185a6eb972680ce5d279b24d3d78ef8e9ac26b5a67102f8b1183d227c55b08a55138b138
7
- data.tar.gz: 8166754425d64a1747fb3f13b95e2a4a5d1a8c70e1dde578841f161e4aa645dbe1adfafb75a8dcfa63f4fb62b52f9e7dcf7f352597ecec3898818e65674ee1ab
6
+ metadata.gz: 6395fd4a6db49def30c07f3031f6ffc3fb68b2110c51d14dc2c4d6bf0eea5b5bf16ba424207b39d469a45bcd30e8a4387dd841d81884840e5c53078445444061
7
+ data.tar.gz: 87ce7921d6973573f6a7b281a5e97234ceab96b50dc75388527e09e1380bb274b3d5a71b5de2409499cea8732188f978f7ad82646cdcd4a5b6bebdfdce89ed4f
@@ -35,7 +35,7 @@ jobs:
35
35
  fail-fast: false
36
36
  matrix:
37
37
  os: [ ubuntu ]
38
- ruby: [ 2.7 ]
38
+ ruby: [ '3.0' ]
39
39
  steps:
40
40
  - name: checkout
41
41
  uses: actions/checkout@v2
@@ -25,3 +25,8 @@ Naming/MethodParameterName:
25
25
 
26
26
  Metrics/MethodLength:
27
27
  Max: 16
28
+
29
+ Lint/RescueException:
30
+ Enabled: false
31
+ Security/MarshalLoad:
32
+ Enabled: false
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 with blocks are atomic yet allow reentrant 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 shared object is always shareable. This can be done by customizing the client:
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
@@ -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
- block_result = with_requests_nested(response) { yield(result) }
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 [:tell | :conclude => from, _]
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, "Request with `ask` sync must be responded with a `conclude` sync, got #{sync.inspect}"
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
@@ -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
- if rq.converse?
22
- public_send(method_name, *args, **options) do |yield_arg|
23
- yield_client(rq, yield_arg)
24
- end
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
- public_send(method_name, *args, **options, &block)
27
- end => result
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
- return data.first if rq.conclude?
37
-
38
- # Reentrant request
39
- process(rq, *data)
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
 
@@ -1,27 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  # shareable_constant_value: literal
3
3
 
4
- module RefinementExporter
5
- refine Module do
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
@@ -3,6 +3,6 @@
3
3
 
4
4
  class Ractor
5
5
  module Server
6
- VERSION = '0.1.1'
6
+ VERSION = '0.2.0'
7
7
  end
8
8
  end
@@ -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.1.1
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-22 00:00:00.000000000 Z
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