async-grpc-xds 0.3.0 → 0.4.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: fb5e9b5b4fbb4b738a33f5714254602767c65774e0d3cab49c0c7528042885c5
4
- data.tar.gz: 7c1fc5b76c954ed6c2a61f6b2f3e154f1287492f9f81438cfccec0788d803beb
3
+ metadata.gz: 3f5ca6a95f124eec9df174142e956b1e5c619e22258ed905202526042e3519a9
4
+ data.tar.gz: af26002e1170beff3b9ba228d12f21f0c1fc6893d8d8c5cf3da30b1af3bced3e
5
5
  SHA512:
6
- metadata.gz: ba0e68b3e2b1a7767ae5d3e62646818a746d5553d7a2187511914d2cb8cc37a491fae834865294936c6982abefff76b676b0b8bfb67ad93eca55eb3b9f6245d9
7
- data.tar.gz: e973bf4dd5bbf1e54efd2c6fba9134960d879a66039e49c0361b47d6bacba1f9e458d0cbcd4c109fe2d134794fef3983c86911332564a3557244b39bb95fbdde
6
+ metadata.gz: d906ffc9d3874ae1430cf345ee0ec7cc6455266b55743e6fdaa9d9893c923188bb77612dfa8dd7e021940ba586bd06a3554d29dd0561ea3c6dbdb67a0629c0a5
7
+ data.tar.gz: 37aa45338885d4ed19a0f76d9c81da3c95391e694e9d6919d5cb8f00f0137e575bc982e0e72ca85a2bbe660cb1d7dd0e8d3f65ef861260bee54741087bba8b51
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,363 @@
1
+ # Getting Started
2
+
3
+ This guide explains how to use `async-grpc-xds` to publish CDS and EDS resources to Envoy, or to discover gRPC backends from Ruby using xDS.
4
+
5
+ ## Installation
6
+
7
+ Add the gem to your project:
8
+
9
+ ~~~ bash
10
+ $ bundle add async-grpc-xds
11
+ ~~~
12
+
13
+ The gem provides both sides of an xDS integration:
14
+
15
+ - A control-plane server which publishes in-memory resources to Envoy over ADS or dedicated discovery services.
16
+ - An experimental Ruby gRPC client which discovers clusters and endpoints from an ADS server.
17
+
18
+ ## Core Concepts
19
+
20
+ xDS separates logical proxy configuration from the concrete destinations which currently provide it:
21
+
22
+ | API | Resource | Purpose |
23
+ | --- | --- | --- |
24
+ | CDS | `Cluster` | Describes an upstream service, protocol, health checks, load-balancing policy, and how to discover its endpoints. |
25
+ | EDS | `ClusterLoadAssignment` | Supplies the concrete IP or Unix-socket endpoints for a cluster. |
26
+ | RDS | `RouteConfiguration` | Maps incoming requests to clusters. |
27
+ | LDS | `Listener` | Describes the addresses and network filters where Envoy accepts traffic. |
28
+
29
+ `async-grpc-xds` currently builds and serves CDS and EDS resources. Routes and listeners normally remain in Envoy's bootstrap configuration or come from another control plane.
30
+
31
+ Three names connect the configuration:
32
+
33
+ - The **management cluster** is a static Envoy cluster, such as `xds_cluster`, which reaches the Ruby xDS server.
34
+ - The **application cluster** is the logical upstream service, such as `application`.
35
+ - The EDS `service_name` and `ClusterLoadAssignment#cluster_name` identify the endpoint assignment used by that application cluster. They default to the application cluster name.
36
+
37
+ The following diagram shows the dedicated CDS and EDS arrangement used throughout this guide:
38
+
39
+ ``` mermaid
40
+ flowchart LR
41
+ Bootstrap[Envoy bootstrap] --> Management[xds_cluster]
42
+ Management -->|CDS stream| ControlPlane[Ruby control plane]
43
+ Management -->|EDS stream| ControlPlane
44
+ ControlPlane -->|Cluster| Application[application cluster]
45
+ ControlPlane -->|ClusterLoadAssignment| Workers[worker endpoints]
46
+ Traffic[Application traffic] --> Envoy
47
+ Envoy --> Application
48
+ Application --> Workers
49
+ ```
50
+
51
+ ## Serving Dedicated CDS and EDS
52
+
53
+ Dedicated discovery services are a good fit when this control plane owns application clusters and their local workers, but should not claim Envoy's single ADS connection. Another control plane can then use ADS for coordinated listener, route, or other configuration.
54
+
55
+ Create a control plane, publish an application cluster and its initial workers, then serve the dedicated CDS and EDS interfaces:
56
+
57
+ ``` ruby
58
+ require "async"
59
+ require "async/grpc/xds"
60
+ require "async/http/endpoint"
61
+
62
+ control_plane = Async::GRPC::XDS::ControlPlane.new(identifier: "application-supervisor")
63
+ eds_config = Async::GRPC::XDS::ConfigSource.grpc("xds_cluster")
64
+ health_check = Async::GRPC::XDS::HTTPHealthCheck.build(
65
+ "/health",
66
+ interval: 2,
67
+ timeout: 1
68
+ )
69
+
70
+ control_plane.update_cluster(
71
+ "application",
72
+ protocol: :http1,
73
+ eds_config: eds_config,
74
+ health_checks: [health_check]
75
+ )
76
+
77
+ control_plane.update_endpoints("application", [
78
+ {
79
+ hostname: "worker-1",
80
+ addresses: [{address: "127.0.0.1", port: 9292}],
81
+ healthy: true,
82
+ },
83
+ {
84
+ hostname: "worker-2",
85
+ addresses: [{address: "127.0.0.1", port: 9293}],
86
+ healthy: true,
87
+ },
88
+ ])
89
+
90
+ server = Async::GRPC::XDS::Server.new(
91
+ control_plane,
92
+ services: [
93
+ Async::GRPC::XDS::ClusterDiscoveryService,
94
+ Async::GRPC::XDS::EndpointDiscoveryService,
95
+ ]
96
+ )
97
+
98
+ endpoint = Async::HTTP::Endpoint.parse(
99
+ "http://0.0.0.0:18000",
100
+ protocol: Async::HTTP::Protocol::HTTP2
101
+ )
102
+
103
+ Sync do
104
+ server.run(endpoint)
105
+ end
106
+ ```
107
+
108
+ The management endpoint uses HTTP/2 because xDS is a gRPC protocol. This example uses plaintext HTTP/2 within a trusted local network; deployments can instead configure TLS on the endpoint.
109
+
110
+ ### Envoy Bootstrap
111
+
112
+ Envoy must know how to reach the management server before it can discover anything else. Define `xds_cluster` statically and configure CDS to use it:
113
+
114
+ ``` yaml
115
+ node:
116
+ id: application-proxy-1
117
+ cluster: application-proxies
118
+
119
+ admin:
120
+ address:
121
+ socket_address:
122
+ address: 127.0.0.1
123
+ port_value: 19000
124
+
125
+ dynamic_resources:
126
+ cds_config:
127
+ resource_api_version: V3
128
+ api_config_source:
129
+ api_type: GRPC
130
+ transport_api_version: V3
131
+ grpc_services:
132
+ - envoy_grpc:
133
+ cluster_name: xds_cluster
134
+
135
+ static_resources:
136
+ listeners:
137
+ - name: ingress
138
+ address:
139
+ socket_address:
140
+ address: 0.0.0.0
141
+ port_value: 8080
142
+ filter_chains:
143
+ - filters:
144
+ - name: envoy.filters.network.http_connection_manager
145
+ typed_config:
146
+ "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
147
+ stat_prefix: ingress_http
148
+ route_config:
149
+ name: local_route
150
+ validate_clusters: false
151
+ virtual_hosts:
152
+ - name: application
153
+ domains: ["*"]
154
+ routes:
155
+ - match:
156
+ prefix: "/"
157
+ route:
158
+ cluster: application
159
+ http_filters:
160
+ - name: envoy.filters.http.router
161
+ typed_config:
162
+ "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
163
+
164
+ clusters:
165
+ - name: xds_cluster
166
+ connect_timeout: 1s
167
+ type: STATIC
168
+ typed_extension_protocol_options:
169
+ envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
170
+ "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
171
+ explicit_http_config:
172
+ http2_protocol_options: {}
173
+ load_assignment:
174
+ cluster_name: xds_cluster
175
+ endpoints:
176
+ - lb_endpoints:
177
+ - endpoint:
178
+ address:
179
+ socket_address:
180
+ address: 127.0.0.1
181
+ port_value: 18000
182
+ ```
183
+
184
+ The static route names `application` before CDS has delivered that cluster, so `validate_clusters: false` allows the bootstrap configuration to load. Envoy keeps the discovered cluster warming until its EDS assignment arrives.
185
+
186
+ The `xds_cluster` name must match the name passed to {ruby Async::GRPC::XDS::ConfigSource.grpc}. The generated application cluster then opens its dedicated EDS stream through the same management cluster.
187
+
188
+ ## Publishing Endpoint Changes
189
+
190
+ The control plane is thread-safe and immediately notifies connected discovery streams after a resource changes. Publish the complete current endpoint assignment whenever workers start, stop, or change health:
191
+
192
+ ``` ruby
193
+ control_plane.update_endpoints("application", [
194
+ {
195
+ hostname: "worker-2",
196
+ addresses: [{address: "127.0.0.1", port: 9293}],
197
+ healthy: true,
198
+ },
199
+ ])
200
+ ```
201
+
202
+ This implementation uses state-of-the-world discovery: each EDS response contains the complete assignment requested by Envoy. It does not currently implement the delta discovery RPCs.
203
+
204
+ Every call to `update_cluster` or `update_endpoints` increments the version for that resource type, even if the generated resource is unchanged. Producers should therefore avoid publishing redundant updates.
205
+
206
+ To remove a service completely, remove both resources:
207
+
208
+ ``` ruby
209
+ control_plane.remove_endpoints("application")
210
+ control_plane.remove_cluster("application")
211
+ ```
212
+
213
+ ### IP and Unix-Socket Addresses
214
+
215
+ An endpoint must contain one or more addresses. An IP address uses `:address` and `:port`:
216
+
217
+ ``` ruby
218
+ {
219
+ addresses: [{address: "127.0.0.1", port: 9292}],
220
+ healthy: true,
221
+ }
222
+ ```
223
+
224
+ A Unix domain socket uses `:path`:
225
+
226
+ ``` ruby
227
+ {
228
+ addresses: [{path: "/run/application/worker-1.ipc"}],
229
+ healthy: true,
230
+ }
231
+ ```
232
+
233
+ Several addresses in one `:addresses` array describe alternative addresses for one logical load-balancer endpoint. The first becomes Envoy's primary address and the remainder become `additional_addresses`; they do not represent additional workers.
234
+
235
+ ### Health
236
+
237
+ The `:healthy` value sets the EDS `health_status` for an endpoint. It accepts healthy, unhealthy, degraded, or unknown states; booleans map to healthy and unhealthy.
238
+
239
+ This reported state is separate from active health checks. Adding a health check to the cluster tells Envoy to probe every published endpoint itself:
240
+
241
+ ``` ruby
242
+ health_check = Async::GRPC::XDS::HTTPHealthCheck.build(
243
+ "/health",
244
+ interval: 2,
245
+ timeout: 1,
246
+ unhealthy_threshold: 2,
247
+ healthy_threshold: 1
248
+ )
249
+
250
+ control_plane.update_cluster(
251
+ "application",
252
+ protocol: :http1,
253
+ eds_config: Async::GRPC::XDS::ConfigSource.grpc("xds_cluster"),
254
+ health_checks: [health_check]
255
+ )
256
+ ```
257
+
258
+ Use reported health for information already known by the resource owner, such as whether a worker remains registered. Use active health checks when Envoy should independently verify that it can send application traffic to the endpoint.
259
+
260
+ ## ADS or Dedicated Services
261
+
262
+ Both transports carry the same `DiscoveryRequest` and `DiscoveryResponse` messages. The difference is how Envoy organizes its streams and management servers:
263
+
264
+ | | ADS | Dedicated CDS and EDS |
265
+ | --- | --- | --- |
266
+ | Streams | One aggregated stream for several resource types. | One stream per resource type. |
267
+ | Coordination | Provides ordering across related resource types from one control plane. | Each resource type progresses independently. |
268
+ | Ownership | Envoy has one ADS management server. | Each resource type can use its own configuration source. |
269
+ | Best fit | One control plane owns coordinated proxy configuration. | A focused control plane owns only clusters or endpoints. |
270
+
271
+ The default server exposes ADS:
272
+
273
+ ``` ruby
274
+ control_plane = Async::GRPC::XDS::ControlPlane.new
275
+ control_plane.update_cluster("application", protocol: :http1)
276
+ control_plane.update_endpoints("application", endpoints)
277
+
278
+ server = Async::GRPC::XDS::Server.new(control_plane)
279
+ ```
280
+
281
+ {ruby Async::GRPC::XDS::Cluster.build} uses ADS for EDS by default, so no explicit `eds_config` is needed in this mode. Configure Envoy accordingly:
282
+
283
+ ``` yaml
284
+ dynamic_resources:
285
+ ads_config:
286
+ api_type: GRPC
287
+ transport_api_version: V3
288
+ grpc_services:
289
+ - envoy_grpc:
290
+ cluster_name: xds_cluster
291
+ cds_config:
292
+ resource_api_version: V3
293
+ ads: {}
294
+ ```
295
+
296
+ Do not use the default ADS EDS source when serving dedicated CDS and EDS. In that arrangement, pass `ConfigSource.grpc("xds_cluster")` while building or updating every application cluster.
297
+
298
+ ## Using the Ruby xDS Client
299
+
300
+ {ruby Async::GRPC::XDS::Client} is an experimental gRPC client which discovers a named cluster and its healthy endpoints over ADS, then load-balances RPC calls between them.
301
+
302
+ Supply bootstrap configuration directly:
303
+
304
+ ``` ruby
305
+ bootstrap = {
306
+ xds_servers: [
307
+ {
308
+ server_uri: "127.0.0.1:18000",
309
+ channel_creds: [{type: "insecure"}],
310
+ }
311
+ ],
312
+ node: {
313
+ id: "orders-client-1",
314
+ cluster: "orders-clients",
315
+ }
316
+ }
317
+
318
+ Sync do
319
+ client = Async::GRPC::XDS::Client.new("application", bootstrap: bootstrap)
320
+
321
+ begin
322
+ stub = client.stub(Greeter::Interface, "example.Greeter")
323
+ response = stub.say_hello(Greeter::Request.new(name: "World"))
324
+ ensure
325
+ client.close
326
+ end
327
+ end
328
+ ```
329
+
330
+ Alternatively, pass the path to a JSON bootstrap file. When `bootstrap` is omitted, the client checks `GRPC_XDS_BOOTSTRAP` and then `~/.config/grpc/bootstrap.json`.
331
+
332
+ The Ruby client currently consumes ADS, supports CDS and EDS, filters endpoints by reported health, and provides basic client-side load balancing and retry behavior. Dedicated CDS and EDS client streams are not yet implemented.
333
+
334
+ ## Operational Checks
335
+
336
+ Envoy's admin interface shows whether it accepted the resources and whether the application cluster has usable members:
337
+
338
+ ~~~ bash
339
+ $ curl -s http://127.0.0.1:19000/config_dump
340
+ $ curl -s http://127.0.0.1:19000/clusters
341
+ $ curl -s http://127.0.0.1:19000/stats | grep -E 'cluster\.application\.(warming|membership_healthy|update_rejected)'
342
+ ~~~
343
+
344
+ When a cluster remains in `warming`, check:
345
+
346
+ - The `xds_cluster` address and port reach the Ruby server using HTTP/2.
347
+ - The management cluster name matches the name in every gRPC configuration source.
348
+ - The CDS cluster name, EDS service name, and `ClusterLoadAssignment#cluster_name` agree.
349
+ - A dedicated CDS cluster uses a dedicated EDS configuration source rather than `ads: {}`.
350
+ - Envoy has not incremented `update_rejected`; rejected responses are also returned to the control plane as NACKs and logged.
351
+
352
+ ## Current Scope
353
+
354
+ `async-grpc-xds` currently provides:
355
+
356
+ - xDS v3 protobuf definitions.
357
+ - State-of-the-world ADS, CDS, and EDS server streams.
358
+ - In-memory cluster and endpoint resources with per-type versions.
359
+ - HTTP/1 and HTTP/2 upstream cluster configuration.
360
+ - IP, Unix-socket, health-status, active HTTP health-check, and out-of-band ORCA policy resource builders.
361
+ - An experimental ADS-based Ruby gRPC client.
362
+
363
+ Delta discovery, complete NACK recovery, persistent resource storage, LDS/RDS serving, locality weighting, and complete routing semantics are not implemented yet.
@@ -0,0 +1,14 @@
1
+ # Automatically generated context index for Utopia::Project guides.
2
+ # Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
3
+ ---
4
+ description: xDS support for Async::GRPC clients.
5
+ metadata:
6
+ bug_tracker_uri: https://github.com/socketry/async-grpc-xds/issues
7
+ changelog_uri: https://github.com/socketry/async-grpc-xds/blob/main/releases.md
8
+ documentation_uri: https://socketry.github.io/async-grpc-xds/
9
+ source_code_uri: https://github.com/socketry/async-grpc-xds.git
10
+ files:
11
+ - path: getting-started.md
12
+ title: Getting Started
13
+ description: This guide explains how to use `async-grpc-xds` to publish CDS and
14
+ EDS resources to Envoy, or to discover gRPC backends from Ruby using xDS.
@@ -6,9 +6,10 @@
6
6
  require "google/protobuf/duration_pb"
