ractor-server 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '029685fbbe91ed839a800417dfe53141a7bbce3fb5058575f4607a7956872ccc'
4
+ data.tar.gz: 71376351108500ba31e6161ed4229fc836bc90b49d1992c8e0d9cbe4a07a8678
5
+ SHA512:
6
+ metadata.gz: f728a4a7862ed48732aaf688480e0f5ae64af885a02175b34bbfdec6185a6eb972680ce5d279b24d3d78ef8e9ac26b5a67102f8b1183d227c55b08a55138b138
7
+ data.tar.gz: 8166754425d64a1747fb3f13b95e2a4a5d1a8c70e1dde578841f161e4aa645dbe1adfafb75a8dcfa63f4fb62b52f9e7dcf7f352597ecec3898818e65674ee1ab
@@ -0,0 +1,49 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ pull_request:
8
+
9
+ jobs:
10
+ tests:
11
+ name: >-
12
+ Specs | ${{ matrix.ruby }}
13
+ runs-on: ${{ matrix.os }}-latest
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ os: [ ubuntu ]
18
+ ruby: [ '3.0', head ]
19
+ steps:
20
+ - name: checkout
21
+ uses: actions/checkout@v2
22
+ - name: set up Ruby
23
+ uses: ruby/setup-ruby@v1
24
+ with:
25
+ ruby-version: ${{ matrix.ruby }}
26
+ - name: install dependencies
27
+ run: bundle install --jobs 3 --retry 3
28
+ - name: spec
29
+ run: bundle exec rake
30
+ internal_investigation:
31
+ name: >-
32
+ Coding Style
33
+ runs-on: ${{ matrix.os }}-latest
34
+ strategy:
35
+ fail-fast: false
36
+ matrix:
37
+ os: [ ubuntu ]
38
+ ruby: [ 2.7 ]
39
+ steps:
40
+ - name: checkout
41
+ uses: actions/checkout@v2
42
+ - name: set up Ruby
43
+ uses: ruby/setup-ruby@v1
44
+ with:
45
+ ruby-version: ${{ matrix.ruby }}
46
+ - name: install dependencies
47
+ run: bundle install --jobs 3 --retry 3
48
+ - name: internal investigation
49
+ run: bundle exec rubocop
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+ Gemfile.lock
13
+ .rubocop-https-*
data/.pryrc ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH << './lib'
4
+ require 'ractor/server'
5
+
6
+ # Pry.config.hooks.add_hook(:when_started, :set_context) do |binding, options, pry|
7
+ # if binding.eval('self').class == Object # true when starting `pry`
8
+ # # false when called from binding.pry
9
+ # pry.input = StringIO.new('cd Ractor::Server')
10
+ # end
11
+ # end
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,27 @@
1
+ inherit_from:
2
+ - https://raw.githubusercontent.com/ractor-tools/rubocop-ractor-tools/master/.rubocop.yml
3
+
4
+ AllCops:
5
+ TargetRubyVersion: 3.0
6
+
7
+ # Move:
8
+
9
+ Layout/EmptyLineAfterMagicComment:
10
+ Enabled: false # https://github.com/rubocop-hq/rubocop/issues/9327
11
+ Style/RescueModifier:
12
+ Exclude:
13
+ - 'spec/**/*.rb'
14
+
15
+ Style/MutableConstant:
16
+ Enabled: false # https://github.com/rubocop-hq/rubocop/issues/9328
17
+
18
+ Style/TrailingCommaInArguments:
19
+ EnforcedStyleForMultiline: consistent_comma
20
+
21
+ Naming/MethodParameterName:
22
+ AllowedNames:
23
+ - rq
24
+ # Local:
25
+
26
+ Metrics/MethodLength:
27
+ Max: 16
@@ -0,0 +1,84 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or
22
+ advances of any kind
23
+ * Trolling, insulting or derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information, such as a physical or email
26
+ address, without their explicit permission
27
+ * Other conduct which could reasonably be considered inappropriate in a
28
+ professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at github@marc-andre.ca. All complaints will be reviewed and investigated promptly and fairly.
43
+
44
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
+
46
+ ## Enforcement Guidelines
47
+
48
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
+
50
+ ### 1. Correction
51
+
52
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
+
54
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
+
56
+ ### 2. Warning
57
+
58
+ **Community Impact**: A violation through a single incident or series of actions.
59
+
60
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
+
62
+ ### 3. Temporary Ban
63
+
64
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
+
66
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
+
68
+ ### 4. Permanent Ban
69
+
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
+
72
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
73
+
74
+ ## Attribution
75
+
76
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
+ available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
+
81
+ [homepage]: https://www.contributor-covenant.org
82
+
83
+ For answers to common questions about this code of conduct, see the FAQ at
84
+ https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in ractor-server.gemspec
6
+ gemspec
7
+
8
+ gem 'rake'
9
+
10
+ gem 'rspec'
11
+ gem 'rspec-its'
12
+
13
+ gem 'rubocop'
14
+
15
+ gem 'pry-byebug'
16
+
17
+ gem 'backports', '~> 3.20'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Marc-Andre Lafortune
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,335 @@
1
+ # Ractor::Server
2
+
3
+ ## Usage
4
+
5
+ ### Intro
6
+
7
+ This gem streamlines communication to a Ractor:
8
+ * a "Server" that makes its methods available (think Elixir/Erlang's `GenServer`)
9
+ * a "Client" that is immutable (Ractor shareable) and can call a "Server" from any Ractor.
10
+
11
+ Any class can `include Ractor::Server` and this automatically creates an interface:
12
+
13
+ ```ruby
14
+ class RactorHash < Hash
15
+ include Ractor::Server
16
+ end
17
+
18
+ H = RactorHash.start
19
+
20
+ Ractor.new { H[:example] = 42 }.take
21
+ puts Ractor.new { H[:example] }.take # => 42
22
+ ```
23
+
24
+ Calls with blocks are atomic yet allow reentrant calls:
25
+
26
+ ```ruby
27
+ ractors = 3.times.map do |i|
28
+ Ractor.new(i) do |i|
29
+ H.fetch_values(:foo, :bar) do |val|
30
+ H[val] = i
31
+ end
32
+ end
33
+ end
34
+
35
+ puts H # => {:example => 42, :foo => 0, :bar => 0}
36
+ # (maybe 0 will be 1 or 2, but both will be same)
37
+ ```
38
+
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
+
41
+ The implementation relies on three layers of functionality.
42
+
43
+ ### Low-level API: `Request`
44
+
45
+ The first layer is the concept of a `Request` that uniquely identifies a message sent to a Ractor.
46
+
47
+ This enables a way to safely reply to a request:
48
+
49
+ ```ruby
50
+ using Ractor::Server::Talk
51
+
52
+ ractor = Ractor.new do
53
+ request, data = receive_request
54
+ puts data # => :example
55
+ request.send(:hello)
56
+ end
57
+
58
+ request = ractor.send_request(:example)
59
+ response_request, result = request.receive
60
+ puts result # => :hello
61
+ ```
62
+
63
+ #### `Request` is an envelope
64
+
65
+ The `Request` itself contains no data other than the initiating Ractor (`Request#initiating_ractor`) and if it is a reply to another `Request` (`Request#response_to`):
66
+
67
+ ```ruby
68
+ request.initiating_ractor == Ractor.current # => true
69
+ response_request.initiating_ractor == ractor # => true
70
+ request.response_to # => nil
71
+ response_request.response_to # => request
72
+ ```
73
+
74
+ Note that a `Request` is immutable and thus Ractor-shareable irrespective of the data that accompanies it.
75
+
76
+ #### Nesting `Request`s
77
+
78
+ One may reply to a `Request` any number of times; it is up to the requester to receive the proper amount of times.
79
+
80
+ A response to a `Request` is itself a `Request`; `Requests` may be nested as deeply as required:
81
+
82
+ ```ruby
83
+ # as above...
84
+ ractor = Ractor.new do
85
+ request, data = receive_request
86
+ puts data # => :example
87
+ request.send(:hello)
88
+ response_request = request.send(:world)
89
+ other_request, data = response_request.receive
90
+ puts data # => :inner
91
+ # ...
92
+ end
93
+
94
+ request = ractor.send_request(:example)
95
+ _request, result = request.receive
96
+ puts result # => :hello
97
+ response_request, result = request.receive
98
+ puts result # => :world
99
+ response_request.send(:inner)
100
+ ```
101
+
102
+ The method `receive_request` will only receive a `Request` that was sent with `send_request` and thus is not a response to another `Request`.
103
+
104
+ The method `Request#receive` will only receive a `Request` that is a direct response to the receiver.
105
+
106
+ #### Implementation
107
+
108
+ `send_request` / `receive_request` use `Ractor#send` and `Ractor#receive_if` with the following layout:
109
+
110
+ ```ruby
111
+ message = [Request, ...]
112
+ ```
113
+
114
+ To avoid interfering with `Request`, any other Ractor communication must use `receive_if` and filter out messages of that form (i.e. any array starting with an instance of `Request`).
115
+
116
+ ### Mid-level API: `Talk` using `sync:`
117
+
118
+ One may specify the expected syncing for a `Request`:
119
+
120
+ * `:tell`: receiver may not reply ("do this, I'm assuming it will get done")
121
+ * `:ask`: receiver must reply exactly once with sync type `:conclude` ("do this, let me know when done, and don't me ask questions")
122
+ * `: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")
124
+
125
+ The API uses `send_request`/`send` with a `sync:` named argument:
126
+
127
+ ```ruby
128
+ ractor = Ractor.new do
129
+ request, data = receive_request
130
+ puts data # => :example
131
+ request.send(:hello, sync: :tell)
132
+ response_request = request.send(:world, sync: :ask)
133
+ other_request, data = response_request.receive
134
+ puts data # => :inner
135
+ # ...
136
+ end
137
+
138
+ request = ractor.send_request(:example, sync: :converse)
139
+ response_request, result = request.receive
140
+ puts result # => :hello
141
+ puts response_request.sync # => :tell
142
+ response_request.send(:whatever, sync: :conclude) # => Error "can not reply to sync: say"
143
+ response_request, result = request.receive
144
+ puts result # => :world
145
+ request.receive # => Error, "request must be replied to"
146
+ response_request.send(:inner, sync: :conclude)
147
+ ```
148
+
149
+ This example achieves exactly the same as before, but with clear semantics and checking on the sequence of events.
150
+
151
+ Shortcuts exists:
152
+
153
+ ```ruby
154
+ ractor.send_request(..., sync: :tell) # or :ask, :conclude or :converse
155
+ # shortcuts:
156
+ ractor.tell(...) # or .ask(...), .conclude(...) or .converse(...)
157
+
158
+ # Similarly for `Request#send`:
159
+ request.send(..., sync: :tell)
160
+ # same as
161
+ request.tell(...)
162
+ # etc.
163
+ ```
164
+
165
+ ### High-level API: `Client` & `Server`
166
+
167
+ The `Client` and `Server` module make it easy to use the `sync:` API to allow a client to call methods on the server and for the server to yield back to the client.
168
+
169
+ The client makes a method call using either `:tell` or `:ask` and the data consists of the method name, arguments and keyword parameters.
170
+ The result is either the request (`:tell`) or the data received (`:ask`).
171
+
172
+ For method calls with blocks, the client uses `:converse`. The server may yield back to the client with a nested `:converse` response. From inside the block, the client can send nested calls to the server (simple or with block). The result of the block is returned to the server with `:conclude`. The server may then yield again, or if it is finished it `conclude`s the outer conversation.
173
+
174
+ To define a server, it suffices to define the methods that may be called normally and use `yield` if desired.
175
+
176
+ All public methods are assumed to be callable from a client with `:ask`, except setters that are assumed to be called with `:tell`.
177
+
178
+ Here is a complete example of how to define a `Server` that can hold a value:
179
+
180
+ ```ruby
181
+ class SharedObject < Ractor::Client
182
+ class Server
183
+ include Ractor::Server
184
+
185
+ attr_accessor :value
186
+
187
+ def initialize(value = nil)
188
+ @value = value
189
+ end
190
+
191
+ def update
192
+ @value = yield @value
193
+ end
194
+ end
195
+ end
196
+
197
+ LIST = ShareObject.new([1, 2])
198
+
199
+ Ractor.new do
200
+ LIST.value # => [1, 2]
201
+ LIST.value = [:changed]
202
+ end.take
203
+
204
+ LIST.value # => [:changed]
205
+ LIST.update do |cur|
206
+ cur << :extra
207
+ end
208
+ LIST.value # => [:changed, :extra]
209
+ ```
210
+
211
+ Note that `update` in the example above is atomic; if another Ractor calls `LIST.<anything>`, that request will wait until the `update` is completed. Nevertheless, calls issued from *inside* the `update` block will be processed synchroneously.
212
+
213
+ #### Defining classes
214
+
215
+ To create a `Server` class:
216
+
217
+ ```ruby
218
+ class MyServer
219
+ include Ractor::Server
220
+
221
+ # define your methods...
222
+ end
223
+ ```
224
+
225
+ This adds a few methods (`#main_loop`, `#receive_request` and `#process_request`)
226
+ as well as class methods `tells` (private).
227
+
228
+ This automatically defines a `Client` class and a `Client::ServerCallLayer` module; these may be subclassed/included if desired:
229
+
230
+ ```ruby
231
+ class MyClient < MyServer::Client
232
+
233
+ # special handling (if needed)
234
+ end
235
+ ```
236
+
237
+ Note that subclass defines a method `initialize(...)` that:
238
+ * starts the server
239
+ * make itself shareable
240
+
241
+ An equivalent way to declare a Client is:
242
+
243
+ ```ruby
244
+ class MyClient < Ractor::Client
245
+ include MyServer::Client::ServerCallLayer
246
+
247
+ # special handling (if needed)
248
+ end
249
+ ```
250
+
251
+ #### Customizing the client
252
+
253
+ It may be necessary to customize the `Client` interface.
254
+
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:
256
+
257
+ ```ruby
258
+ class SharedObject
259
+ # ... as above
260
+
261
+ class Client # refine the client interface:
262
+ def initialize(value = nil)
263
+ Ractor.make_shareable(value)
264
+ super
265
+ end
266
+
267
+ def update
268
+ super { Ractor.make_shareable(yield) }
269
+ end
270
+ end
271
+ end
272
+ ```
273
+
274
+ In this case, the `update` block above would raise a `FrozenError` and must be modified to a non-mutating form:
275
+
276
+ ```ruby
277
+ LIST.update do |cur|
278
+ cur + [:extra]
279
+ end
280
+ ```
281
+
282
+ #### Customizing the sync
283
+
284
+ If we wanted to add a method `clear` to our server, there is no real need for the client to wait for the response as the result will not be useful. To specify that the method should be called with `:tell` instead of `:ask`, one may call `tells :clear`, or use the fact that `def` returns the method it defined:
285
+
286
+ ```ruby
287
+ class SharedObject
288
+ # ...
289
+
290
+ tells def clear
291
+ @value = nil
292
+ end
293
+ end
294
+ ```
295
+
296
+ ## To do
297
+
298
+ * Exception rescuing and propagation
299
+ * API to pass block via makeshareable
300
+ * Monitoring
301
+ * Promise-style communication
302
+
303
+ ## Installation
304
+
305
+ Add this line to your application's Gemfile:
306
+
307
+ ```ruby
308
+ gem 'ractor-server'
309
+ ```
310
+
311
+ And then execute:
312
+
313
+ $ bundle install
314
+
315
+ Or install it yourself as:
316
+
317
+ $ gem install ractor-server
318
+
319
+ ## Development
320
+
321
+ 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.
322
+
323
+ 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).
324
+
325
+ ## Contributing
326
+
327
+ Bug reports and pull requests are welcome on GitHub at https://github.com/marcandre/ractor-server. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/marcandre/ractor-server/blob/master/CODE_OF_CONDUCT.md).
328
+
329
+ ## License
330
+
331
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
332
+
333
+ ## Code of Conduct
334
+
335
+ Everyone interacting in the Ractor::Server project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/marcandre/ractor-server/blob/master/CODE_OF_CONDUCT.md).
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: :spec
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'ractor/server'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+
4
+ require_relative 'server'
5
+
6
+ class Ractor
7
+ module Server
8
+ class Client
9
+ include Debugging
10
+ attr_reader :server
11
+
12
+ def initialize(server)
13
+ raise ArgumentError, "Expected a Ractor, got #{server.inspect}" unless server.is_a?(::Ractor)
14
+
15
+ @nest_request_key = :"Ractor::Server::Client#{object_id}"
16
+ @server = server
17
+ freeze
18
+ end
19
+
20
+ CONFIG = {
21
+ share_args: Set[].freeze,
22
+ tell_methods: Set[].freeze,
23
+ }
24
+
25
+ def inspect
26
+ "<##{self.class} server: #{call_server(:inspect)}>"
27
+ end
28
+
29
+ alias_method :to_s, :inspect
30
+
31
+ NOT_IMPLICITLY_DEFINED = (Object.instance_methods | Ractor::Server.instance_methods).freeze
32
+ private_constant :NOT_IMPLICITLY_DEFINED
33
+
34
+ class << self
35
+ include Debugging
36
+
37
+ def start(*args, **options)
38
+ ractor = self.class::Server.start_ractor(*args, **options)
39
+ new(ractor)
40
+ end
41
+
42
+ def refresh_server_call_layer
43
+ layer = self::ServerCallLayer
44
+ server_klass = self::Server
45
+ are_defined = layer.instance_methods
46
+ should_be_defined = server_klass.instance_methods - NOT_IMPLICITLY_DEFINED
47
+ (are_defined - should_be_defined).each { layer.remove_method _1 }
48
+ interface_with_server(*config(:tell_methods) | should_be_defined - are_defined)
49
+ end
50
+
51
+ def interface_with_server(*methods)
52
+ methods.flatten!(1)
53
+ self::ServerCallLayer.class_eval do
54
+ methods.each do |method|
55
+ public alias_method(method, :call_server_alias)
56
+ end
57
+ end
58
+ debug(:interface) { "Defined methods #{methods.join(', ')}" }
59
+
60
+ methods
61
+ end
62
+
63
+ def tells(*methods)
64
+ methods.flatten!(1)
65
+ config(:tell_methods) { |set| set + methods }
66
+ interface_with_server(*methods)
67
+ end
68
+
69
+ def config(key)
70
+ cur = self::CONFIG
71
+ cur_value = cur.fetch(key)
72
+ if block_given?
73
+ cur_value = yield cur_value
74
+ remove_const(:CONFIG) if const_defined?(:CONFIG, false)
75
+ const_set(:CONFIG, Ractor.make_shareable(cur.merge(key => cur_value)))
76
+ end
77
+ cur_value
78
+ end
79
+
80
+ def sync_kind(method, block_given)
81
+ case
82
+ when setter?(method) || config(:tell_methods).include?(method)
83
+ :tell
84
+ when block_given
85
+ :converse
86
+ else
87
+ :ask
88
+ end
89
+ end
90
+
91
+ def share_args(*methods)
92
+ methods.flatten!(1)
93
+ config(:share_args) { |val| val + methods }
94
+
95
+ methods
96
+ end
97
+
98
+ private def inherited(base)
99
+ mod = Module.new do
100
+ private def call_server_alias(*args, **options, &block)
101
+ call_server(__callee__, *args, **options, &block)
102
+ end
103
+ end
104
+
105
+ base.const_set(:ServerCallLayer, mod)
106
+ base.include mod
107
+
108
+ super
109
+ end
110
+
111
+ NON_SETTERS = Set[*%i[<= == === != >=]].freeze
112
+ private_constant :NON_SETTERS
113
+
114
+ private def setter?(method)
115
+ method.end_with?('=') && !NON_SETTERS.include?(method)
116
+ end
117
+ end
118
+
119
+ private def respond_to_missing?(method, priv = false)
120
+ !priv && implemented_by_server?(method) || super
121
+ end
122
+
123
+ private def method_missing(method, *args, **options, &block)
124
+ if implemented_by_server?(method)
125
+ refresh_server_call_layer
126
+ # sanity check
127
+ unless self.class::ServerCallLayer.method_defined?(method)
128
+ raise "`refresh_server_call_layer` failed for #{method}"
129
+ end
130
+
131
+ return __send__(method, *args, **options, &block)
132
+ end
133
+
134
+ super
135
+ end
136
+
137
+ private def implemented_by_server?(method)
138
+ self.class::Server.method_defined?(method)
139
+ end
140
+
141
+ private def refresh_server_call_layer
142
+ self.class.refresh_server_call_layer
143
+ end
144
+
145
+ # @returns [Request] if method should be called as `:tell`,
146
+ # otherwise returns the result of the concluded method call.
147
+ private def call_server(method, *args, **options, &block)
148
+ Ractor.make_shareable([args, options]) if share_inputs?(method)
149
+
150
+ info = format_call(method, *args, **options, &block) if $DEBUG
151
+ rq = Request.send(
152
+ @server, method, args, options,
153
+ response_to: Thread.current[@nest_request_key],
154
+ sync: self.class.sync_kind(method, !!block),
155
+ info: info,
156
+ )
157
+ return rq if rq.tell?
158
+
159
+ await_response(rq, method, &block)
160
+ end
161
+
162
+ private def await_response(rq, method)
163
+ debug(:await) { "Awaiting response to #{rq}" }
164
+
165
+ loop do
166
+ response, result = rq.receive
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
174
+ end
175
+ end
176
+ ensure
177
+ debug(:await) { "Finished waiting for #{rq}" }
178
+ end
179
+
180
+ private def with_requests_nested(context)
181
+ store = Thread.current
182
+ prev = store[@nest_request_key]
183
+ store[@nest_request_key] = context
184
+ yield
185
+ ensure
186
+ store[@nest_request_key] = prev
187
+ end
188
+
189
+ private def share_inputs?(method_name)
190
+ self.class.config(:share_args).include?(method_name)
191
+ end
192
+
193
+ private def format_call(method, *args, **options, &block)
194
+ args = args.map(&:inspect) + options.map { _1.map(&:inspect).join(': ') }
195
+ arg_list = "(#{args.join(', ')})" unless args.empty?
196
+ block_sig = ' {...}' if block
197
+ "#{method}#{arg_list}#{block_sig}"
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+
4
+ class Ractor
5
+ module Server
6
+ module Debugging
7
+ def debug(_kind)
8
+ puts yield if $DEBUG
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+
4
+ class Ractor
5
+ module Server
6
+ class Error < RuntimeError
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+
4
+ using Ractor::Server::Talk
5
+
6
+ class Ractor
7
+ module Server
8
+ class Request
9
+ include Debugging
10
+ attr_reader :response_to, :initiating_ractor, :sync, :info
11
+
12
+ def initialize(response_to: nil, sync: nil, info: nil)
13
+ @response_to = response_to
14
+ @initiating_ractor = Ractor.current
15
+ @sync = sync
16
+ @info = info # for display only
17
+ enforce_valid_sync!
18
+ Ractor.make_shareable(self)
19
+ end
20
+
21
+ # Match any request that is a response to the receiver (or an array message starting with such)
22
+ def ===(message)
23
+ request, = message
24
+
25
+ match = request.is_a?(Request) && self == request.response_to
26
+
27
+ debug(:receive) { "Request #{request.inspect} does not match #{self}" } unless match
28
+
29
+ match
30
+ end
31
+
32
+ def to_proc
33
+ method(:===).to_proc
34
+ end
35
+
36
+ # @return [Request]
37
+ def send(*args, **options)
38
+ Request.send(initiating_ractor, *args, **options, response_to: self)
39
+ end
40
+
41
+ %i[tell ask converse conclude].each do |sync|
42
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
43
+ def #{sync}(*args, **options) # def tell(*args, **options)
44
+ send(*args, **options, sync: :#{sync}) # send(*args, **options, sync: :tell)
45
+ end # end
46
+
47
+ def #{sync}? # def tell?
48
+ sync == :#{sync} # sync == :tell
49
+ end # end
50
+ RUBY
51
+ end
52
+
53
+ def receive
54
+ enforce_sync_when_receiving!
55
+ Request.receive_if(&self)
56
+ end
57
+
58
+ def inspect
59
+ [
60
+ '<Request',
61
+ info,
62
+ ("for: #{response_to}" if response_to),
63
+ ("sync: #{sync}" if sync),
64
+ "from: #{ractor_name(initiating_ractor)}>",
65
+ ].compact.join(' ')
66
+ end
67
+ alias_method :to_s, :inspect
68
+
69
+ def respond_to_ractor
70
+ response_to.initiating_ractor
71
+ end
72
+
73
+ class << self
74
+ include Debugging
75
+
76
+ def message(*args, **options)
77
+ request = new(**options)
78
+ [request, *args].freeze
79
+ end
80
+
81
+ def pending_send_conclusion
82
+ ::Ractor.current[:ractor_server_request_send_conclusion] ||= ::ObjectSpace::WeakMap.new
83
+ end
84
+
85
+ def pending_receive_conclusion
86
+ ::Ractor.current[:ractor_server_request_receive_conclusion] ||= ::ObjectSpace::WeakMap.new
87
+ end
88
+
89
+ def receive_if(&block)
90
+ message = ::Ractor.receive_if(&block)
91
+ rq, = message
92
+ rq.sync_after_receiving
93
+ debug(:receive) { "Received #{message}" }
94
+ message
95
+ end
96
+
97
+ def send(ractor, *arguments, move: false, **options)
98
+ message = Request.message(*arguments, **options)
99
+ request, = message
100
+ request.enforce_sync_when_sending!
101
+ debug(:send) { "Sending #{message}" }
102
+ ractor.send(message, move: move)
103
+ request
104
+ end
105
+
106
+ %i[tell ask converse conclude].each do |sync|
107
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
108
+ def #{sync}(r, *args, **options) # def tell(r, *args, **options)
109
+ send(r, *args, **options, sync: :#{sync}) # send(r, *args, **options, sync: :tell)
110
+ end # end
111
+ RUBY
112
+ end
113
+ end
114
+
115
+ # @api private
116
+ def enforce_sync_when_sending!
117
+ # Only dynamic checks are done here; static validity checked in constructor
118
+ case sync
119
+ when :conclude
120
+ registry = Request.pending_send_conclusion
121
+ raise Talk::Error, "Request #{response_to} already answered" unless registry[response_to]
122
+
123
+ registry[response_to] = false
124
+ when :ask, :converse
125
+ Request.pending_receive_conclusion[self] = true
126
+ end
127
+ end
128
+
129
+ # @api private
130
+ def sync_after_receiving
131
+ # Only dynamic checks are done here; static validity checked in constructor
132
+ case sync
133
+ when :conclude
134
+ Request.pending_receive_conclusion[response_to] = false
135
+ when :ask, :converse
136
+ Request.pending_send_conclusion[self] = true
137
+ end
138
+ end
139
+
140
+ # Receiver is request to receive a reply from
141
+ private def enforce_sync_when_receiving!
142
+ case sync
143
+ when :tell, :conclude
144
+ raise Talk::Error, "Can not receive from a Request for a `#{sync}` sync: #{self}"
145
+ when :ask, :converse
146
+ return :ok if Request.pending_receive_conclusion[self]
147
+
148
+ raise Talk::Error, "Can not receive as #{self} is already answered"
149
+ end
150
+ end
151
+
152
+ private def ractor_name(ractor)
153
+ ractor.name || "##{ractor.to_s.match(/#(\d+) /)[1]}"
154
+ end
155
+
156
+ private def enforce_valid_sync!
157
+ case [response_to&.sync, sync]
158
+ in [nil, nil]
159
+ :ok_unsynchronized
160
+ in [nil | :converse, :tell | :ask | :converse]
161
+ :ok_talk
162
+ in [:ask | :converse, :conclude]
163
+ :ok_concluding
164
+ in [:tell | :conclude => from, _]
165
+ raise Talk::Error, "Can not respond to a Request with `#{from.inspect}` sync"
166
+ in [:ask, _]
167
+ raise Talk::Error, "Request with `ask` sync must be responded with a `conclude` sync, got #{sync.inspect}"
168
+ in [_, nil]
169
+ raise Talk::Error, "Specify sync to respond to a Request with #{sync.inspect}"
170
+ else
171
+ raise ArgumentError, "Unrecognized sync: #{sync.inspect}"
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+
4
+ class Ractor
5
+ module Server
6
+ include Debugging
7
+ include Talk
8
+
9
+ private def main_loop
10
+ debug(:server) { "Running #{inspect}" }
11
+
12
+ loop do
13
+ process(*receive_request)
14
+ end
15
+
16
+ debug(:server) { "Terminated #{inspect}" }
17
+ :done
18
+ end
19
+
20
+ 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
25
+ else
26
+ public_send(method_name, *args, **options, &block)
27
+ end => result
28
+
29
+ rq.conclude(result) unless rq.tell?
30
+ end
31
+
32
+ private def yield_client(rq, arg)
33
+ yield_request = rq.converse(arg)
34
+ loop do
35
+ rq, *data = yield_request.receive
36
+ return data.first if rq.conclude?
37
+
38
+ # Reentrant request
39
+ process(rq, *data)
40
+ end
41
+ end
42
+
43
+ module ClassMethods
44
+ def tells(*methods)
45
+ self::Client.tells(*methods)
46
+ end
47
+
48
+ def share_args(*methods)
49
+ self::Client.share_args(*methods)
50
+ end
51
+
52
+ def start(*args, **options)
53
+ ractor = start_ractor(*args, **options)
54
+ self::Client.new(ractor)
55
+ end
56
+
57
+ # @returns [Ractor] running an instance of the Server
58
+ def start_ractor(*args, **options)
59
+ ::Ractor.new(self, args.freeze, options.freeze) do |klass, args, options|
60
+ server = klass.new(*args, **options)
61
+ server.__send__ :main_loop
62
+ end
63
+ end
64
+ end
65
+
66
+ class << self
67
+ private def included(base)
68
+ base.const_set(:Client, ::Class.new(Client) { const_set(:Server, base) })
69
+ base.extend ClassMethods
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
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
25
+
26
+ class Ractor
27
+ module Server
28
+ module Talk
29
+ class Error < Server::Error
30
+ end
31
+
32
+ class << self
33
+ def receive_request
34
+ Request.receive_if { |rq,| rq.is_a?(Request) }
35
+ end
36
+ end
37
+
38
+ refine ::Ractor, export: true do
39
+ include Debugging
40
+
41
+ # @return [Request]
42
+ private def receive_request
43
+ Talk.receive_request
44
+ end
45
+
46
+ # @return [Request]
47
+ def send_request(*arguments, **options)
48
+ Request.send(self, *arguments, **options)
49
+ end
50
+ end
51
+
52
+ refine ::Ractor.singleton_class do
53
+ def receive_request
54
+ Talk.receive_request
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+
4
+ class Ractor
5
+ module Server
6
+ VERSION = '0.1.1'
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+
4
+ require 'require_relative_dir'
5
+ require 'set'
6
+
7
+ using RequireRelativeDir
8
+ require_relative_dir first: %i[debugging error talk]
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/ractor/server/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'ractor-server'
7
+ spec.version = Ractor::Server::VERSION
8
+ spec.authors = ['Marc-Andre Lafortune']
9
+ spec.email = ['github@marc-andre.ca']
10
+
11
+ spec.summary = 'Ractor based communication inspired by GenServer.'
12
+ spec.description = 'Ractor based communication inspired by GenServer.'
13
+ spec.homepage = 'https://github.com/ractor-tools/ractor-server'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 3.0.0')
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/ractor-tools/ractor-server'
19
+ # spec.metadata["changelog_uri"] = "https://github.com/ractor-tools/ractor-server"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
25
+ end
26
+ spec.bindir = 'exe'
27
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ['lib']
29
+
30
+ # Uncomment to register a new dependency of your gem
31
+ spec.add_dependency 'require_relative_dir', '>= 1.1.0'
32
+
33
+ # For more information and examples about making a new gem, checkout our
34
+ # guide at: https://bundler.io/guides/creating_gem.html
35
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ractor-server
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Marc-Andre Lafortune
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-01-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: require_relative_dir
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.1.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.1.0
27
+ description: Ractor based communication inspired by GenServer.
28
+ email:
29
+ - github@marc-andre.ca
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".github/workflows/main.yml"
35
+ - ".gitignore"
36
+ - ".pryrc"
37
+ - ".rspec"
38
+ - ".rubocop.yml"
39
+ - CODE_OF_CONDUCT.md
40
+ - Gemfile
41
+ - LICENSE.txt
42
+ - README.md
43
+ - Rakefile
44
+ - bin/console
45
+ - bin/setup
46
+ - lib/ractor/server.rb
47
+ - lib/ractor/server/client.rb
48
+ - lib/ractor/server/debugging.rb
49
+ - lib/ractor/server/error.rb
50
+ - lib/ractor/server/request.rb
51
+ - lib/ractor/server/server.rb
52
+ - lib/ractor/server/talk.rb
53
+ - lib/ractor/server/version.rb
54
+ - ractor-server.gemspec
55
+ homepage: https://github.com/ractor-tools/ractor-server
56
+ licenses:
57
+ - MIT
58
+ metadata:
59
+ homepage_uri: https://github.com/ractor-tools/ractor-server
60
+ source_code_uri: https://github.com/ractor-tools/ractor-server
61
+ post_install_message:
62
+ rdoc_options: []
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: 3.0.0
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ requirements: []
76
+ rubygems_version: 3.2.3
77
+ signing_key:
78
+ specification_version: 4
79
+ summary: Ractor based communication inspired by GenServer.
80
+ test_files: []