ruvoy 0.3.0-x86_64-linux → 0.4.0-x86_64-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: 267edfc5bd783c89f4cdf67a901208abc8bdc864731e317f54844a90edccdc07
4
- data.tar.gz: 3f2606403c1b214ac244e22bd4eb9bb93fb6a015fbc06cdc57412b7cc9822323
3
+ metadata.gz: 43b67f3cd76185d5e52d15ad82f8be514dfb50b6bfa3bcdfc4eec646f27674e4
4
+ data.tar.gz: 4389a11aaecd521e79cb814a0c8a3946fe94641a65fa83838fbd75595b4698f8
5
5
  SHA512:
6
- metadata.gz: 04dff6a6abdaaf19a26d522a66d72c334ae59dc2e7b60bd9b149f0196df7718d90a5e41a76e43a743c64353620da088772ced2d13ce00bd32aaf3fa133194405
7
- data.tar.gz: 4da1c6f133dd80441c1966eb396b1fd6b898d84a094dc8340257cb9b5e758648f5e55130e365a6296aff601426d65c776fde20dad770f784bffad023fd4541b4
6
+ metadata.gz: d7c5244ba2652458809d847392bd94b576f9f470b65fdaf83c0d4d0fa42bd15b23938f5c37b3923931dab0403867382428a8db439ef2bf64b304ac8d6c6af531
7
+ data.tar.gz: c0dd2782811b58957818c178959492dbda33af21474449e17e73a26b6e8b0c9568b8ba7f9180c61fe76b62bc91f55d87871133486a9286d40f72aa2b1db42306
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
 
@@ -213,7 +216,6 @@ JSON object:
213
216
 
214
217
  ```json
215
218
  {
216
- "rackup": "/srv/app/config.ru",
217
219
  "context": { "metadata_namespaces": ["acme.tenant"] },
218
220
  "upstream": { "clusters": ["users"], "timeout_ms": 15000, "max_response_bytes": 1048576 }
219
221
  }
@@ -250,7 +252,8 @@ What Envoy's module interface can report sets some limits:
250
252
 
251
253
  ### `ruvoy.upstream`
252
254
 
253
- 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:
254
257
 
255
258
  ```ruby
256
259
  status, headers, body = env["ruvoy.upstream"].call(
@@ -293,9 +296,72 @@ Envoy applies no flow control to it, so a reader that falls more than
293
296
  With `protocol: "websocket"`, the response upgrades the client too and relays
294
297
  both ways.
295
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
+
296
302
  [`examples/extensions`](examples/extensions) runs both against a stand-in
297
303
  service, including two calls from one request overlapping.
298
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
+
299
365
  ## Scaling
300
366
 
301
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.3.0"
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.3.0
4
+ version: 0.4.0
5
5
  platform: x86_64-linux
6
6
  authors:
7
7
  - Zixin Zhou