7
7
 
8
8
  require "envoy/config/cluster/v3/cluster_pb"
9
- require "envoy/config/core/v3/config_source_pb"
10
9
  require "envoy/config/core/v3/protocol_pb"
11
10
 
11
+ require_relative "config_source"
12
+
12
13
  module Async
13
14
  module GRPC
14
15
  module XDS
@@ -21,21 +22,20 @@ module Async
21
22
  # Build an EDS cluster resource.
22
23
  # @parameter name [String] The cluster name.
23
24
  # @parameter service_name [String] The EDS service name.
25
+ # @parameter eds_config [Envoy::Config::Core::V3::ConfigSource] The source used to discover endpoint assignments.
24
26
  # @parameter load_balancing_policy [Envoy::Config::Cluster::V3::LoadBalancingPolicy | Nil] The typed Envoy load-balancing policy.
25
27
  # @parameter health_checks [Array(Envoy::Config::Core::V3::HealthCheck)] The active health checks applied to cluster endpoints.
26
28
  # @parameter connect_timeout [Numeric] The upstream connection timeout in seconds.
27
29
  # @parameter protocol [Symbol] The canonical upstream protocol, either `:http1` or `:http2`.
28
30
  # @returns [Envoy::Config::Cluster::V3::Cluster] The generated cluster resource.
