ruvoy 0.2.1-aarch64-linux → 0.4.0-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: a585700c8fa8895e0202a89938ba64268f56cd01936857684f276b3904ec408a
4
- data.tar.gz: aac2b3e316c6ed0ef47c0c205ce232cc6164e0102cbfd4a8ca0761e885faa72a
3
+ metadata.gz: 569b932c2661eaffd11fde7921044f4eadeb6e79dc4c951ed293c664d7b778bc
4
+ data.tar.gz: bcbe0a8668f8d396d9b64779077c23d914a7de5678bd56edc3e197bcf9012f0d
5
5
  SHA512:
6
- metadata.gz: 1cd84c20274deae0cb6105f37d5872c590de9a888ade90715e84be42a72f3a4c4e270dd5a3a412621845fa195c7cd15fedf6237a7fd0c7153c3bbd1e10b85b42
7
- data.tar.gz: a3feba17ee4f6253718e95d1902d5edad010899228b4783e97c3e344ee736360edb9ecddef811bfeecfb534a32d7b1e74b4cfb8050ad8d50209d67ba6362484d
6
+ metadata.gz: faabfaa8fdfe5dcc4512613a340514ad68856535b78e419b9451a5afaa0fecd6b76e4e6f0b1e0e9712620c3f7e6c255eb5e068ffbc79b597f7ef2e8181534ad5
7
+ data.tar.gz: 9f3695b1f964180948a46529a3168942856e545486762f90b55361c64d1fb42e07172c613833a61cf64aa53150531988bb8ea67e8a3c2bc7c66a8c5d559eb75e
data/README.md CHANGED
@@ -53,17 +53,20 @@ Usage: ruvoy [options] [config.ru]
53
53
  --print-config Print the generated Envoy configuration and exit
54
54
  ```
55
55
 
56
- To run it under an Envoy you configure yourself, `--print-config` prints the
57
- listener Ruvoy would have used; the module is `ruvoy_fiber` and its filter
58
- config is the path to the rackup, or a JSON object naming it (see
56
+ To run it under an Envoy you configure yourself, `--print-config` prints what
57
+ Ruvoy would have used: the `ruvoy` bootstrap extension, which names the rackup
58
+ (see [Lifecycle](#lifecycle)), and a listener whose rack filter joins it. The
59
+ filter's own config is an optional JSON object (see
59
60
  [Envoy context and upstream calls](#envoy-context-and-upstream-calls)).
60
61
 
61
62
  ## Status
62
63
 
63
64
  The data path is implemented and covered by suites that run against a real
64
65
  Envoy in CI. Configuration reload has been driven through a running Envoy, and
65
- a one-hour soak of 1.8 million requests left the heap, file descriptors and
66
- threads flat. Interfaces may change.
66
+ a three-hour soak of 5.7 million requests, 22,800 WebSocket sessions and 359
67
+ listener updates left live memory, file descriptors, threads and p99 latency
68
+ flat; the resident set still rose about 3% an hour, held by the allocator and
69
+ Ruby's heap rather than by anything live. Interfaces may change.
67
70
 
68
71
  ## Features
69
72
 
@@ -141,6 +144,10 @@ kept, in memory up to 128 KiB and in a temporary file beyond that, so
141
144
  Rack versions expect. Raw socket hijacking
142
145
  (`rack.hijack`) is not supported; the connection always belongs to Envoy.
143
146
 
147
+ WebSocket and other upgrades use Rack 3's `rack.protocol` with a streaming
148
+ body instead, over HTTP/1.1 and HTTP/2, once Envoy allows them
149
+ (`upgrade_configs`, plus `allow_connect` for HTTP/2).
150
+
144
151
  A response body that fails after its headers are already on the wire resets
145
152
  the stream, so the client sees an error rather than a short body that looks
146
153
  complete.
@@ -209,7 +216,6 @@ JSON object:
209
216
 
210
217
  ```json
211
218
  {
212
- "rackup": "/srv/app/config.ru",
213
219
  "context": { "metadata_namespaces": ["acme.tenant"] },
214
220
  "upstream": { "clusters": ["users"], "timeout_ms": 15000, "max_response_bytes": 1048576 }
215
221
  }
@@ -246,7 +252,8 @@ What Envoy's module interface can report sets some limits:
246
252
 
247
253
  ### `ruvoy.upstream`
248
254
 
249
- Calls a cluster Envoy was configured with and returns a Rack-style triple:
255
+ Calls a cluster Envoy was configured with and returns the status, the headers
256
+ and the whole body as a String:
250
257
 
251
258
  ```ruby
252
259
  status, headers, body = env["ruvoy.upstream"].call(
@@ -276,32 +283,85 @@ one request overlap. A repeated response header arrives as an Array.
276
283
  - `call` buffers the response whole, so `max_response_bytes` bounds what one
277
284
  call can hold in memory.
278
285
 
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:
286
+ `stream` takes the same arguments but returns once the head is in, with a
287
+ body that streams and can be returned as the Rack response, so a proxy is one
288
+ line:
282
289
 
283
290
  ```ruby
