async-service-supervisor-envoy 0.4.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d51f4a0841888cdb84d1d321b67202fdeab2aaba77bc54e3dbcb856d57658406
4
- data.tar.gz: feea549ad0b41d010cad21f9375159bd3e43c2cb97c9314172f9288e697048cb
3
+ metadata.gz: e8bbbdeea238ea31ad9ff156f47a43d168b52965e1cc0e54f972ccb2a994cfaa
4
+ data.tar.gz: e07f1e1ba197e0c7b4a629072dc8177a371d9f31189f4f6c8e03bf4899fd93c5
5
5
  SHA512:
6
- metadata.gz: 2cfa5c93b3e1090d045e5087ad70931c539b37d607aff20de1bab17625078dd728f0312fba050a7e9158fc4aed8bfb3e8b93863ce4b00c2fd552e20d09173317
7
- data.tar.gz: e8008adf78279955e66f6852ce111bb2c451a377cbddf6f51ab2bbea0f1f51d91b2bdfce2420738eaf0e77e601a1ea282ff6ad105abfaadb31a0774d902ea960
6
+ metadata.gz: de6300d313bc18e8002defade3348a29216571401b6aba6effa3d4a9c2389704765014f395a3e0f4121fa8ae7e2e254dd0049b93fecd779838601444bd2f0d60
7
+ data.tar.gz: 4d36bd1ceb4d691e15b02042a3c0d214a656a170fd518585d5a266338011381a8eef97956a0bc9caecc9a6e3d02595e92411381294934628f835bbad256a5d6f
checksums.yaml.gz.sig CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  # Getting Started
2
2
 
3
- This guide explains how to use `async-service-supervisor-envoy` to publish supervised worker endpoints to Envoy using xDS.
3
+ This guide explains how to use `async-service-supervisor-envoy` to publish supervised worker clusters and endpoints to Envoy using xDS.
4
4
 
5
5
  ## Installation
6
6
 
@@ -16,10 +16,12 @@ The gem depends on `async-service-supervisor` and `async-grpc-xds`.
16
16
 
17
17
  `async-service-supervisor-envoy` provides:
18
18
 
19
- - {ruby Async::Service::Supervisor::Envoy::Monitor} - A supervisor monitor that publishes worker endpoints through xDS.
19
+ - {ruby Async::Service::Supervisor::Envoy::Monitor} - A supervisor monitor that publishes worker clusters through CDS and their endpoints through EDS.
20
20
  - {ruby Async::Service::Supervisor::Envoy::Endpoint} - A small value object for endpoint state.
21
21
 
22
- The monitor runs an xDS control plane endpoint. Envoy connects to it using ADS and receives CDS/EDS updates derived from supervisor worker state.
22
+ The monitor always serves a dedicated Endpoint Discovery Service stream. By default it also serves Cluster Discovery Service. It does not claim Envoy's Aggregated Discovery Service, so another control plane can use ADS for listeners, routes, and other configuration.
23
+
24
+ CDS describes the logical services exposed by supervised workers, including their supported protocol, active health checks, and load-balancing policy. EDS supplies the concrete workers currently available for each service.
23
25
 
24
26
  ## Endpoint State
25
27
 
@@ -62,11 +64,59 @@ Add the monitor to your supervisor environment:
62
64
  require "async/service/supervisor/envoy"
63
65
 
64
66
  Async::Service::Supervisor::Envoy::Monitor.new(
65
- bind: "http://127.0.0.1:18000"
67
+ bind: "http://127.0.0.1:18000",
68
+ management_cluster: "xds_cluster"
66
69
  )
