ruvoy 0.1.0-aarch64-linux → 0.2.1-aarch64-linux

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 240c3296eb26fb27f1010ac292658b666b8d61e0e8e3b68468f07e9b4d0ca989
4
- data.tar.gz: 72249773a9def101672c13f389ac5e8935003c6a854dd3c8710fe0e9b580fc1c
3
+ metadata.gz: a585700c8fa8895e0202a89938ba64268f56cd01936857684f276b3904ec408a
4
+ data.tar.gz: aac2b3e316c6ed0ef47c0c205ce232cc6164e0102cbfd4a8ca0761e885faa72a
5
5
  SHA512:
6
- metadata.gz: 944d5a8a115b5019806e824e40d21c187e2000a97cc56ed821d995bc3edbc707bf1bc8968ba1050f9b143f961678fa3381b5da3a9fdc5b316d4d16ce6d1b8deb
7
- data.tar.gz: 974e24d42aa2c3d9c4fac85d876bde1bf9b71919ee19b679d9daf51d4107d589b31224e0912d360e833c722305c26ea2da5a39deb08ab28416f43f59afd432ad
6
+ metadata.gz: 1cd84c20274deae0cb6105f37d5872c590de9a888ade90715e84be42a72f3a4c4e270dd5a3a412621845fa195c7cd15fedf6237a7fd0c7153c3bbd1e10b85b42
7
+ data.tar.gz: a3feba17ee4f6253718e95d1902d5edad010899228b4783e97c3e344ee736360edb9ecddef811bfeecfb534a32d7b1e74b4cfb8050ad8d50209d67ba6362484d
data/README.md CHANGED
@@ -19,17 +19,18 @@ speaks Rack, and owned Rust messages bridge the two.
19
19
  ## Install
20
20
 
21
21
  ```console
22
- $ gem install ruvoy
22
+ $ gem install ruvoy ruvoy-envoy
23
23
  ```
24
24
 
25
- The gem carries the dynamic module and the Envoy binary, so there is nothing
26
- else to install and no version to match by hand.
25
+ The `ruvoy` gem carries the dynamic module. Envoy comes from the `ruvoy-envoy`
26
+ gem or the machine: ruvoy looks for `RUVOY_ENVOY`, then `ruvoy-envoy`, then
27
+ `envoy` on `PATH`.
27
28
 
28
29
  | | |
29
30
  |---|---|
30
31
  | Ruby | 4.0.x. The module links against the Ruby it was built for, so the gem is published per ABI. |
31
32
  | Platform | Linux `x86_64` and `aarch64`, matching the platforms Envoy publishes binaries for. |
32
- | Envoy | Bundled (1.39.1, dynamic module ABI 0.1.0). Not installed separately. |
33
+ | Envoy | 1.39 (dynamic module ABI 0.1.0), from the `ruvoy-envoy` gem or the machine. |
33
34
 
34
35
  ## Quickstart
35
36
 
@@ -54,7 +55,8 @@ Usage: ruvoy [options] [config.ru]
54
55
 
55
56
  To run it under an Envoy you configure yourself, `--print-config` prints the
56
57
  listener Ruvoy would have used; the module is `ruvoy_fiber` and its filter
