ractor-server 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/main.yml +49 -0
- data/.gitignore +13 -0
- data/.pryrc +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +27 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +335 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/ractor/server/client.rb +201 -0
- data/lib/ractor/server/debugging.rb +12 -0
- data/lib/ractor/server/error.rb +9 -0
- data/lib/ractor/server/request.rb +176 -0
- data/lib/ractor/server/server.rb +73 -0
- data/lib/ractor/server/talk.rb +59 -0
- data/lib/ractor/server/version.rb +8 -0
- data/lib/ractor/server.rb +8 -0
- data/ractor-server.gemspec +35 -0
- metadata +80 -0
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
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
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
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -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
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,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,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,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: []
|