jrpc 1.1.7 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +55 -0
- data/.gitignore +1 -0
- data/.rspec +1 -0
- data/.rubocop.yml +228 -0
- data/CHANGELOG.md +84 -0
- data/Gemfile +17 -0
- data/README.md +163 -13
- data/Rakefile +3 -1
- data/bin/console +15 -0
- data/bin/jrpc +111 -0
- data/bin/jrpc-shell +109 -0
- data/bin/setup +8 -0
- data/jrpc.gemspec +9 -8
- data/lib/jrpc/errors.rb +65 -0
- data/lib/jrpc/id_generator.rb +22 -0
- data/lib/jrpc/message.rb +78 -0
- data/lib/jrpc/shared_client/outbound_queue.rb +71 -0
- data/lib/jrpc/shared_client/registry.rb +46 -0
- data/lib/jrpc/shared_client/ticket.rb +84 -0
- data/lib/jrpc/shared_client/transport_loop.rb +290 -0
- data/lib/jrpc/shared_client.rb +194 -0
- data/lib/jrpc/simple_client.rb +89 -0
- data/lib/jrpc/transport/base.rb +60 -0
- data/lib/jrpc/transport/tcp.rb +243 -0
- data/lib/jrpc/transport.rb +12 -0
- data/lib/jrpc/version.rb +3 -1
- data/lib/jrpc.rb +15 -16
- metadata +35 -76
- data/.travis.yml +0 -4
- data/lib/jrpc/base_client.rb +0 -123
- data/lib/jrpc/error/client_error.rb +0 -5
- data/lib/jrpc/error/connection_error.rb +0 -11
- data/lib/jrpc/error/error.rb +0 -5
- data/lib/jrpc/error/internal_error.rb +0 -9
- data/lib/jrpc/error/internal_server_error.rb +0 -5
- data/lib/jrpc/error/invalid_params.rb +0 -9
- data/lib/jrpc/error/invalid_request.rb +0 -9
- data/lib/jrpc/error/method_not_found.rb +0 -9
- data/lib/jrpc/error/parse_error.rb +0 -9
- data/lib/jrpc/error/server_error.rb +0 -11
- data/lib/jrpc/error/unknown_error.rb +0 -5
- data/lib/jrpc/tcp_client.rb +0 -112
- data/lib/jrpc/transport/socket_base.rb +0 -88
- data/lib/jrpc/transport/socket_tcp.rb +0 -98
- data/lib/jrpc/utils.rb +0 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e9d76a4b0925e75829ba1a406594a0ef8f0c8045e38d6a06362c3eac2892affb
|
|
4
|
+
data.tar.gz: f6f28cf60f79bd9108f82299b8cac6ada99ae3a0a80bcc85baa3fa1b51c86560
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 365d4ab7d191d4853b48f9c05875afb11e4619bf4a8a733b66bf52f8dd79bcb702c4d91bdff561088bee4086dcda01af58c3ee4ce0e89d838c8490019c827aaa
|
|
7
|
+
data.tar.gz: cb74295dd00108aa5e2de6bf93584204e903a36aa523cb6ea44cc5f18bf787c3273969a8cb18d89efbed89db8757b65ad24a45ba5ba66cd49aa2518413f989b8
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- master
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
pull-requests: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
rspec:
|
|
15
|
+
name: RSpec (Ruby ${{ matrix.ruby }})
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
strategy:
|
|
18
|
+
fail-fast: false
|
|
19
|
+
matrix:
|
|
20
|
+
ruby:
|
|
21
|
+
- '3.3'
|
|
22
|
+
- '3.4'
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
|
|
26
|
+
- name: Set up Ruby
|
|
27
|
+
uses: ruby/setup-ruby@v1
|
|
28
|
+
with:
|
|
29
|
+
ruby-version: ${{ matrix.ruby }}
|
|
30
|
+
bundler-cache: true
|
|
31
|
+
|
|
32
|
+
- name: Run RuboCop
|
|
33
|
+
run: bundle exec rubocop --parallel
|
|
34
|
+
|
|
35
|
+
- name: Run tests
|
|
36
|
+
run: bundle exec rake spec
|
|
37
|
+
|
|
38
|
+
- name: Code Coverage Summary
|
|
39
|
+
if: matrix.ruby == '3.4'
|
|
40
|
+
uses: irongut/CodeCoverageSummary@v1.3.0
|
|
41
|
+
with:
|
|
42
|
+
filename: coverage/coverage.xml
|
|
43
|
+
format: markdown
|
|
44
|
+
output: both
|
|
45
|
+
|
|
46
|
+
- name: Write coverage to job summary
|
|
47
|
+
if: matrix.ruby == '3.4'
|
|
48
|
+
run: cat code-coverage-results.md >> "$GITHUB_STEP_SUMMARY"
|
|
49
|
+
|
|
50
|
+
- name: Add coverage comment to PR
|
|
51
|
+
if: matrix.ruby == '3.4' && github.event_name == 'pull_request'
|
|
52
|
+
uses: marocchino/sticky-pull-request-comment@v2
|
|
53
|
+
with:
|
|
54
|
+
recreate: true
|
|
55
|
+
path: code-coverage-results.md
|
data/.gitignore
CHANGED
data/.rspec
CHANGED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
plugins:
|
|
2
|
+
- rubocop-performance
|
|
3
|
+
- rubocop-rspec
|
|
4
|
+
- rubocop-rake
|
|
5
|
+
|
|
6
|
+
AllCops:
|
|
7
|
+
TargetRubyVersion: 3.3
|
|
8
|
+
DisplayStyleGuide: true
|
|
9
|
+
ExtraDetails: true
|
|
10
|
+
NewCops: enable
|
|
11
|
+
Exclude:
|
|
12
|
+
- 'vendor/**/*'
|
|
13
|
+
- 'tmp/**/*'
|
|
14
|
+
|
|
15
|
+
##################### Bundler ###############################
|
|
16
|
+
|
|
17
|
+
Bundler/OrderedGems:
|
|
18
|
+
Exclude:
|
|
19
|
+
- Gemfile
|
|
20
|
+
|
|
21
|
+
##################### Gemspec ###############################
|
|
22
|
+
|
|
23
|
+
Gemspec/RequireMFA:
|
|
24
|
+
Enabled: false
|
|
25
|
+
|
|
26
|
+
Gemspec/RequiredRubyVersion:
|
|
27
|
+
Enabled: false
|
|
28
|
+
|
|
29
|
+
##################### Lint ###############################
|
|
30
|
+
|
|
31
|
+
Lint/UnderscorePrefixedVariableName:
|
|
32
|
+
Enabled: false
|
|
33
|
+
|
|
34
|
+
Lint/AmbiguousOperatorPrecedence:
|
|
35
|
+
Enabled: false
|
|
36
|
+
|
|
37
|
+
Lint/MissingSuper:
|
|
38
|
+
Enabled: false
|
|
39
|
+
|
|
40
|
+
Lint/AmbiguousBlockAssociation:
|
|
41
|
+
Exclude:
|
|
42
|
+
- 'spec/**/*.rb'
|
|
43
|
+
|
|
44
|
+
Lint/UselessAccessModifier:
|
|
45
|
+
Enabled: true
|
|
46
|
+
MethodCreatingMethods:
|
|
47
|
+
- delegate
|
|
48
|
+
- attribute
|
|
49
|
+
- service_entity
|
|
50
|
+
- parameter
|
|
51
|
+
- parameters
|
|
52
|
+
ContextCreatingMethods:
|
|
53
|
+
- concerning
|
|
54
|
+
- included
|
|
55
|
+
|
|
56
|
+
##################### Layout #############################
|
|
57
|
+
|
|
58
|
+
Layout/LineLength:
|
|
59
|
+
Max: 311
|
|
60
|
+
|
|
61
|
+
##################### Performance #########################
|
|
62
|
+
|
|
63
|
+
Performance/CollectionLiteralInLoop:
|
|
64
|
+
Exclude:
|
|
65
|
+
- 'spec/**/*.rb'
|
|
66
|
+
|
|
67
|
+
##################### Style ###############################
|
|
68
|
+
|
|
69
|
+
Style/StringLiterals:
|
|
70
|
+
EnforcedStyle: single_quotes
|
|
71
|
+
|
|
72
|
+
Style/StringLiteralsInInterpolation:
|
|
73
|
+
EnforcedStyle: single_quotes
|
|
74
|
+
|
|
75
|
+
Style/KeywordParametersOrder:
|
|
76
|
+
Enabled: false
|
|
77
|
+
|
|
78
|
+
Style/IfUnlessModifier:
|
|
79
|
+
Enabled: false
|
|
80
|
+
|
|
81
|
+
Style/WhenThen:
|
|
82
|
+
Enabled: false
|
|
83
|
+
|
|
84
|
+
Style/SafeNavigation:
|
|
85
|
+
Enabled: false
|
|
86
|
+
|
|
87
|
+
Style/Documentation:
|
|
88
|
+
Enabled: false
|
|
89
|
+
|
|
90
|
+
Style/SymbolArray:
|
|
91
|
+
Enabled: false
|
|
92
|
+
|
|
93
|
+
Style/HashAsLastArrayItem:
|
|
94
|
+
Enabled: false
|
|
95
|
+
|
|
96
|
+
Style/ClassAndModuleChildren:
|
|
97
|
+
Enabled: false
|
|
98
|
+
|
|
99
|
+
Style/GuardClause:
|
|
100
|
+
Enabled: false
|
|
101
|
+
|
|
102
|
+
Style/Lambda:
|
|
103
|
+
Enabled: false
|
|
104
|
+
EnforcedStyle: literal
|
|
105
|
+
|
|
106
|
+
Style/WordArray:
|
|
107
|
+
Enabled: false
|
|
108
|
+
|
|
109
|
+
Style/BlockDelimiters:
|
|
110
|
+
Enabled: true
|
|
111
|
+
EnforcedStyle: braces_for_chaining
|
|
112
|
+
|
|
113
|
+
Style/InvertibleUnlessCondition:
|
|
114
|
+
Enabled: true
|
|
115
|
+
InverseMethods:
|
|
116
|
+
:!=: :==
|
|
117
|
+
:>: :<=
|
|
118
|
+
:<=: :>
|
|
119
|
+
:<: :>=
|
|
120
|
+
:>=: :<
|
|
121
|
+
:!~: :=~
|
|
122
|
+
:zero?: :nonzero?
|
|
123
|
+
:nonzero?: :zero?
|
|
124
|
+
:any?: :none?
|
|
125
|
+
:none?: :any?
|
|
126
|
+
:even?: :odd?
|
|
127
|
+
:odd?: :even?
|
|
128
|
+
:present?: :blank?
|
|
129
|
+
:blank?: :present?
|
|
130
|
+
|
|
131
|
+
##################### Metrics #############################
|
|
132
|
+
|
|
133
|
+
Metrics/PerceivedComplexity:
|
|
134
|
+
Max: 45
|
|
135
|
+
|
|
136
|
+
Metrics/ClassLength:
|
|
137
|
+
Max: 1060
|
|
138
|
+
|
|
139
|
+
Metrics/BlockNesting:
|
|
140
|
+
Max: 5
|
|
141
|
+
|
|
142
|
+
Metrics/BlockLength:
|
|
143
|
+
Enabled: false
|
|
144
|
+
|
|
145
|
+
Metrics/CyclomaticComplexity:
|
|
146
|
+
Max: 45
|
|
147
|
+
|
|
148
|
+
Metrics/MethodLength:
|
|
149
|
+
Enabled: false
|
|
150
|
+
|
|
151
|
+
Metrics/ModuleLength:
|
|
152
|
+
Enabled: false
|
|
153
|
+
|
|
154
|
+
Metrics/AbcSize:
|
|
155
|
+
Max: 241
|
|
156
|
+
|
|
157
|
+
Metrics/ParameterLists:
|
|
158
|
+
Max: 9
|
|
159
|
+
|
|
160
|
+
##################### Naming ##############################
|
|
161
|
+
|
|
162
|
+
Naming/MethodParameterName:
|
|
163
|
+
AllowedNames:
|
|
164
|
+
- "iv"
|
|
165
|
+
- "by"
|
|
166
|
+
- "to"
|
|
167
|
+
- "id"
|
|
168
|
+
- "io"
|
|
169
|
+
- "on"
|
|
170
|
+
|
|
171
|
+
Naming/VariableNumber:
|
|
172
|
+
Enabled: false
|
|
173
|
+
|
|
174
|
+
Naming/PredicatePrefix:
|
|
175
|
+
Enabled: false
|
|
176
|
+
|
|
177
|
+
Naming/PredicateMethod:
|
|
178
|
+
Enabled: false
|
|
179
|
+
|
|
180
|
+
##################### RSpec ###############################
|
|
181
|
+
|
|
182
|
+
RSpec/NamedSubject:
|
|
183
|
+
Enabled: false
|
|
184
|
+
|
|
185
|
+
RSpec/MultipleMemoizedHelpers:
|
|
186
|
+
Enabled: false
|
|
187
|
+
|
|
188
|
+
RSpec/ExampleLength:
|
|
189
|
+
Max: 141
|
|
190
|
+
|
|
191
|
+
RSpec/MultipleExpectations:
|
|
192
|
+
Max: 79
|
|
193
|
+
|
|
194
|
+
RSpec/DescribeClass:
|
|
195
|
+
Enabled: false
|
|
196
|
+
|
|
197
|
+
RSpec/MessageSpies:
|
|
198
|
+
Enabled: false
|
|
199
|
+
|
|
200
|
+
RSpec/SharedExamples:
|
|
201
|
+
Enabled: false
|
|
202
|
+
|
|
203
|
+
RSpec/NestedGroups:
|
|
204
|
+
Max: 18
|
|
205
|
+
|
|
206
|
+
RSpec/ContextWording:
|
|
207
|
+
Enabled: false
|
|
208
|
+
|
|
209
|
+
RSpec/ExampleWording:
|
|
210
|
+
Enabled: false
|
|
211
|
+
|
|
212
|
+
RSpec/ExpectChange:
|
|
213
|
+
Enabled: false
|
|
214
|
+
|
|
215
|
+
RSpec/AnyInstance:
|
|
216
|
+
Enabled: false
|
|
217
|
+
|
|
218
|
+
RSpec/SpecFilePathFormat:
|
|
219
|
+
Enabled: false
|
|
220
|
+
|
|
221
|
+
RSpec/IndexedLet:
|
|
222
|
+
Enabled: false
|
|
223
|
+
|
|
224
|
+
RSpec/ExpectInLet:
|
|
225
|
+
Enabled: true
|
|
226
|
+
|
|
227
|
+
RSpec/IncludeExamples:
|
|
228
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
### Unreleased
|
|
4
|
+
|
|
5
|
+
### 2.0.0
|
|
6
|
+
|
|
7
|
+
Full rewrite. JRPC 2.0 is not API-compatible with 1.x.
|
|
8
|
+
|
|
9
|
+
**New**
|
|
10
|
+
|
|
11
|
+
* `JRPC::SharedClient` — one shared instance, one connection, serving many caller
|
|
12
|
+
threads and/or fibers. Owns a dedicated transport thread that multiplexes
|
|
13
|
+
responses by id. Supports Puma threads, rage-rb/Falcon fibers, and mixed
|
|
14
|
+
thread/fiber callers. Fiber callers require a spec-compliant `Fiber.scheduler`.
|
|
15
|
+
(Internally drafted as `ThreadQueueClient`; never shipped under that name.)
|
|
16
|
+
* `JRPC::SimpleClient` — single-threaded client, the functional replacement for
|
|
17
|
+
the old `TcpClient`.
|
|
18
|
+
* `concurrent-ruby` (`~> 1.2`) added as a runtime dependency (backs the shared
|
|
19
|
+
client's result futures).
|
|
20
|
+
* `logger` added as an explicit runtime dependency (no longer guaranteed bundled
|
|
21
|
+
on Ruby 3.5+).
|
|
22
|
+
|
|
23
|
+
**Removed / breaking**
|
|
24
|
+
|
|
25
|
+
* `JRPC::TcpClient` removed — use `JRPC::SimpleClient`.
|
|
26
|
+
* `JRPC::BaseClient` removed, including the `BaseClient.connect` block helper.
|
|
27
|
+
* All top-level error constants moved under `JRPC::Errors::*`.
|
|
28
|
+
* `method_missing` magic removed — pass the full method name as a String or Symbol.
|
|
29
|
+
* `invoke_request` / `invoke_notification` removed.
|
|
30
|
+
* `perform_request` removed — use `request` and `notification`.
|
|
31
|
+
* `namespace:` option removed.
|
|
32
|
+
* Umbrella `timeout:` option removed — use `read_timeout` / `write_timeout` /
|
|
33
|
+
`connect_timeout` (`SimpleClient`), or `ttl:` (`SharedClient`).
|
|
34
|
+
* `close_after_sent:` renamed to `autoclose:`.
|
|
35
|
+
* `connect_retry_count` default changed from `10` to `0`.
|
|
36
|
+
* Constructors no longer connect eagerly — the first call connects.
|
|
37
|
+
* Malformed responses now raise `Errors::MalformedResponseError` (a `ServerError`),
|
|
38
|
+
not `ClientError`. In 1.x the missing-comma-terminator case raised `ClientError`.
|
|
39
|
+
* `SimpleClient` read/write/connect timeouts now raise `Errors::Timeout`, not
|
|
40
|
+
`ConnectionError`.
|
|
41
|
+
* `oj` runtime dependency dropped — JRPC uses stdlib `json`. For Oj speed,
|
|
42
|
+
`require 'oj'; Oj.mimic_JSON` yourself.
|
|
43
|
+
* `netstring` is no longer a dependency — framing is owned in-tree by the transport.
|
|
44
|
+
* `required_ruby_version` set to `>= 3.3` (the floor where the
|
|
45
|
+
`ConditionVariable` ↔ `Fiber.scheduler` cooperation that fiber callers depend on
|
|
46
|
+
is verified).
|
|
47
|
+
* `bin/jrpc` and `bin/jrpc-shell` rewritten on top of `SimpleClient`; flag/usage
|
|
48
|
+
changes (see `README.md` and `jrpc --help`).
|
|
49
|
+
|
|
50
|
+
### 1.1.8
|
|
51
|
+
* handling FIN signal for TCP socket [didww/jrpc#19](https://github.com/didww/jrpc/pull/19)
|
|
52
|
+
* add gem executables [didww/jrpc#19](https://github.com/didww/jrpc/pull/19)
|
|
53
|
+
|
|
54
|
+
### 1.1.7
|
|
55
|
+
* connect ot socket in nonblock mode
|
|
56
|
+
|
|
57
|
+
### 1.1.6
|
|
58
|
+
* update oj version to ~> 3.0
|
|
59
|
+
|
|
60
|
+
### 1.1.5
|
|
61
|
+
* update oj version to ~> 2.0
|
|
62
|
+
|
|
63
|
+
### 1.1.4
|
|
64
|
+
* handle EOF on read
|
|
65
|
+
* fix jrpc error require
|
|
66
|
+
* use JRPC::Error as base class for JRPC::Transport::SocketBase::Error
|
|
67
|
+
|
|
68
|
+
### 1.1.3
|
|
69
|
+
* close socket when clearing socket if it's not closed
|
|
70
|
+
|
|
71
|
+
### 1.1.2
|
|
72
|
+
* reset socket when broken pipe error appears
|
|
73
|
+
|
|
74
|
+
### 1.1.1
|
|
75
|
+
* fix rescuing error in TcpClient initializer
|
|
76
|
+
|
|
77
|
+
### 1.1.0
|
|
78
|
+
* use own socket wrapper
|
|
79
|
+
|
|
80
|
+
### 1.0.1
|
|
81
|
+
* Net::TCPClient#read method process data with buffer variable
|
|
82
|
+
|
|
83
|
+
### 1.0.0
|
|
84
|
+
* stable release
|
data/Gemfile
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
source 'https://rubygems.org'
|
|
2
4
|
|
|
3
5
|
# Specify your gem's dependencies in jrpc.gemspec
|
|
4
6
|
gemspec
|
|
7
|
+
|
|
8
|
+
gem 'bundler'
|
|
9
|
+
gem 'rake', '~> 13.0'
|
|
10
|
+
gem 'rspec', '~> 3.0'
|
|
11
|
+
|
|
12
|
+
# Provides a real Fiber.scheduler for the fiber-caller specs (SharedClient §9.8).
|
|
13
|
+
gem 'async', '~> 2.0'
|
|
14
|
+
|
|
15
|
+
gem 'rubocop', '~> 1.21'
|
|
16
|
+
gem 'rubocop-performance'
|
|
17
|
+
gem 'rubocop-rspec'
|
|
18
|
+
gem 'rubocop-rake', '~> 0.7.1'
|
|
19
|
+
|
|
20
|
+
gem 'simplecov', '~> 0.22', require: false
|
|
21
|
+
gem 'simplecov-cobertura', '~> 3.1', require: false
|
data/README.md
CHANGED
|
@@ -1,33 +1,183 @@
|
|
|
1
1
|
# JRPC
|
|
2
2
|
|
|
3
|
-
JSON
|
|
3
|
+
A JSON-RPC 2.0 client for Ruby, over TCP, with netstring framing.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
JRPC ships two clients with sharp, separate responsibilities:
|
|
6
|
+
|
|
7
|
+
| | `JRPC::SimpleClient` | `JRPC::SharedClient` |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| Concurrency | single thread/fiber only | shared across many threads **and/or** fibers |
|
|
10
|
+
| Connection | one socket, lazy connect | one shared socket, dedicated transport thread |
|
|
11
|
+
| Multiplexing | one in-flight call at a time | many in-flight calls, id-demuxed |
|
|
12
|
+
| Timeouts | per-call `read_timeout`/`write_timeout` | per-message `ttl` |
|
|
13
|
+
| Use it for | CLI tools, scripts, one-shot calls, per-thread pools | Rails+Puma, rage-rb, Falcon, any long-lived shared client |
|
|
14
|
+
|
|
15
|
+
Pick `SimpleClient` unless you need one client instance to serve concurrent callers. It is not thread-safe or fiber-safe; use one instance per thread/fiber (or a pool). Pick `SharedClient` when a single process-wide instance must serve many caller threads or fibers over a single connection.
|
|
6
16
|
|
|
7
|
-
|
|
17
|
+
## Installation
|
|
8
18
|
|
|
9
19
|
```ruby
|
|
10
20
|
gem 'jrpc'
|
|
11
21
|
```
|
|
12
22
|
|
|
13
|
-
|
|
23
|
+
```sh
|
|
24
|
+
$ bundle install
|
|
25
|
+
```
|
|
14
26
|
|
|
15
|
-
|
|
27
|
+
Requires **Ruby >= 3.3**. Fiber callers additionally require a spec-compliant `Fiber.scheduler` (e.g. [`async`](https://github.com/socketry/async)) on their thread — see [Fiber callers](#fiber-callers).
|
|
16
28
|
|
|
17
|
-
|
|
29
|
+
## SimpleClient
|
|
18
30
|
|
|
19
|
-
|
|
31
|
+
```ruby
|
|
32
|
+
client = JRPC::SimpleClient.new(
|
|
33
|
+
"127.0.0.1:1234",
|
|
34
|
+
connect_timeout: 60, # total wall-clock budget for connect, across retries (seconds)
|
|
35
|
+
read_timeout: 60,
|
|
36
|
+
write_timeout: 60,
|
|
37
|
+
connect_retry_count: 0, # retries after the first failed connect
|
|
38
|
+
autoclose: false, # close the socket after every call
|
|
39
|
+
id_prefix: nil, # random per instance if nil
|
|
40
|
+
logger: nil
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
result = client.request(:sum, [1, 2])
|
|
44
|
+
result = client.request(:sum, [1, 2], read_timeout: 10, write_timeout: 10)
|
|
45
|
+
client.notification(:log, { msg: "hi" })
|
|
46
|
+
client.notification(:log, { msg: "hi" }, write_timeout: 10)
|
|
47
|
+
|
|
48
|
+
client.close # terminal; the instance cannot be reused
|
|
49
|
+
client.closed? # => true
|
|
50
|
+
client.server # => "127.0.0.1:1234"
|
|
51
|
+
```
|
|
20
52
|
|
|
21
|
-
|
|
53
|
+
Behavior:
|
|
22
54
|
|
|
23
|
-
|
|
55
|
+
- The constructor does **not** open the connection. The first `request`/`notification` connects.
|
|
56
|
+
- `autoclose: true` closes the socket in an `ensure` after each call. The **client is still reusable** — the next call reconnects. `autoclose` controls the socket, not the client.
|
|
57
|
+
- `#close` is **terminal**. After it, `#closed?` is `true` and every call raises `ClientError("client closed")`. There is no reopen — make a new client.
|
|
58
|
+
- Not thread-safe, not fiber-safe.
|
|
24
59
|
|
|
25
|
-
##
|
|
60
|
+
## SharedClient
|
|
26
61
|
|
|
27
|
-
|
|
62
|
+
One instance, one connection, many concurrent callers. A dedicated transport thread owns the socket and demultiplexes responses by id.
|
|
28
63
|
|
|
64
|
+
```ruby
|
|
65
|
+
client = JRPC::SharedClient.new(
|
|
66
|
+
"127.0.0.1:1234",
|
|
67
|
+
connect_timeout: 60,
|
|
68
|
+
connect_retry_count: 0,
|
|
69
|
+
connect_retry_interval: 0.5,
|
|
70
|
+
write_timeout: 5, # MUST be < default_ttl (see below)
|
|
71
|
+
reap_timeout: nil, # nil = never close an idle connection
|
|
72
|
+
default_ttl: 30, # per-message lifetime, seconds
|
|
73
|
+
max_queue_size: 10_000, # bounded; pass nil for unbounded (opt-in OOM risk)
|
|
74
|
+
id_prefix: nil,
|
|
75
|
+
logger: nil
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
result = client.request(:sum, [1, 2])
|
|
79
|
+
result = client.request(:sum, [1, 2], ttl: 10)
|
|
80
|
+
|
|
81
|
+
client.notification(:log, { msg: "hi" })
|
|
82
|
+
client.notification(:log, { msg: "hi" }, ttl: 5)
|
|
83
|
+
client.notification(:metric, [1], fire_and_forget: true) # send errors/TTL expiry are logged, not raised
|
|
84
|
+
|
|
85
|
+
client.close # graceful shutdown, default timeout: 5 seconds
|
|
86
|
+
client.close(timeout: 10)
|
|
87
|
+
client.closed?
|
|
88
|
+
client.server
|
|
89
|
+
```
|
|
29
90
|
|
|
30
|
-
|
|
91
|
+
Behavior:
|
|
92
|
+
|
|
93
|
+
- **TTL, not per-call timeout.** Each message carries `expires_at = now + ttl`. The transport thread is the timer authority; the caller blocks until the message resolves, fails, or its TTL elapses. `ttl: nil` blocks forever (opt-in).
|
|
94
|
+
- **`write_timeout < default_ttl` is enforced.** While the transport thread is parked in a single `write_frame`, it cannot fire TTL deadlines for other messages, so `write_timeout` is the maximum TTL-firing lag. The constructor raises `ArgumentError` if `write_timeout >= default_ttl`.
|
|
95
|
+
- **`notification` blocks until sent by default** (send errors propagate). Pass `fire_and_forget: true` to return immediately; then send errors and TTL expiry are logged, not raised. `request` never accepts `fire_and_forget`.
|
|
96
|
+
- **Bounded queue.** When the outbound queue is at `max_queue_size`, enqueue raises `ClientError("queue full")`.
|
|
97
|
+
- **Connection drops** resolve every in-flight request with `ConnectionError`; the transport thread keeps running and reconnects on the next message.
|
|
98
|
+
- **Reaping.** With `reap_timeout` set, the connection closes after that many idle seconds (no in-flight messages, empty queue, no bytes received) and reopens on the next message.
|
|
99
|
+
- `#close` is graceful: it lets in-flight work drain up to `timeout`, then force-closes. It returns `true` on a clean join, `false` on a forced close. Idempotent.
|
|
100
|
+
- A crash in the transport thread is surfaced, not hidden: in-flight requests fail with `ConnectionError`, the client transitions to an unusable state, and every subsequent call raises `ClientError("client unusable: transport thread exited")`. The client does not auto-restart — instantiate a new one.
|
|
101
|
+
|
|
102
|
+
### Fiber callers
|
|
103
|
+
|
|
104
|
+
`SharedClient` is shareable across the full Ruby concurrency matrix:
|
|
105
|
+
|
|
106
|
+
| Deployment | Caller is a... |
|
|
107
|
+
|---|---|
|
|
108
|
+
| Rails + Puma | Thread |
|
|
109
|
+
| rage-rb | Fiber under a single-thread Async reactor |
|
|
110
|
+
| Rails + Falcon | Fiber under a multi-thread Async reactor |
|
|
111
|
+
| Mixed | Some threads, some fibers, one client instance |
|
|
112
|
+
|
|
113
|
+
A caller blocks in a scheduler-aware wait, so a fiber under `Async`/`Falcon`/`rage-rb` **yields to the reactor** instead of stalling its OS thread; other fibers keep running and the response is routed back to the right fiber. This requires:
|
|
114
|
+
|
|
115
|
+
- Ruby **>= 3.3** (where the `ConditionVariable` ↔ `Fiber.scheduler` cooperation is verified), and
|
|
116
|
+
- a spec-compliant `Fiber.scheduler` active on the caller's thread, with correct cross-thread `unblock` (Async and Polyphony qualify).
|
|
117
|
+
|
|
118
|
+
No scheduler library is a runtime dependency — callers bring their own. Plain (non-scheduler) fibers are unsupported: they would block the OS thread on every wait. Use `SimpleClient` for non-scheduler code.
|
|
119
|
+
|
|
120
|
+
## Errors
|
|
121
|
+
|
|
122
|
+
All errors live under `JRPC::Errors::*` and descend from `JRPC::Errors::Error`. The four public-facing classes are siblings (no inheritance between them), so rescue each by name or rescue `Errors::Error` to catch all:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
Errors::Error (RuntimeError)
|
|
126
|
+
├── Errors::ClientError # caller-side: bad args, bad URI, client closed, queue full
|
|
127
|
+
├── Errors::ConnectionError # cannot connect, or connection died (see Exception#cause)
|
|
128
|
+
├── Errors::Timeout # message TTL elapsed, or SimpleClient read/write/connect timeout
|
|
129
|
+
└── Errors::ServerError # peer returned an error, or the response was unusable
|
|
130
|
+
attr_reader :code # nil for malformed responses
|
|
131
|
+
├── Errors::ParseError # -32700
|
|
132
|
+
├── Errors::InvalidRequest # -32600
|
|
133
|
+
├── Errors::MethodNotFound # -32601
|
|
134
|
+
├── Errors::InvalidParams # -32602
|
|
135
|
+
├── Errors::InternalError # -32603
|
|
136
|
+
├── Errors::InternalServerError # -32099..-32000
|
|
137
|
+
├── Errors::UnknownError # any other code
|
|
138
|
+
└── Errors::MalformedResponseError # bad framing/JSON, id mismatch, wrong jsonrpc version
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`MalformedResponseError` is a `ServerError`, not a `ClientError`: a malformed response is the peer's fault.
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
begin
|
|
145
|
+
client.request(:do_thing, [1, 2])
|
|
146
|
+
rescue JRPC::Errors::ServerError => e
|
|
147
|
+
warn "rpc error #{e.code}: #{e.message}"
|
|
148
|
+
rescue JRPC::Errors::Timeout
|
|
149
|
+
warn "timed out"
|
|
150
|
+
rescue JRPC::Errors::ConnectionError => e
|
|
151
|
+
warn "connection: #{e.message} (cause: #{e.cause})"
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## JSON serialization
|
|
156
|
+
|
|
157
|
+
JRPC uses the stdlib `json`. To swap in [oj](https://github.com/ohler55/oj) for speed, monkey-patch it yourself before use:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
require 'oj'
|
|
161
|
+
Oj.mimic_JSON
|
|
162
|
+
```
|
|
31
163
|
|
|
32
|
-
|
|
164
|
+
## CLI tools
|
|
165
|
+
|
|
166
|
+
Two executables ship with the gem:
|
|
167
|
+
|
|
168
|
+
- `jrpc` — one-shot request/notification from the shell (`jrpc --help`).
|
|
169
|
+
- `jrpc-shell` — an interactive REPL (`connect`, `request`, `notification`, `disconnect`).
|
|
170
|
+
|
|
171
|
+
Both use `SimpleClient`.
|
|
172
|
+
|
|
173
|
+
## Upgrading from 1.x
|
|
174
|
+
|
|
175
|
+
2.0 is a full rewrite with many breaking changes (`JRPC::TcpClient`/`BaseClient` removed, error constants moved under `JRPC::Errors::*`, `method_missing`/`namespace:` dropped, no eager connect, and more). See the [CHANGELOG](CHANGELOG.md) for the complete list.
|
|
176
|
+
|
|
177
|
+
## Contributing
|
|
178
|
+
|
|
179
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/didww/jrpc.
|
|
180
|
+
|
|
181
|
+
## License
|
|
33
182
|
|
|
183
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
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 'jrpc'
|
|
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__)
|