29
31
  # @raises [ArgumentError] If the upstream protocol is unsupported.
30
- def build(name, service_name: name, load_balancing_policy: nil, health_checks: [], connect_timeout: 5, protocol: :http2)
32
+ def build(name, service_name: name, eds_config: ConfigSource.ads, load_balancing_policy: nil, health_checks: [], connect_timeout: 5, protocol: :http2)
31
33
  options = {
32
34
  name: name.to_s,
33
35
  type: Envoy::Config::Cluster::V3::Cluster::DiscoveryType::EDS,
34
36
  eds_cluster_config: Envoy::Config::Cluster::V3::Cluster::EdsClusterConfig.new(
35
37
  service_name: service_name.to_s,
36
- eds_config: Envoy::Config::Core::V3::ConfigSource.new(
37
- ads: Envoy::Config::Core::V3::AggregatedConfigSource.new
38
- )
38
+ eds_config: eds_config
39
39
  ),
40
40
  connect_timeout: duration(connect_timeout),
41
41
  health_checks: health_checks,
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "envoy/service/discovery/v3/discovery_pb"
7
+ require "protocol/grpc/interface"
8
+
9
+ require_relative "control_plane"
10
+ require_relative "discovery_service"
11
+
12
+ module Async
13
+ module GRPC
14
+ module XDS
15
+ # Serves Cluster Discovery Service requests from a {ControlPlane}.
16
+ class ClusterDiscoveryService < DiscoveryService
17
+ SERVICE_NAME = "envoy.service.cluster.v3.ClusterDiscoveryService"
18
+ RESOURCE_TYPE = ControlPlane::CLUSTER_TYPE
19
+
20
+ # The gRPC interface for cluster discovery.
21
+ class Interface < Protocol::GRPC::Interface
22
+ rpc :StreamClusters,
23
+ request_class: Envoy::Service::Discovery::V3::DiscoveryRequest,
24
+ response_class: Envoy::Service::Discovery::V3::DiscoveryResponse,
25
+ streaming: :bidirectional
26
+
27
+ rpc :DeltaClusters,
28
+ request_class: Envoy::Service::Discovery::V3::DeltaDiscoveryRequest,
29
+ response_class: Envoy::Service::Discovery::V3::DeltaDiscoveryResponse,
30
+ streaming: :bidirectional
31
+ end
32
+
33
+ # Initialize a Cluster Discovery Service.
34
+ # @parameter control_plane [ControlPlane] The control plane that provides clusters.
35
+ def initialize(control_plane)
36
+ super(Interface, SERVICE_NAME, control_plane, resource_type: RESOURCE_TYPE)
37
+ end
38
+
39
+ # Serve a state-of-the-world cluster discovery stream.
40
+ # @parameter input [Enumerable] The stream of discovery requests.
41
+ # @parameter output [Interface(:write)] The discovery response stream.
42
+ # @parameter call [Object] The gRPC call context.
43
+ # @asynchronous
44
+ def stream_clusters(input, output, call)
45
+ stream_resources(input, output)
46
+ end
47
+
48
+ # Reject a delta cluster discovery stream, which is not supported.
49
+ # @raises [Protocol::GRPC::Error] Always raised because delta xDS is not implemented.
50
+ def delta_clusters(input, output, call)
51
+ delta_resources
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "envoy/config/core/v3/config_source_pb"
7
+ require "envoy/config/core/v3/grpc_service_pb"
8
+
9
+ module Async
10
+ module GRPC
11
+ module XDS
12
+ # Builds Envoy xDS configuration sources.
13
+ module ConfigSource
14
+ extend self
15
+
16
+ # Build a configuration source that uses the global ADS server.
17
+ # @returns [Envoy::Config::Core::V3::ConfigSource] The ADS configuration source.
18
+ def ads
19
+ Envoy::Config::Core::V3::ConfigSource.new(
20
+ ads: Envoy::Config::Core::V3::AggregatedConfigSource.new
21
+ )
22
+ end
23
+
24
+ # Build a configuration source for a dedicated gRPC discovery service.
25
+ # @parameter cluster_name [String] The static Envoy cluster used to reach the management server.
26
+ # @returns [Envoy::Config::Core::V3::ConfigSource] The gRPC API configuration source.
27
+ def grpc(cluster_name)
28
+ Envoy::Config::Core::V3::ConfigSource.new(
29
+ resource_api_version: :V3,
30
+ api_config_source: Envoy::Config::Core::V3::ApiConfigSource.new(
31
+ api_type: :GRPC,
32
+ transport_api_version: :V3,
33
+ grpc_services: [
34
+ Envoy::Config::Core::V3::GrpcService.new(
35
+ envoy_grpc: Envoy::Config::Core::V3::GrpcService::EnvoyGrpc.new(
36
+ cluster_name: cluster_name.to_s
37
+ )
38
+ )
39
+ ]
40
+ )
41
+ )
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -19,7 +19,7 @@ require_relative "endpoint"
19
19
  module Async
20
20
  module GRPC
21
21
  module XDS
22
- # Maintains xDS resource snapshots and notifies ADS streams when resources change.
22
+ # Maintains xDS resource snapshots and notifies discovery streams when resources change.
23
23
  class ControlPlane
24
24
  CLUSTER_TYPE = Cluster::TYPE_URL
25
25
  ENDPOINT_TYPE = Endpoint::TYPE_URL
@@ -152,7 +152,7 @@ module Async
152
152
  end
153
153
 
154
154
  # Register a stream to receive resource-change notifications.
155
- # @parameter stream [Service::Stream] The stream to register.
155
+ # @parameter stream [Stream] The stream to register.
156
156
  def register_stream(stream)
157
157
  @mutex.synchronize do
158
158
  @streams.add(stream)
@@ -160,7 +160,7 @@ module Async
160
160
  end
161
161
 
162
162
  # Remove a registered stream.
163
- # @parameter stream [Service::Stream] The stream to remove.
163
+ # @parameter stream [Stream] The stream to remove.
164
164
  def remove_stream(stream)
165
165
  @mutex.synchronize do
166
166
  @streams.delete(stream)
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "async"
7
+ require "async/grpc/service"
8
+ require "protocol/grpc/error"
9
+ require "protocol/grpc/status"
10
+
11
+ require_relative "control_plane"
12
+ require_relative "stream"
13
+
14
+ module Async
15
+ module GRPC
16
+ module XDS
17
+ # Shared implementation for state-of-the-world xDS discovery services.
18
+ class DiscoveryService < Async::GRPC::Service
19
+ # Initialize a discovery service.
20
+ # @parameter interface [Class] The gRPC service interface.
21
+ # @parameter service_name [String] The fully qualified gRPC service name.
22
+ # @parameter control_plane [ControlPlane] The control plane that provides resources.
23
+ # @parameter resource_type [String | Nil] The fixed resource type, or `nil` for aggregated discovery.
24
+ def initialize(interface, service_name, control_plane, resource_type: nil)
25
+ super(interface, service_name)
26
+
27
+ @control_plane = control_plane
28
+ @resource_type = resource_type
29
+ end
30
+
31
+ # Serve a state-of-the-world discovery stream.
32
+ # @parameter input [Enumerable] The stream of discovery requests.
33
+ # @parameter output [Interface(:write)] The discovery response stream.
34
+ # @asynchronous
35
+ def stream_resources(input, output)
36
+ stream = Stream.new(@control_plane, output, resource_type: @resource_type)
37
+ @control_plane.register_stream(stream)
38
+
39
+ reader = Async::Task.current.async do
40
+ input.each do |request|
41
+ stream.request(request)
42
+ end
43
+ end
44
+
45
+ writer = Async::Task.current.async do
46
+ stream.run
47
+ end
48
+
49
+ reader.wait
50
+ ensure
51
+ stream&.close
52
+ reader&.stop
53
+ writer&.stop
54
+ @control_plane.remove_stream(stream) if stream
55
+ end
56
+
57
+ # Reject a delta discovery stream, which is not supported.
58
+ # @raises [Protocol::GRPC::Error] Always raised because delta xDS is not implemented.
59
+ def delta_resources
60
+ raise Protocol::GRPC::Error.new(
61
+ Protocol::GRPC::Status::UNIMPLEMENTED,
62
+ "Delta xDS is not implemented."
63
+ )
64
+ end
65
+
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "envoy/service/discovery/v3/discovery_pb"
7
+ require "protocol/grpc/interface"
8
+
9
+ require_relative "control_plane"
10
+ require_relative "discovery_service"
11
+
12
+ module Async
13
+ module GRPC
14
+ module XDS
15
+ # Serves Endpoint Discovery Service requests from a {ControlPlane}.
16
+ class EndpointDiscoveryService < DiscoveryService
17
+ SERVICE_NAME = "envoy.service.endpoint.v3.EndpointDiscoveryService"
18
+ RESOURCE_TYPE = ControlPlane::ENDPOINT_TYPE
19
+
20
+ # The gRPC interface for endpoint discovery.
21
+ class Interface < Protocol::GRPC::Interface
22
+ rpc :StreamEndpoints,
23
+ request_class: Envoy::Service::Discovery::V3::DiscoveryRequest,
24
+ response_class: Envoy::Service::Discovery::V3::DiscoveryResponse,
25
+ streaming: :bidirectional
26
+
27
+ rpc :DeltaEndpoints,
28
+ request_class: Envoy::Service::Discovery::V3::DeltaDiscoveryRequest,
29
+ response_class: Envoy::Service::Discovery::V3::DeltaDiscoveryResponse,
30
+ streaming: :bidirectional
31
+ end
32
+
33
+ # Initialize an Endpoint Discovery Service.
34
+ # @parameter control_plane [ControlPlane] The control plane that provides endpoint assignments.
35
+ def initialize(control_plane)
36
+ super(Interface, SERVICE_NAME, control_plane, resource_type: RESOURCE_TYPE)
37
+ end
38
+
39
+ # Serve a state-of-the-world endpoint discovery stream.
40
+ # @parameter input [Enumerable] The stream of discovery requests.
41
+ # @parameter output [Interface(:write)] The discovery response stream.
42
+ # @parameter call [Object] The gRPC call context.
43
+ # @asynchronous
44
+ def stream_endpoints(input, output, call)
45
+ stream_resources(input, output)
46
+ end
47
+
48
+ # Reject a delta endpoint discovery stream, which is not supported.
49
+ # @raises [Protocol::GRPC::Error] Always raised because delta xDS is not implemented.
50
+ def delta_endpoints(input, output, call)
51
+ delta_resources
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end