67
70
  ```
68
71
 
69
- By default, workers are grouped into clusters by `state[:name]`.
72
+ By default, workers are grouped into clusters by `state[:name]`. When cluster publication is enabled, `management_cluster` must match the static Envoy cluster used to reach the monitor.
73
+
74
+ If another control plane owns cluster configuration, disable CDS publication while retaining supervisor-owned endpoint discovery:
75
+
76
+ ``` ruby
77
+ Async::Service::Supervisor::Envoy::Monitor.new(
78
+ bind: "http://127.0.0.1:18000",
79
+ publish_clusters: false
80
+ )
81
+ ```
82
+
83
+ In this mode, the external control plane or bootstrap configuration must define each cluster, configure it to use the monitor's dedicated EDS service, and supply its protocol, health checks, and load-balancing policy. The cluster's EDS service name must match the worker cluster name published by the monitor.
84
+
85
+ ## Envoy Configuration
86
+
87
+ Configure Envoy to obtain clusters from the monitor's dedicated CDS service. The monitor configures each discovered cluster to obtain its endpoints from the dedicated EDS service on the same management server:
88
+
89
+ ``` yaml
90
+ dynamic_resources:
91
+ cds_config:
92
+ resource_api_version: V3
93
+ api_config_source:
94
+ api_type: GRPC
95
+ transport_api_version: V3
96
+ grpc_services:
97
+ - envoy_grpc:
98
+ cluster_name: xds_cluster
99
+
100
+ static_resources:
101
+ clusters:
102
+ - name: xds_cluster
103
+ connect_timeout: 1s
104
+ type: STRICT_DNS
105
+ http2_protocol_options: {}
106
+ load_assignment:
107
+ cluster_name: xds_cluster
108
+ endpoints:
109
+ - lb_endpoints:
110
+ - endpoint:
111
+ address:
112
+ socket_address:
113
+ address: 127.0.0.1
114
+ port_value: 18000
115
+ ```
116
+
117
+ The bootstrap cluster name must match the monitor's `management_cluster`. Envoy uses it for independent CDS and EDS gRPC streams; no `ads_config` is required.
118
+
119
+ If a static route refers to a cluster delivered by CDS, set `validate_clusters: false` on that route configuration. Envoy can then load the route before the cluster arrives and will begin routing once CDS and EDS have warmed it.
70
120
 
71
121
  ## Custom Mapping
72
122
 
@@ -113,10 +163,14 @@ utilization_monitor = Async::Service::Supervisor::UtilizationMonitor.new(interva
113
163
  ]
114
164
  ```
115
165
 
116
- The supervisor utilization monitor manages each worker's shared-memory allocation and registration. The Envoy monitor samples it through `sample_by_worker`, combines each worker's `requests_total` counter with processor usage from `process-metrics`, and serves the resulting ORCA reports from the same HTTP/2 endpoint as ADS. It also configures each discovered cluster to use Envoy's client-side weighted-round-robin policy.
166
+ The supervisor utilization monitor manages each worker's shared-memory allocation and registration. The Envoy monitor samples it through `sample_by_worker`, combines each worker's `requests_total` counter with processor usage from `process-metrics`, and serves the resulting ORCA reports from the same HTTP/2 endpoint as CDS and EDS. It also configures each discovered cluster to use Envoy's client-side weighted-round-robin policy.
117
167
 
118
168
  The first sample establishes a baseline. Subsequent reports contain normalized `cpu_utilization` and `rps_fractional` values for each worker. Reports are removed immediately when a worker disconnects.
119
169
 
170
+ Workers are identified by the `hostname` published with each endpoint, which Envoy sends as the request authority when it opens an out-of-band reporting stream. Enabling ORCA therefore publishes one endpoint per worker rather than one per shared listener.
171
+
172
+ The generated load-balancing policy uses the monitor's bind port and reporting interval. Envoy dials that port on each endpoint's own address, which reaches the monitor because it shares a network namespace with the workers.
173
+
120
174
  Out-of-band ORCA requires:
121
175
 
122
176
  - Envoy 1.39 or later.
data/context/index.yaml CHANGED
@@ -9,4 +9,4 @@ files:
9
9
  - path: getting-started.md
10
10
  title: Getting Started
11
11
  description: This guide explains how to use `async-service-supervisor-envoy` to