284
291
  run ->(env) { env["ruvoy.upstream"].stream("api", env["REQUEST_METHOD"], env["PATH_INFO"], body: env["rack.input"]) }
285
292
  ```
286
293
 
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.
294
+ Envoy applies no flow control to it, so a reader that falls more than
295
+ `upstream.stream_buffer_bytes` (1 MiB by default) behind has the stream reset.
296
+ With `protocol: "websocket"`, the response upgrades the client too and relays
297
+ both ways.
298
+
299
+ Calls and streams carry the request's trace context on: `traceparent`,
300
+ `tracestate`, `x-request-id` and the B3 headers, unless the call sets them.
301
301
 
302
302
  [`examples/extensions`](examples/extensions) runs both against a stand-in
303
303
  service, including two calls from one request overlapping.
304
304
 
305
+ ## Lifecycle
306
+
307
+ The `ruvoy` bootstrap extension owns the Ruby runtime: it names the rackup and
308
+ the terms the runtime serves it on, and every listener's rack filter joins it.
309
+ A rack filter configured without it is refused.
310
+
311
+ ```yaml
312
+ bootstrap_extensions:
313
+ - name: ruvoy
314
+ typed_config:
315
+ "@type": type.googleapis.com/envoy.extensions.bootstrap.dynamic_modules.v3.DynamicModuleBootstrapExtension
316
+ dynamic_module_config: { name: ruvoy_fiber, do_not_close: true }
317
+ extension_name: ruvoy
318
+ extension_config:
319
+ "@type": type.googleapis.com/google.protobuf.StringValue
320
+ value: '{"rackup": "/srv/app/config.ru"}'
321
+ ```
322
+
323
+ | Setting | Default | |
324
+ |---|---|---|
325
+ | `rackup` | | The application. Required. |
326
+ | `max_inflight_requests` | 1024 | Requests in Ruby at once; one more is refused with 503. |
327
+ | `shutdown_timeout_ms` | 10000 | How long shutdown waits for fibers still running. |
328
+ | `diagnostics` | false | Adds the `x-ruvoy-*` diagnostic headers to responses. |
329
+
330
+ - Envoy becomes ready only once the application has loaded and the hooks
331
+ registered with `Ruvoy::Lifecycle.before_ready { }` have run. An application
332
+ that fails to load stops Envoy.
333
+ - `Ruvoy::Lifecycle.on_drain { }` runs when Envoy begins draining, while
334
+ requests are still served: close persistent connections, stop background
335
+ work.
336
+ - Envoy's shutdown stops the runtime: fibers still running are cancelled and
337
+ given `shutdown_timeout_ms` to finish.
338
+ - The admin interface reaches Ruby: `/ruvoy` (overview), `/ruvoy/gc` (`GET`
339
+ for `GC.stat`, `POST` to collect, `?compact=1` to compact too),
340
+ `/ruvoy/fibers` (live fibers with backtraces) and `/ruvoy/config`. Ruby
341
+ answers from its own thread; what it does not answer within two seconds gets
342
+ 504.
343
+ - `Ruvoy::Lifecycle.settings` holds the settings above.
344
+ - Envoy's allocator keeps freed memory unless its `memory_allocator_manager`
345
+ sets `bytes_to_release`; without it the resident set only grows. The
346
+ generated configuration returns 1 MiB a second, and a hand-written one
347
+ should set it too.
348
+
349
+ ## Statistics
350
+
351
+ Ruvoy reports through Envoy's statistics, so the numbers reach whatever sink
352
+ Envoy has, the admin interface's `/stats/prometheus` among them.
353
+
354
+ - `requests_total`, `rejected_total{reason}`, `responses_total{outcome}` and
355
+ `duration_ms`: requests, and how those that reached the application ended.
356
+ - `inflight_requests`, `reactor_idle_ms` and `resident_bytes`: the bootstrap
357
+ extension reads them once a second on the main thread, so the request path
358
+ never publishes anything and an idle proxy reports an idle runtime.
359
+ - `upstream_calls_total{cluster, outcome}`: calls by how the application saw
360
+ them end, `completed`, `refused`, `timed_out`, `overflow`, `too_large`,
361
+ `reset` or `abandoned`. `upstream_duration_ms{cluster}` times a `call` to its
362
+ whole response and a `stream` to its head. Status codes and Envoy's own
363
+ timeouts are in the cluster's statistics.
364
+
305
365
  ## Scaling
306
366
 
307
367
  Each Ruvoy process runs exactly one CRuby VM, so Ruby work in one process is
Binary file
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
3
4
  require "yaml"
4
5
 
5
6
  module Ruvoy
@@ -12,11 +13,17 @@ module Ruvoy
12
13
  STREAM_IDLE_TIMEOUT = "15s"
13
14
  REQUEST_TIMEOUT = "30s"
14
15
  CONNECTION_BUFFER_LIMIT_BYTES = 4_194_304
16
+ EXTENSION_NAME = "ruvoy"
17
+ STRING_VALUE = "type.googleapis.com/google.protobuf.StringValue"
15
18
 
16
19
  module_function
17
20
 
18
21
  def build(rackup:, address:, port:, admin_port: nil)
19
- config = { "static_resources" => { "listeners" => [ listener(rackup, address, port) ] } }
22
+ config = {
23
+ "bootstrap_extensions" => [ bootstrap_extension(rackup: rackup) ],
24
+ "memory_allocator_manager" => memory_allocator_manager,
25
+ "static_resources" => { "listeners" => [ listener(address, port) ] }
26
+ }
20
27
  config["admin"] = admin(address, admin_port) if admin_port
21
28
  config
22
29
  end
@@ -25,16 +32,42 @@ module Ruvoy
25
32
  YAML.dump(build(**options))
26
33
  end
27
34
 
28
- def listener(rackup, address, port)
35
+ # The extension that starts the Ruby runtime and names the application.
36
+ # Every listener's rack filter joins the runtime it started, so there is
37
+ # exactly one per Envoy.
38
+ def bootstrap_extension(rackup:, **settings)
39
+ {
40
+ "name" => EXTENSION_NAME,
41
+ "typed_config" => {
42
+ "@type" => "type.googleapis.com/envoy.extensions.bootstrap.dynamic_modules.v3." \
43
+ "DynamicModuleBootstrapExtension",
44
+ "dynamic_module_config" => module_config,
45
+ "extension_name" => EXTENSION_NAME,
46
+ "extension_config" => {
47
+ "@type" => STRING_VALUE,
48
+ "value" => JSON.generate({ "rackup" => rackup }.merge(settings.transform_keys(&:to_s)))
49
+ }
50
+ }
51
+ }
52
+ end
53
+
54
+ # The same as a top-level YAML section, to head a configuration written
55
+ # by hand.
56
+ def bootstrap_yaml(rackup:, **settings)
57
+ YAML.dump("bootstrap_extensions" => [ bootstrap_extension(rackup: rackup, **settings) ])
58
+ .delete_prefix("---\n")
59
+ end
60
+
61
+ def listener(address, port)
29
62
  {
30
63
  "name" => "ruvoy",
31
64
  "per_connection_buffer_limit_bytes" => CONNECTION_BUFFER_LIMIT_BYTES,
32
65
  "address" => { "socket_address" => { "address" => address, "port_value" => port } },
33
- "filter_chains" => [ { "filters" => [ connection_manager(rackup) ] } ]
66
+ "filter_chains" => [ { "filters" => [ connection_manager ] } ]
34
67
  }
35
68
  end
36
69
 
37
- def connection_manager(rackup)
70
+ def connection_manager
38
71
  {
39
72
  "name" => "envoy.filters.network.http_connection_manager",
40
73
  "typed_config" => {
@@ -48,29 +81,41 @@ module Ruvoy
48
81
  "name" => "local_route",
49
82
  "virtual_hosts" => [ { "name" => "local_service", "domains" => [ "*" ] } ]
50
83
  },
51
- "http_filters" => [ rack_filter(rackup) ]
84
+ "http_filters" => [ rack_filter ]
52
85
  }
53
86
  }
54
87
  end
55
88
 
56
89
  # Terminal: the Rack application answers the request, so no router follows.
57
- def rack_filter(rackup)
90
+ # The filter has nothing of its own to configure here; the application is
91
+ # the bootstrap extension's.
92
+ def rack_filter
58
93
  {
59
94
  "name" => "envoy.extensions.filters.http.dynamic_modules",
60
95
  "typed_config" => {
61
96
  "@type" => "type.googleapis.com/envoy.extensions.filters.http." \
62
97
  "dynamic_modules.v3.DynamicModuleFilter",
63
- "dynamic_module_config" => { "name" => "ruvoy_fiber", "do_not_close" => true },
98
+ "dynamic_module_config" => module_config,
64
99
  "filter_name" => "fiber_rack",
65
100
  "terminal_filter" => true,
66
- "filter_config" => {
67
- "@type" => "type.googleapis.com/google.protobuf.StringValue",
68
- "value" => rackup
69
- }
101
+ "filter_config" => { "@type" => STRING_VALUE, "value" => "{}" }
70
102
  }
71
103
  }
72
104
  end
73
105
 
106
+ # Envoy's allocator keeps what Ruby and Envoy free unless told to return
107
+ # it, so the resident set would only ever grow. Its own background thread
108
+ # returns a megabyte a second, more than a busy process frees.
109
+ def memory_allocator_manager
110
+ { "bytes_to_release" => 1_048_576, "memory_release_interval" => "1s" }
111
+ end
112
+
113
+ # A fresh hash each time: one object in two places would come out of the
114
+ # YAML dumper as an anchor and an alias.
115
+ def module_config
116
+ { "name" => "ruvoy_fiber", "do_not_close" => true }
117
+ end
118
+
74
119
  def admin(address, port)
75
120
  {
76
121
  "address" => { "socket_address" => { "address" => address, "port_value" => port } }
data/lib/ruvoy/version.rb CHANGED
@@ -3,5 +3,5 @@
3
3
  module Ruvoy
4
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.2.1"
6
+ VERSION = "0.4.0"
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.2.1
4
+ version: 0.4.0
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Zixin Zhou