57
- config is the path to the rackup.
58
+ config is the path to the rackup, or a JSON object naming it (see
59
+ [Envoy context and upstream calls](#envoy-context-and-upstream-calls)).
58
60
 
59
61
  ## Status
60
62
 
@@ -83,6 +85,9 @@ threads flat. Interfaces may change.
83
85
  reaches Ruby.
84
86
  - **No upstream HTTP hop** — Rack calls do not require a loopback connection to
85
87
  a separate Ruby application server.
88
+ - **Envoy context and upstream calls** — optionally, the application sees the
89
+ route, connection, TLS and metadata Envoy has for the request, and calls
90
+ Envoy's clusters from inside it.
86
91
 
87
92
  ## With and without Ruvoy
88
93
 
@@ -130,13 +135,172 @@ stops the enumeration and closes the body.
130
135
  Request bodies stream as well: the application is called once the headers
131
136
  arrive and reads through `rack.input` as the body lands, so uploading and
132
137
  processing overlap. What the application has not read yet waits in Envoy, which
133
- stops reading from the client until there is room again. Raw socket hijacking
138
+ stops reading from the client until there is room again. What it has read is
139
+ kept, in memory up to 128 KiB and in a temporary file beyond that, so
140
+ `rack.input` can be rewound and read again, as frameworks written for earlier
141
+ Rack versions expect. Raw socket hijacking
134
142
  (`rack.hijack`) is not supported; the connection always belongs to Envoy.
135
143
 
136
- A response body that fails after its headers are already on the wire can only
137
- stop: a dynamic module cannot reset a stream it has started answering, so the
138
- client receives a short body rather than an error. Applications that can fail
139
- partway through should send their own length or checksum.
144
+ A response body that fails after its headers are already on the wire resets
145
+ the stream, so the client sees an error rather than a short body that looks
146
+ complete.
147
+
148
+ ## Blocking calls
149
+
150
+ A fiber waits without holding the runtime thread only when the call it waits
151
+ on cooperates with Ruby's fiber scheduler: Ruby's own sockets and IO, `sleep`,
152
+ and the asynchronous APIs many drivers offer do. A C extension call that
153
+ blocks in the kernel instead holds the one thread every request shares, so
154
+ requests queue behind it: fifty-millisecond calls of that kind serve about
155
+ twenty requests a second however many clients wait.
156
+
157
+ `Ruvoy.offload` runs such a call on its own Ruby thread while the calling
158
+ fiber waits:
159
+
160
+ ```ruby
161
+ rows = Ruvoy.offload { connection.sync_exec(sql) }
162
+
163
+ # At most 16 in flight; callers beyond that wait their turn.
164
+ DATABASE = Ruvoy::Offload.new(limit: 16)
165
+ rows = DATABASE.call { connection.sync_exec(sql) }
166
+ ```
167
+
168
+ - It helps extensions that release the GVL while they block, which is how
169
+ blocking C calls are normally written. One that holds the GVL stalls every
170
+ thread, and nothing on the Ruby side helps.
171
+ - A call cannot be cut short. When the caller is interrupted, because its
172
+ client went away or a timeout fired, it still waits for the call to finish
173
+ before it unwinds, so a connection the call is using is not handed back
174
+ while in use.
175
+ - The block runs on another thread, so thread-local state, such as a
176
+ per-thread connection, belongs to that thread rather than to the caller.
177
+ - Prefer a driver's scheduler-aware API where it has one. `offload` costs a
178
+ thread per call and bounds nothing unless given a limit.
179
+
180
+ ## Rails
181
+
182
+ Rails applications run unchanged, with one setting every fiber server needs:
183
+
184
+ ```ruby
185
+ config.active_support.isolation_level = :fiber
186
+ ```
187
+
188
+ Active Record hands out connections per thread by default, and every
189
+ request's fiber runs on the one runtime thread, so without it concurrent
190
+ requests share a single connection. With it, each request in flight holds a
191
+ connection of its own.
192
+
193
+ - Action Cable is not supported: it needs `rack.hijack`.
194
+ - Requests reach Rails through Envoy, which adds `X-Forwarded-Proto` to what
195
+ it passes on.
196
+ - Envoy serves static files ahead of the application, so
197
+ `config.public_file_server.enabled = false` spares Rails a filesystem lookup
198
+ on every request: about a fifth more throughput on small responses here.
199
+ - `test/e2e/rails_app_test.rb` runs a production Rails 8.1 application on
200
+ Ruvoy and on a reference server, compares every answer, and checks
201
+ connections under concurrency and when a client leaves mid-transaction.
202
+
203
+ ## Envoy context and upstream calls
204
+
205
+ Two optional capabilities give the application more of Envoy than a Rack
206
+ request carries. Both stay off unless the filter configuration asks for them,
207
+ and both are available on the fiber runtime only. The configuration is then a
208
+ JSON object:
209
+
210
+ ```json
211
+ {
212
+ "rackup": "/srv/app/config.ru",
213
+ "context": { "metadata_namespaces": ["acme.tenant"] },
214
+ "upstream": { "clusters": ["users"], "timeout_ms": 15000, "max_response_bytes": 1048576 }
215
+ }
216
+ ```
217
+
218
+ ### `ruvoy.context`
219
+
220
+ Envoy's view of the request, copied on the worker when the headers arrive.
221
+ Ruby values are only built when the application reads them.
222
+
223
+ ```ruby
224
+ context = env["ruvoy.context"]
225
+ context.route_name # => "api", or nil for an unnamed route
226
+ context.connection # => {"id"=>7, "source_address"=>"10.0.0.7", "source_port"=>51234, ...}
227
+ context.tls # => nil on plaintext, else {"version"=>"TLSv1.3", "server_name"=>..., "peer_certificate"=>...}
228
+ context.dynamic_metadata # => {"acme.tenant"=>{"id"=>"t-42"}}, from filters ahead of Ruvoy
229
+ context.route_metadata # => the same namespaces, from the matched route
230
+ context.to_h # => all of the above
231
+ ```
232
+
233
+ What Envoy's module interface can report sets some limits:
234
+
235
+ - A route has a name only if the route configuration gives it one. A terminal
236
+ filter still needs a route entry to be named: give it
237
+ `non_forwarding_action: {}`.
238
+ - `server_name` is the SNI the client sent, which Envoy records only when the
239
+ listener has the TLS inspector listener filter.
240
+ - `peer_certificate` means the client presented a certificate. It identifies
241
+ the client only on a listener that requires client certificates signed by a
242
+ trusted CA.
243
+ - Only the first URI and the first DNS subject alternative name are reported.
244
+ - Metadata values are strings, numbers (always Float), booleans and lists of
245
+ those. Nested structures are left out.
246
+
247
+ ### `ruvoy.upstream`
248
+
249
+ Calls a cluster Envoy was configured with and returns a Rack-style triple:
250
+
251
+ ```ruby
252
+ status, headers, body = env["ruvoy.upstream"].call(
253
+ "users", "GET", "/users/42",
254
+ headers: { "accept" => "application/json" }, timeout: 2
255
+ )
256
+ ```
257
+
258
+ The request's worker sends the call through the cluster, so the cluster's
259
+ connection pool, TLS settings and circuit breakers apply, and headers such as
260
+ `x-envoy-retry-on` ask Envoy to retry. The calling fiber waits without holding
261
+ the runtime thread, so other requests keep being served and calls made from
262
+ one request overlap. A repeated response header arrives as an Array.
263
+
264
+ - Only the clusters in `upstream.clusters` can be called; any other raises
265
+ `ArgumentError`.
266
+ - Answers Envoy produces while the call is under way arrive as ordinary
267
+ responses: 504 when it timed out, 503 when the upstream could not be reached
268
+ or hung up.
269
+ - `Ruvoy::UpstreamError` is raised when the call could not be sent: the cluster
270
+ does not exist, or turned the call away at once because it has no healthy
271
+ host or a circuit breaker is open. It is also raised when the request finished
272
+ before the call did.
273
+ - A response body over `max_response_bytes`, or over Envoy's own buffer limit,
274
+ raises `Ruvoy::UpstreamResponseTooLarge`, a subclass of `Ruvoy::UpstreamError`,
275
+ so a caller can tell asking for less apart from trying again later.
276
+ - `call` buffers the response whole, so `max_response_bytes` bounds what one
277
+ call can hold in memory.
278
+
279
+ `stream` takes the same arguments and returns once the response head is in.
280
+ The body yields the response as it arrives and can be returned as the Rack
281
+ response, so a proxy is one line:
282
+
283
+ ```ruby
284
+ run ->(env) { env["ruvoy.upstream"].stream("api", env["REQUEST_METHOD"], env["PATH_INFO"], body: env["rack.input"]) }
285
+ ```
286
+
287
+ - The request body may be a String or anything that responds to `read`, such
288
+ as `rack.input`. One that ends within its first 64 KiB goes with its length,
289
+ an empty one as none, and a longer one chunked while the response comes down.
290
+ - `idle_timeout:` bounds the wait for the head and between chunks, `timeout_ms`
291
+ by default, and raises `Ruvoy::UpstreamTimeout`. `timeout:` bounds the whole
292
+ exchange and is unset by default: Envoy answers 504 before the head, and
293
+ resets the stream after it.
294
+ - Envoy applies no flow control to a stream a module starts, so at most
295
+ `upstream.stream_buffer_bytes` (default 1 MiB) of the response waits for a
296
+ slow reader. Past that the stream is reset and the reader gets
297
+ `Ruvoy::UpstreamOverflow`. The request body is held to the same budget until
298
+ Envoy takes it; what Envoy buffers after that is not bounded by Ruvoy.
299
+ - Closing the body before its end, or finishing the request, resets the
300
+ stream. `body.trailers` holds the upstream's trailers once the body has ended.
301
+
302
+ [`examples/extensions`](examples/extensions) runs both against a stand-in
303
+ service, including two calls from one request overlapping.
140
304
 
141
305
  ## Scaling
142
306
 
@@ -258,7 +422,7 @@ Four honest qualifications:
258
422
  Not measured: CPU-bound Rack, real database or HTTP-client drivers, and
259
423
  multi-process deployment.
260
424
 
261
- Measurements were produced by `scripts/run-benchmark.sh`, which enforces the
425
+ Measurements were produced by `bench/run.rb`, which enforces the
262
426
  protocol above — remote load generation, rotation, warm-up, per-round
263
427
  validation, and the pre-registered judgement — and writes raw oha output,
264
428
  CPU and RSS samples, and the generated summary for every run.
Binary file
data/lib/ruvoy/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruvoy
4
- # Kept in step with the Cargo workspace version by scripts/test-cli.sh: the
4
+ # Kept in step with the Cargo workspace version by test/e2e/cli_test.rb: the
5
5
  # gem and the module it carries are the same release.
6
- VERSION = "0.1.0"
6
+ VERSION = "0.2.1"
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruvoy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Zixin Zhou