12
- publish supervised worker endpoints to Envoy using xDS.
12
+ publish supervised worker clusters and endpoints to Envoy using xDS.
@@ -3,12 +3,15 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2026, by Samuel Williams.
5
5
 
6
- require "async/http/endpoint"
7
- require "async/service/supervisor/monitor"
8
- require "async/service/supervisor/utilization_monitor"
9
6
  require "async/grpc/xds/client_side_weighted_round_robin"
7
+ require "async/grpc/xds/cluster_discovery_service"
8
+ require "async/grpc/xds/config_source"
10
9
  require "async/grpc/xds/control_plane"
10
+ require "async/grpc/xds/endpoint_discovery_service"
11
11
  require "async/grpc/xds/server"
12
+ require "async/http/endpoint"
13
+ require "async/service/supervisor/monitor"
14
+ require "async/service/supervisor/utilization_monitor"
12
15
  require "process/metrics"
13
16
  require "xds/data/orca/v3/orca_load_report_pb"
14
17
 
@@ -22,12 +25,17 @@ module Async
22
25
  module Supervisor
23
26
  # Provides Envoy integration for supervisor-managed services.
24
27
  module Envoy
25
- # Represents a supervisor monitor that publishes worker endpoints to Envoy using xDS.
28
+ # Represents a supervisor monitor that publishes worker endpoints and optionally clusters to Envoy using xDS.
29
+ #
30
+ # The monitor always serves a dedicated EDS stream and can additionally serve
31
+ # CDS, leaving ADS available to another control plane.
26
32
  class Monitor < Async::Service::Supervisor::Monitor
27
33
  # Initialize the monitor.
28
- # @parameter bind [String | Nil] The optional address for the xDS control plane server.
34
+ # @parameter bind [String | Nil] The optional address for the discovery server.
29
35
  # @parameter delegate [Delegate] The delegate used to map supervisor state into Envoy endpoints.
30
36
  # @parameter control_plane [Async::GRPC::XDS::ControlPlane] The xDS control plane to update.
37
+ # @parameter management_cluster [String] The static Envoy cluster used to reach this discovery server.
38
+ # @parameter publish_clusters [Boolean] Whether to publish derived cluster configuration through CDS.
31
39
  # @parameter health_checks [Array(Envoy::Config::Core::V3::HealthCheck)] The active health checks applied to published clusters.
32
40
  # @parameter orca [Boolean] Whether to collect and serve per-worker ORCA load reports.
33
41
  # @parameter processor [Process::Metrics::Processor | Nil] The optional process CPU sampler.
@@ -37,6 +45,8 @@ module Async
37
45
  bind: nil,
38
46
  delegate: Delegate.new,
39
47
  control_plane: Async::GRPC::XDS::ControlPlane.new,
48
+ management_cluster: "xds_cluster",
49
+ publish_clusters: true,
40
50
  health_checks: [],
41
51
  orca: false,
42
52
  processor: nil,
@@ -49,12 +59,14 @@ module Async
49
59
  @bind = bind
50
60
  @delegate = delegate
51
61
  @control_plane = control_plane
62
+ @eds_config = Async::GRPC::XDS::ConfigSource.grpc(management_cluster)
63
+ @publish_clusters = publish_clusters
52
64
  @health_checks = health_checks
53
65
  @interval = interval
54
66
  @orca = orca
55
67
  @controllers = {}
56
68
  @published_clusters = {}
57
- @server_task = nil
69
+ @published_endpoints = {}
58
70
  @mutex = Mutex.new
59
71
 
60
72
  if @orca
@@ -105,15 +117,21 @@ module Async
105
117
  end
106
118
  end
107
119
 
108
- # Run the monitor and optional xDS server task.
109
- # @parameter parent [Async::Task] The parent task used for the xDS server.
120
+ # Run the monitor and optional discovery server task.
121
+ # @parameter parent [Async::Task] The parent task used for the server.
110
122
  # @returns [Async::Task] The monitor task.
111
123
  def run(parent: Async::Task.current)
112
124
  task = super(parent: parent)
113
125
 
114
126
  if @bind
115
- @server_task = parent.async do
116
- server = Async::GRPC::XDS::Server.new(@control_plane)
127
+ parent.async do
128
+ services = [Async::GRPC::XDS::EndpointDiscoveryService]
129
+ services.unshift(Async::GRPC::XDS::ClusterDiscoveryService) if @publish_clusters
130
+
131
+ server = Async::GRPC::XDS::Server.new(
132
+ @control_plane,
133
+ services: services
134
+ )
117
135
  server.dispatcher.register(ORCAService.new(self, minimum_interval: @interval)) if @orca
118
136
  server.run(server_endpoint)
119
137
  end
@@ -192,17 +210,26 @@ module Async
192
210
  records_by_cluster = build_records_by_cluster
193
211
  clusters = build_clusters(records_by_cluster)
194
212
 
195
- records_by_cluster.each do |cluster, records|
196
- configuration = cluster_configuration(records)
197
-
198
- unless @published_clusters[cluster] == configuration
199
- @control_plane.update_cluster(cluster, **configuration)
200
- @published_clusters[cluster] = configuration
213
+ if @publish_clusters
214
+ records_by_cluster.each do |cluster, records|
215
+ configuration = cluster_configuration(records)
216
+
217
+ unless @published_clusters[cluster] == configuration
218
+ @control_plane.update_cluster(cluster, **configuration)
219
+ @published_clusters[cluster] = configuration
220
+ end
201
221
  end
222
+ elsif @orca
223
+ records_by_cluster.each_value{|records| validate_orca_records(records)}
202
224
  end
203
225
 
204
- (@published_clusters.keys | clusters.keys).each do |cluster|
205
- @control_plane.update_endpoints(cluster, clusters.fetch(cluster, []))
226
+ # Skipping an identical assignment only avoids a redundant version bump:
227
+ (@published_endpoints.keys | clusters.keys).each do |cluster|
228
+ endpoints = clusters.fetch(cluster, [])
229
+ next if @published_endpoints[cluster] == endpoints
230
+
231
+ @control_plane.update_endpoints(cluster, endpoints)
232
+ @published_endpoints[cluster] = endpoints
206
233
  end
207
234
  end
208
235
 
@@ -218,6 +245,9 @@ module Async
218
245
  records_by_cluster.transform_values do |records|
219
246
  if @orca
220
247
  records.map do |record|
248
+ # Envoy sends the endpoint hostname as the request authority when
249
+ # it opens an out-of-band reporting stream, which is how a single
250
+ # ORCA service identifies which worker a report is for:
221
251
  {
222
252
  addresses: record[:endpoint].addresses,
223
253
  healthy: record[:healthy],
@@ -248,13 +278,12 @@ module Async
248
278
 
249
279
  configuration = {
250
280
  protocol: envoy_protocol(common_protocols),
281
+ eds_config: @eds_config,
251
282
  health_checks: @health_checks,
252
283
  }
253
284
 
254
285
  if @orca
255
- if records.any?{|record| record[:endpoint].addresses.any?{|address| address[:path]}}
256
- raise ArgumentError, "Out-of-band ORCA reporting requires IP endpoints!"
257
- end
286
+ validate_orca_records(records)
258
287
 
259
288
  configuration[:load_balancing_policy] = Async::GRPC::XDS::ClientSideWeightedRoundRobin.build(
260
289
  @orca_port,
@@ -265,6 +294,12 @@ module Async
265
294
  configuration
266
295
  end
267
296
 
297
+ def validate_orca_records(records)
298
+ if records.any?{|record| record[:endpoint].addresses.any?{|address| address[:path]}}
299
+ raise ArgumentError, "Out-of-band ORCA reporting requires IP endpoints!"
300
+ end
301
+ end
302
+
268
303
  def sample_load_reports
269
304
  controllers = @mutex.synchronize{@controllers.dup}
270
305
  workers = @utilization_monitor.sample_by_worker
@@ -11,7 +11,7 @@ module Async
11
11
  module Supervisor
12
12
  # @namespace
13
13
  module Envoy
14
- VERSION = "0.4.0"
14
+ VERSION = "0.5.0"
15
15
  end
16
16
  end
17
17
  end
data/readme.md CHANGED
@@ -8,7 +8,7 @@ Provides an Envoy xDS monitor for `async-service-supervisor`.
8
8
 
9
9
  `async-service-supervisor-envoy` publishes supervised worker endpoints to Envoy:
10
10
 
11
- - **xDS control plane** - Runs an ADS server backed by `async-grpc-xds`.
11
+ - **Cluster and endpoint discovery** - Runs dedicated CDS and EDS services backed by `async-grpc-xds`, leaving ADS free for listener, route, and other configuration.
12
12
  - **Supervisor integration** - Registers and removes endpoints from supervisor worker lifecycle events.
13
13
  - **Multiple clusters** - Groups workers by `state[:name]` by default.
14
14
  - **Endpoint contract** - Converts concrete post-bind worker listeners into Envoy upstream endpoints, including grouped IP or Unix socket addresses.
@@ -19,12 +19,20 @@ Provides an Envoy xDS monitor for `async-service-supervisor`.
19
19
 
20
20
  Please see the [project documentation](https://socketry.github.io/async-service-supervisor-envoy/) for more details.
21
21
 
22
- - [Getting Started](https://socketry.github.io/async-service-supervisor-envoy/guides/getting-started/index) - This guide explains how to use `async-service-supervisor-envoy` to publish supervised worker endpoints to Envoy using xDS.
22
+ - [Getting Started](https://socketry.github.io/async-service-supervisor-envoy/guides/getting-started/index) - This guide explains how to use `async-service-supervisor-envoy` to publish supervised worker clusters and endpoints to Envoy using xDS.
23
23
 
24
24
  ## Releases
25
25
 
26
26
  Please see the [project releases](https://socketry.github.io/async-service-supervisor-envoy/releases/index) for all releases.
27
27
 
28
+ ### v0.5.0
29
+
30
+ - Serve clusters and endpoints through dedicated CDS and EDS services instead of the aggregated discovery service, leaving ADS available for listener, route, and other configuration.
31
+ - Allow cluster publication to be disabled when clusters are owned by another control plane.
32
+ - Configure generated clusters to obtain endpoint assignments from the dedicated EDS service.
33
+ - Publish endpoint assignments only when they change, instead of on every reconciliation.
34
+ - Use normalized processor utilization from `process-metrics` v0.13.
35
+
28
36
  ### v0.4.0
29
37
 
30
38
  - Publish configured active health checks with Envoy clusters.
data/releases.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Releases
2
2
 
3
+ ## v0.5.0
4
+
5
+ - Serve clusters and endpoints through dedicated CDS and EDS services instead of the aggregated discovery service, leaving ADS available for listener, route, and other configuration.
6
+ - Allow cluster publication to be disabled when clusters are owned by another control plane.
7
+ - Configure generated clusters to obtain endpoint assignments from the dedicated EDS service.
8
+ - Publish endpoint assignments only when they change, instead of on every reconciliation.
9
+ - Use normalized processor utilization from `process-metrics` v0.13.
10
+
3
11
  ## v0.4.0
4
12
 
5
13
  - Publish configured active health checks with Envoy clusters.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-service-supervisor-envoy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '0.3'
61
+ version: '0.4'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '0.3'
68
+ version: '0.4'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: async-http
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -100,14 +100,14 @@ dependencies:
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '0.12'
103
+ version: '0.13'
104
104
  type: :runtime
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '0.12'
110
+ version: '0.13'
111
111
  executables: []
112
112
  extensions: []
113
113
  extra_rdoc_files: []
metadata.gz.sig CHANGED
Binary file