async-grpc-xds 0.0.1 → 0.2.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/lib/async/grpc/xds/ads_stream.rb +4 -0
  4. data/lib/async/grpc/xds/client_side_weighted_round_robin.rb +53 -0
  5. data/lib/async/grpc/xds/cluster.rb +71 -0
  6. data/lib/async/grpc/xds/control_plane.rb +47 -6
  7. data/lib/async/grpc/xds/discovery_client.rb +12 -2
  8. data/lib/async/grpc/xds/endpoint.rb +102 -0
  9. data/lib/async/grpc/xds/resource_cache.rb +1 -0
  10. data/lib/async/grpc/xds/resources.rb +9 -0
  11. data/lib/async/grpc/xds/server.rb +7 -0
  12. data/lib/async/grpc/xds/service.rb +24 -0
  13. data/lib/async/grpc/xds/version.rb +4 -1
  14. data/lib/async/grpc/xds.rb +3 -1
  15. data/lib/envoy/config/common/mutation_rules/v3/mutation_rules_pb.rb +33 -0
  16. data/lib/envoy/config/route/v3/route_components_pb.rb +108 -0
  17. data/lib/envoy/extensions/load_balancing_policies/client_side_weighted_round_robin/v3/client_side_weighted_round_robin_pb.rb +30 -0
  18. data/lib/envoy/extensions/load_balancing_policies/common/v3/common_pb.rb +41 -0
  19. data/lib/envoy/type/tracing/v3/custom_tag_pb.rb +31 -0
  20. data/lib/xds/data/orca/v3/orca_load_report_pb.rb +24 -0
  21. data/lib/xds/service/orca/v3/open_rca_service.rb +23 -0
  22. data/lib/xds/service/orca/v3/orca_pb.rb +25 -0
  23. data/proto/envoy/config/common/mutation_rules/v3/mutation_rules.proto +113 -0
  24. data/proto/envoy/extensions/load_balancing_policies/client_side_weighted_round_robin/v3/client_side_weighted_round_robin.proto +96 -0
  25. data/proto/envoy/extensions/load_balancing_policies/common/v3/common.proto +188 -0
  26. data/proto/envoy/type/tracing/v3/custom_tag.proto +109 -0
  27. data/proto/readme.md +3 -0
  28. data/proto/xds/data/orca/v3/orca_load_report.proto +58 -0
  29. data/proto/xds/service/orca/v3/orca.proto +36 -0
  30. data/readme.md +16 -2
  31. data/releases.md +12 -0
  32. data/xds/readme.md +2 -29
  33. data/xds/test/async/grpc/xds/control_plane.rb +5 -6
  34. data.tar.gz.sig +0 -0
  35. metadata +18 -2
  36. metadata.gz.sig +0 -0
  37. data/lib/async/grpc/xds/resource_builder.rb +0 -138
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fba6859fd43ebb2de75d228d4511125d1c7b36f08321fdda99eabf573fea900a
4
- data.tar.gz: 2bca6eb3ec749a434703f0296f6a12c14e6d75f5518930618355e3abd8b37d83
3
+ metadata.gz: 49d59879d476dd4d2e5f134a637371d29e1d0f352b35f22494630e424919e64d
4
+ data.tar.gz: 8a6898813f97f84ef507a4bd001f8fc224912d297296cc3439f523853bf43f44
5
5
  SHA512:
6
- metadata.gz: 78f056024e2e5c0cfbf1e1ca67acf9edd9b6ca357659911cbd733eeb2fde2ff0d73c169724a2caebd1ea1860bfaa09ce07784d950a5e9908c9c1a07c2021e81d
7
- data.tar.gz: 4e9a3490cfb4e8918994301ef300a2780929dffd31dec5278bf690f976e37681e9476f58899fc668f9799d3d9398fbc740654d42599a7ee8b01c265a47a70e7b
6
+ metadata.gz: 0f19714dcf5dd780cdfa9e7df25dcc52aa921ece5f168d10b0e8b2b34896a171098f4c907590e0ca27f8e43bf01e0e737faeb41bb2a746770762bb7dd807d1dd
7
+ data.tar.gz: a5c9b8d7e1cfff638f626c03f7643c5e27385365e30cb9bdaaf7802c3ba8655fd71dff1523506f2cb96ab70d9230a86ec9040bccf1c446b4de9ea15eb6788990
checksums.yaml.gz.sig CHANGED
Binary file
@@ -24,6 +24,10 @@ module Async
24
24
  end
25
25
  end
26
26
 
27
+ # Initialize an ADS stream.
28
+ # @parameter client [Async::GRPC::Client] The gRPC client used to open the stream.
29
+ # @parameter node [Envoy::Config::Core::V3::Node] The xDS node identity.
30
+ # @parameter delegate [Delegate] The object that receives stream events.
27
31
  def initialize(client, node, delegate:)
28
32
  @client = client
29
33
  @node = node
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "google/protobuf/any_pb"
7
+ require "google/protobuf/duration_pb"
8
+ require "google/protobuf/wrappers_pb"
9
+
10
+ require "envoy/config/cluster/v3/cluster_pb"
11
+ require "envoy/extensions/load_balancing_policies/client_side_weighted_round_robin/v3/client_side_weighted_round_robin_pb"
12
+
13
+ module Async
14
+ module GRPC
15
+ module XDS
16
+ # Builds Envoy's client-side weighted-round-robin policy with out-of-band ORCA reporting.
17
+ module ClientSideWeightedRoundRobin
18
+ # Build the typed load-balancing policy.
19
+ # @parameter port [Integer] The alternative TCP port hosting the ORCA service.
20
+ # @parameter reporting_period [Numeric] The requested ORCA reporting interval in seconds.
21
+ # @returns [Envoy::Config::Cluster::V3::LoadBalancingPolicy] The typed load-balancing policy.
22
+ def self.build(port, reporting_period: 1)
23
+ seconds = reporting_period.to_i
24
+ duration = Google::Protobuf::Duration.new(
25
+ seconds: seconds,
26
+ nanos: ((reporting_period.to_f - seconds) * 1_000_000_000).to_i
27
+ )
28
+ configuration = Envoy::Extensions::LoadBalancingPolicies::ClientSideWeightedRoundRobin::V3::ClientSideWeightedRoundRobin.new(
29
+ enable_oob_load_report: Google::Protobuf::BoolValue.new(value: true),
30
+ oob_reporting_period: duration,
31
+ oob_reporting_config: Envoy::Extensions::LoadBalancingPolicies::Common::V3::OrcaOobReportingConfig.new(
32
+ port_value: Integer(port)
33
+ )
34
+ )
35
+
36
+ Envoy::Config::Cluster::V3::LoadBalancingPolicy.new(
37
+ policies: [
38
+ Envoy::Config::Cluster::V3::LoadBalancingPolicy::Policy.new(
39
+ typed_extension_config: Envoy::Config::Core::V3::TypedExtensionConfig.new(
40
+ name: "envoy.load_balancing_policies.client_side_weighted_round_robin",
41
+ typed_config: Google::Protobuf::Any.new(
42
+ type_url: "type.googleapis.com/#{configuration.class.descriptor.name}",
43
+ value: configuration.to_proto
44
+ )
45
+ )
46
+ )
47
+ ]
48
+ )
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "google/protobuf/duration_pb"
7
+
8
+ require "envoy/config/cluster/v3/cluster_pb"
9
+ require "envoy/config/core/v3/config_source_pb"
10
+ require "envoy/config/core/v3/protocol_pb"
11
+
12
+ module Async
13
+ module GRPC
14
+ module XDS
15
+ # Builds Envoy cluster resources.
16
+ module Cluster
17
+ extend self
18
+
19
+ TYPE_URL = "type.googleapis.com/envoy.config.cluster.v3.Cluster"
20
+
21
+ # Build an EDS cluster resource.
22
+ # @parameter name [String] The cluster name.
23
+ # @parameter service_name [String] The EDS service name.
24
+ # @parameter load_balancing_policy [Envoy::Config::Cluster::V3::LoadBalancingPolicy | Nil] The typed Envoy load-balancing policy.
25
+ # @parameter connect_timeout [Numeric] The upstream connection timeout in seconds.
26
+ # @parameter protocol [Symbol] The canonical upstream protocol, either `:http1` or `:http2`.
27
+ # @returns [Envoy::Config::Cluster::V3::Cluster] The generated cluster resource.
28
+ # @raises [ArgumentError] If the upstream protocol is unsupported.
29
+ def build(name, service_name: name, load_balancing_policy: nil, connect_timeout: 5, protocol: :http2)
30
+ options = {
31
+ name: name.to_s,
32
+ type: Envoy::Config::Cluster::V3::Cluster::DiscoveryType::EDS,
33
+ eds_cluster_config: Envoy::Config::Cluster::V3::Cluster::EdsClusterConfig.new(
34
+ service_name: service_name.to_s,
35
+ eds_config: Envoy::Config::Core::V3::ConfigSource.new(
36
+ ads: Envoy::Config::Core::V3::AggregatedConfigSource.new
37
+ )
38
+ ),
39
+ connect_timeout: duration(connect_timeout),
40
+ }
41
+
42
+ options[:load_balancing_policy] = load_balancing_policy if load_balancing_policy
43
+
44
+ case protocol
45
+ when :http1
46
+ # Envoy uses HTTP/1 by default.
47
+ when :http2
48
+ options[:http2_protocol_options] = Envoy::Config::Core::V3::Http2ProtocolOptions.new
49
+ else
50
+ raise ArgumentError, "Unsupported upstream protocol: #{protocol.inspect}"
51
+ end
52
+
53
+ Envoy::Config::Cluster::V3::Cluster.new(**options)
54
+ end
55
+
56
+ private
57
+
58
+ # Convert seconds to a protobuf duration.
59
+ # @parameter seconds [Numeric] The duration in seconds.
60
+ # @returns [Google::Protobuf::Duration] The protobuf duration.
61
+ # @private
62
+ def duration(seconds)
63
+ whole_seconds = seconds.to_i
64
+ nanos = ((seconds.to_f - whole_seconds) * 1_000_000_000).to_i
65
+
66
+ Google::Protobuf::Duration.new(seconds: whole_seconds, nanos: nanos)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -10,17 +10,22 @@ require "async/queue"
10
10
 
11
11
  require "envoy/config/core/v3/base_pb"
12
12
  require "envoy/service/discovery/v3/discovery_pb"
13
+ require "google/protobuf/any_pb"
14
+ require "google/protobuf/well_known_types"
13
15
 
14
- require_relative "resource_builder"
16
+ require_relative "cluster"
17
+ require_relative "endpoint"
15
18
 
16
19
  module Async
17
20
  module GRPC
18
21
  module XDS
19
22
  # Maintains xDS resource snapshots and notifies ADS streams when resources change.
20
23
  class ControlPlane
21
- CLUSTER_TYPE = ResourceBuilder::CLUSTER_TYPE
22
- ENDPOINT_TYPE = ResourceBuilder::ENDPOINT_TYPE
24
+ CLUSTER_TYPE = Cluster::TYPE_URL
25
+ ENDPOINT_TYPE = Endpoint::TYPE_URL
23
26
 
27
+ # Initialize an empty control plane.
28
+ # @parameter identifier [String] The identifier reported in discovery responses.
24
29
  def initialize(identifier: "async-grpc-xds")
25
30
  @identifier = identifier
26
31
  @resources = Hash.new{|hash, type_url| hash[type_url] = {}}
@@ -31,27 +36,42 @@ module Async
31
36
 
32
37
  attr :identifier
33
38
 
39
+ # Add or replace a cluster resource.
40
+ # @parameter name [String] The cluster name.
41
+ # @parameter resource [Envoy::Config::Cluster::V3::Cluster | Nil] An existing cluster resource, or `nil` to build one from `options`.
42
+ # @parameter options [Hash] Options forwarded to {Cluster.build}.
34
43
  def update_cluster(name, resource = nil, **options)
35
- resource ||= ResourceBuilder.cluster(name, **options)
44
+ resource ||= Cluster.build(name, **options)
36
45
  update_resource(CLUSTER_TYPE, name.to_s, resource)
37
46
  end
38
47
 
48
+ # Add or replace the endpoint assignment for a cluster.
49
+ # @parameter cluster_name [String] The cluster name.
50
+ # @parameter endpoints [Array(Hash)] The normalized endpoint states.
39
51
  def update_endpoints(cluster_name, endpoints)
40
52
  update_resource(
41
53
  ENDPOINT_TYPE,
42
54
  cluster_name.to_s,
43
- ResourceBuilder.cluster_load_assignment(cluster_name, endpoints)
55
+ Endpoint.build(cluster_name, endpoints)
44
56
  )
45
57
  end
46
58
 
59
+ # Remove a cluster resource.
60
+ # @parameter name [String] The cluster name.
47
61
  def remove_cluster(name)
48
62
  remove_resource(CLUSTER_TYPE, name.to_s)
49
63
  end
50
64
 
65
+ # Remove the endpoint assignment for a cluster.
66
+ # @parameter cluster_name [String] The cluster name.
51
67
  def remove_endpoints(cluster_name)
52
68
  remove_resource(ENDPOINT_TYPE, cluster_name.to_s)
53
69
  end
54
70
 
71
+ # Add or replace an xDS resource and notify subscribed streams.
72
+ # @parameter type_url [String] The xDS resource type URL.
73
+ # @parameter name [String] The resource name.
74
+ # @parameter resource [Google::Protobuf::MessageExts] The protobuf resource.
55
75
  def update_resource(type_url, name, resource)
56
76
  notify = false
57
77
 
@@ -64,6 +84,9 @@ module Async
64
84
  notify_streams(type_url) if notify
65
85
  end
66
86
 
87
+ # Remove an xDS resource and notify subscribed streams.
88
+ # @parameter type_url [String] The xDS resource type URL.
89
+ # @parameter name [String] The resource name.
67
90
  def remove_resource(type_url, name)
68
91
  notify = false
69
92
 
@@ -77,12 +100,19 @@ module Async
77
100
  notify_streams(type_url) if notify
78
101
  end
79
102
 
103
+ # Get the available resource names for a type.
104
+ # @parameter type_url [String] The xDS resource type URL.
105
+ # @returns [Array(String)] The resource names.
80
106
  def resource_names(type_url)
81
107
  @mutex.synchronize do
82
108
  @resources[type_url].keys
83
109
  end
84
110
  end
85
111
 
112
+ # Get resources of a given type.
113
+ # @parameter type_url [String] The xDS resource type URL.
114
+ # @parameter names [Array(String) | Nil] The requested resource names, or `nil` for all resources.
115
+ # @returns [Array(Google::Protobuf::MessageExts)] The matching resources.
86
116
  def resources(type_url, names = nil)
87
117
  @mutex.synchronize do
88
118
  resources = @resources[type_url]
@@ -95,31 +125,42 @@ module Async
95
125
  end
96
126
  end
97
127
 
128
+ # Get the current version for a resource type.
129
+ # @parameter type_url [String] The xDS resource type URL.
130
+ # @returns [String] The monotonically increasing version.
98
131
  def version(type_url)
99
132
  @mutex.synchronize do
100
133
  @versions[type_url].to_s
101
134
  end
102
135
  end
103
136
 
137
+ # Build a discovery response for a resource type.
138
+ # @parameter type_url [String] The xDS resource type URL.
139
+ # @parameter names [Array(String) | Nil] The requested resource names, or `nil` for all resources.
140
+ # @returns [Envoy::Service::Discovery::V3::DiscoveryResponse] The current discovery response.
104
141
  def response(type_url, names = nil)
105
142
  resources = self.resources(type_url, names)
106
143
  version = self.version(type_url)
107
144
 
108
145
  Envoy::Service::Discovery::V3::DiscoveryResponse.new(
109
146
  version_info: version,
110
- resources: resources.map{|resource| ResourceBuilder.pack(resource)},
147
+ resources: resources.map{|resource| Google::Protobuf::Any.pack(resource)},
111
148
  type_url: type_url,
112
149
  nonce: "#{type_url}:#{version}:#{SecureRandom.hex(8)}",
113
150
  control_plane: Envoy::Config::Core::V3::ControlPlane.new(identifier: @identifier)
114
151
  )
115
152
  end
116
153
 
154
+ # Register a stream to receive resource-change notifications.
155
+ # @parameter stream [Service::Stream] The stream to register.
117
156
  def register_stream(stream)
118
157
  @mutex.synchronize do
119
158
  @streams.add(stream)
120
159
  end
121
160
  end
122
161
 
162
+ # Remove a registered stream.
163
+ # @parameter stream [Service::Stream] The stream to remove.
123
164
  def remove_stream(stream)
124
165
  @mutex.synchronize do
125
166
  @streams.delete(stream)
@@ -16,6 +16,9 @@ require "envoy/config/cluster/v3/cluster_pb"
16
16
  require "envoy/config/endpoint/v3/endpoint_pb"
17
17
  require "google/protobuf/any_pb"
18
18
 
19
+ require_relative "cluster"
20
+ require_relative "endpoint"
21
+
19
22
  module Async
20
23
  module GRPC
21
24
  module XDS
@@ -27,8 +30,8 @@ module Async
27
30
  # xDS API type URLs (v3 API)
28
31
  LISTENER_TYPE = "type.googleapis.com/envoy.config.listener.v3.Listener"
29
32
  ROUTE_TYPE = "type.googleapis.com/envoy.config.route.v3.RouteConfiguration"
30
- CLUSTER_TYPE = "type.googleapis.com/envoy.config.cluster.v3.Cluster"
31
- ENDPOINT_TYPE = "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment"
33
+ CLUSTER_TYPE = Cluster::TYPE_URL
34
+ ENDPOINT_TYPE = Endpoint::TYPE_URL
32
35
  SECRET_TYPE = "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret"
33
36
 
34
37
  # Initialize xDS discovery client
@@ -152,15 +155,22 @@ module Async
152
155
  # ADSStream::Delegate interface - must be public for ADSStream to call
153
156
  public
154
157
 
158
+ # Record that an ADS stream has opened and unblock pending subscriptions.
159
+ # @parameter stream [ADSStream] The opened stream.
155
160
  def stream_opened(stream)
156
161
  @mutex.synchronize{@ads_stream = stream}
157
162
  @stream_ready_promise&.resolve(stream)
158
163
  end
159
164
 
165
+ # Record that an ADS stream has closed.
166
+ # @parameter stream [ADSStream] The closed stream.
160
167
  def stream_closed(stream)
161
168
  @mutex.synchronize{@ads_stream = nil}
162
169
  end
163
170
 
171
+ # Process a discovery response received by an ADS stream.
172
+ # @parameter response [Envoy::Service::Discovery::V3::DiscoveryResponse] The received response.
173
+ # @parameter stream [ADSStream] The stream that received the response.
164
174
  def discovery_response(response, stream)
165
175
  process_response(response, stream)
166
176
  end
@@ -0,0 +1,102 @@
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/address_pb"
7
+ require "envoy/config/endpoint/v3/endpoint_pb"
8
+ require "envoy/config/endpoint/v3/endpoint_components_pb"
9
+
10
+ module Async
11
+ module GRPC
12
+ module XDS
13
+ # Builds Envoy endpoint resources.
14
+ module Endpoint
15
+ extend self
16
+
17
+ TYPE_URL = "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment"
18
+
19
+ # Build an EDS cluster load assignment from normalized endpoint state.
20
+ # @parameter cluster_name [String] The cluster name.
21
+ # @parameter endpoints [Array(Hash)] The endpoints, each containing `:addresses` and `:healthy`.
22
+ # @returns [Envoy::Config::Endpoint::V3::ClusterLoadAssignment] The generated endpoint resource.
23
+ def build(cluster_name, endpoints)
24
+ Envoy::Config::Endpoint::V3::ClusterLoadAssignment.new(
25
+ cluster_name: cluster_name.to_s,
26
+ endpoints: [
27
+ Envoy::Config::Endpoint::V3::LocalityLbEndpoints.new(
28
+ lb_endpoints: endpoints.map{|endpoint| load_balancer_endpoint(endpoint)}
29
+ )
30
+ ]
31
+ )
32
+ end
33
+
34
+ private
35
+
36
+ # Build an Envoy load-balancer endpoint from normalized endpoint state.
37
+ # @parameter endpoint [Hash] The endpoint containing `:addresses` and `:healthy`.
38
+ # @returns [Envoy::Config::Endpoint::V3::LbEndpoint] The generated load-balancer endpoint.
39
+ # @raises [KeyError] If required endpoint state is missing.
40
+ # @raises [ArgumentError] If the endpoint has no addresses.
41
+ # @private
42
+ def load_balancer_endpoint(endpoint)
43
+ addresses, healthy = endpoint.fetch_values(:addresses, :healthy)
44
+ raise ArgumentError, "An endpoint requires at least one address!" if addresses.empty?
45
+
46
+ address, *additional_addresses = addresses
47
+
48
+ Envoy::Config::Endpoint::V3::LbEndpoint.new(
49
+ endpoint: Envoy::Config::Endpoint::V3::Endpoint.new(
50
+ address: build_address(address),
51
+ hostname: endpoint[:hostname],
52
+ additional_addresses: additional_addresses.map do |additional_address|
53
+ Envoy::Config::Endpoint::V3::Endpoint::AdditionalAddress.new(
54
+ address: build_address(additional_address)
55
+ )
56
+ end
57
+ ),
58
+ health_status: health_status_value(healthy)
59
+ )
60
+ end
61
+
62
+ # Build an Envoy address from a normalized IP or Unix address.
63
+ # @parameter address [Hash] An IP `:address` and `:port`, or a Unix `:path`.
64
+ # @returns [Envoy::Config::Core::V3::Address] The generated Envoy address.
65
+ # @raises [KeyError] If required IP address state is missing.
66
+ # @private
67
+ def build_address(address)
68
+ if path = address[:path]
69
+ Envoy::Config::Core::V3::Address.new(
70
+ pipe: Envoy::Config::Core::V3::Pipe.new(path: path)
71
+ )
72
+ else
73
+ Envoy::Config::Core::V3::Address.new(
74
+ socket_address: Envoy::Config::Core::V3::SocketAddress.new(
75
+ protocol: Envoy::Config::Core::V3::SocketAddress::Protocol::TCP,
76
+ address: address.fetch(:address),
77
+ port_value: address.fetch(:port)
78
+ )
79
+ )
80
+ end
81
+ end
82
+
83
+ # Convert an endpoint health status to its Envoy enum value.
84
+ # @parameter healthy [Boolean | Symbol | String] The normalized health status.
85
+ # @returns [Integer] The Envoy health-status enum value.
86
+ # @private
87
+ def health_status_value(healthy)
88
+ case healthy
89
+ when :healthy, :HEALTHY, "healthy", "HEALTHY", true
90
+ Envoy::Config::Core::V3::HealthStatus::HEALTHY
91
+ when :unhealthy, :UNHEALTHY, "unhealthy", "UNHEALTHY", false
92
+ Envoy::Config::Core::V3::HealthStatus::UNHEALTHY
93
+ when :degraded, :DEGRADED, "degraded", "DEGRADED"
94
+ Envoy::Config::Core::V3::HealthStatus::DEGRADED
95
+ else
96
+ Envoy::Config::Core::V3::HealthStatus::UNKNOWN
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -9,6 +9,7 @@ module Async
9
9
  # Caches discovered xDS resources
10
10
  # Thread-safe cache for clusters and endpoints
11
11
  class ResourceCache
12
+ # Initialize an empty resource cache.
12
13
  def initialize
13
14
  @clusters = {}
14
15
  @endpoints = {}
@@ -6,6 +6,7 @@
6
6
  module Async
7
7
  module GRPC
8
8
  module XDS
9
+ # @namespace
9
10
  module Resources
10
11
  # Represents a discovered cluster
11
12
  # Based on envoy.config.cluster.v3.Cluster
@@ -40,6 +41,8 @@ module Async
40
41
  new(proto)
41
42
  end
42
43
 
44
+ # Determine whether this cluster uses the Endpoint Discovery Service.
45
+ # @returns [Boolean] `true` if the cluster uses EDS.
43
46
  def eds_cluster?
44
47
  @type == :EDS
45
48
  end
@@ -216,6 +219,8 @@ module Async
216
219
  class Endpoint
217
220
  attr_reader :address, :port, :health_status, :metadata
218
221
 
222
+ # Initialize an endpoint from a protobuf message or hash.
223
+ # @parameter load_balancer_endpoint [Envoy::Config::Endpoint::V3::LbEndpoint | Hash] The load-balancer endpoint representation.
219
224
  def initialize(load_balancer_endpoint)
220
225
  if load_balancer_endpoint.is_a?(Hash)
221
226
  endpoint_data = load_balancer_endpoint[:endpoint] || {}
@@ -235,10 +240,14 @@ module Async
235
240
  end
236
241
  end
237
242
 
243
+ # Determine whether this endpoint is eligible to receive traffic.
244
+ # @returns [Boolean] `true` if the endpoint is healthy or has unknown health.
238
245
  def healthy?
239
246
  @health_status == :HEALTHY || @health_status == :UNKNOWN
240
247
  end
241
248
 
249
+ # Build the HTTP URI for this endpoint.
250
+ # @returns [String] The endpoint URI.
242
251
  def uri
243
252
  # Use http for insecure/docker environments (gRPC h2c)
244
253
  scheme = ENV["XDS_ENDPOINT_SCHEME"] || "http"
@@ -14,6 +14,9 @@ module Async
14
14
  module XDS
15
15
  # Convenience wrapper for serving an xDS control plane over gRPC.
16
16
  class Server
17
+ # Initialize an xDS server.
18
+ # @parameter control_plane [ControlPlane] The control plane to serve.
19
+ # @parameter options [Hash] Default options forwarded to `Async::HTTP::Server`.
17
20
  def initialize(control_plane = ControlPlane.new, **options)
18
21
  @control_plane = control_plane
19
22
  @dispatcher = Async::GRPC::Dispatcher.new
@@ -24,6 +27,10 @@ module Async
24
27
  attr :control_plane
25
28
  attr :dispatcher
26
29
 
30
+ # Run the xDS server on an endpoint.
31
+ # @parameter endpoint [Async::HTTP::Endpoint] The endpoint to bind.
32
+ # @parameter options [Hash] Options forwarded to `Async::HTTP::Server`.
33
+ # @asynchronous
27
34
  def run(endpoint, **options)
28
35
  server = Async::HTTP::Server.new(@dispatcher, endpoint, **@options, **options)
29
36
  server.run
@@ -21,11 +21,18 @@ module Async
21
21
  class Service < Async::GRPC::Service
22
22
  SERVICE_NAME = "envoy.service.discovery.v3.AggregatedDiscoveryService"
23
23
 
24
+ # Initialize an Aggregated Discovery Service.
25
+ # @parameter control_plane [ControlPlane] The control plane that provides resources.
24
26
  def initialize(control_plane)
25
27
  super(Envoy::Service::Discovery::V3::AggregatedDiscoveryService, SERVICE_NAME)
26
28
  @control_plane = control_plane
27
29
  end
28
30
 
31
+ # Serve a state-of-the-world Aggregated Discovery Service stream.
32
+ # @parameter input [Enumerable] The stream of discovery requests.
33
+ # @parameter output [Interface(:write)] The discovery response stream.
34
+ # @parameter call [Object] The gRPC call context.
35
+ # @asynchronous
29
36
  def stream_aggregated_resources(input, output, call)
30
37
  stream = Stream.new(@control_plane, output)
31
38
  @control_plane.register_stream(stream)
@@ -48,6 +55,11 @@ module Async
48
55
  @control_plane.remove_stream(stream) if stream
49
56
  end
50
57
 
58
+ # Reject a delta Aggregated Discovery Service stream, which is not supported.
59
+ # @parameter input [Enumerable] The stream of delta discovery requests.
60
+ # @parameter output [Interface(:write)] The delta discovery response stream.
61
+ # @parameter call [Object] The gRPC call context.
62
+ # @raises [Protocol::GRPC::Error] Always raised because delta xDS is not implemented.
51
63
  def delta_aggregated_resources(input, output, call)
52
64
  raise Protocol::GRPC::Error.new(
53
65
  Protocol::GRPC::Status::UNIMPLEMENTED,
@@ -57,6 +69,9 @@ module Async
57
69
 
58
70
  # Represents one ADS stream and its subscribed resources.
59
71
  class Stream
72
+ # Initialize an ADS stream.
73
+ # @parameter control_plane [ControlPlane] The control plane that provides resources.
74
+ # @parameter output [Interface(:write)] The discovery response stream.
60
75
  def initialize(control_plane, output)
61
76
  @control_plane = control_plane
62
77
  @output = output
@@ -66,6 +81,8 @@ module Async
66
81
  @closed = false
67
82
  end
68
83
 
84
+ # Process a discovery request and update the stream's subscriptions.
85
+ # @parameter request [Envoy::Service::Discovery::V3::DiscoveryRequest] The discovery request.
69
86
  def request(request)
70
87
  return if request.type_url.nil? || request.type_url.empty?
71
88
 
@@ -83,10 +100,14 @@ module Async
83
100
  @queue << request.type_url
84
101
  end
85
102
 
103
+ # Schedule a resource type for delivery after it changes.
104
+ # @parameter type_url [String] The changed xDS resource type URL.
86
105
  def changed(type_url)
87
106
  @queue << type_url unless @closed
88
107
  end
89
108
 
109
+ # Deliver scheduled resource updates until the stream closes.
110
+ # @asynchronous
90
111
  def run
91
112
  until @closed
92
113
  type_url = @queue.dequeue
@@ -94,6 +115,8 @@ module Async
94
115
  end
95
116
  end
96
117
 
118
+ # Deliver the latest resource version for a subscribed type.
119
+ # @parameter type_url [String] The xDS resource type URL.
97
120
  def flush(type_url)
98
121
  names = @subscriptions[type_url]
99
122
  return unless names
@@ -106,6 +129,7 @@ module Async
106
129
  @versions[type_url] = version
107
130
  end
108
131
 
132
+ # Close the stream and stop waiting for changes.
109
133
  def close
110
134
  @closed = true
111
135
  @queue.close
@@ -3,10 +3,13 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2026, by Samuel Williams.
5
5
 
6
+ # @namespace
6
7
  module Async
8
+ # @namespace
7
9
  module GRPC
10
+ # @namespace
8
11
  module XDS
9
- VERSION = "0.0.1"
12
+ VERSION = "0.2.0"
10
13
  end
11
14
  end
12
15
  end
@@ -13,7 +13,9 @@ require_relative "xds/health_checker"
13
13
  require_relative "xds/load_balancer"
14
14
  require_relative "xds/context"
15
15
  require_relative "xds/client"
16
- require_relative "xds/resource_builder"
16
+ require_relative "xds/cluster"
17
+ require_relative "xds/endpoint"
18
+ require_relative "xds/client_side_weighted_round_robin"
17
19
  require_relative "xds/control_plane"
18
20
  require_relative "xds/service"
19
21
  require_relative "xds/server"
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
4
+ # source: envoy/config/common/mutation_rules/v3/mutation_rules.proto
5
+
6
+ require "google/protobuf"
7
+
8
+ require "envoy/config/core/v3/base_pb"
9
+ require "envoy/type/matcher/v3/regex_pb"
10
+ require "envoy/type/matcher/v3/string_pb"
11
+ require "google/protobuf/wrappers_pb"
12
+ require "udpa/annotations/status_pb"
13
+ require "validate/validate_pb"
14
+
15
+
16
+ descriptor_data = "\n:envoy/config/common/mutation_rules/v3/mutation_rules.proto\x12%envoy.config.common.mutation_rules.v3\x1a\x1f\x65nvoy/config/core/v3/base.proto\x1a!envoy/type/matcher/v3/regex.proto\x1a\"envoy/type/matcher/v3/string.proto\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x1dudpa/annotations/status.proto\x1a\x17validate/validate.proto\"\x9c\x03\n\x13HeaderMutationRules\x12\x35\n\x11\x61llow_all_routing\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12/\n\x0b\x61llow_envoy\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12\x33\n\x0f\x64isallow_system\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12\x30\n\x0c\x64isallow_all\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12=\n\x10\x61llow_expression\x18\x05 \x01(\x0b\x32#.envoy.type.matcher.v3.RegexMatcher\x12@\n\x13\x64isallow_expression\x18\x06 \x01(\x0b\x32#.envoy.type.matcher.v3.RegexMatcher\x12\x35\n\x11\x64isallow_is_error\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\"\xaf\x02\n\x0eHeaderMutation\x12\x1d\n\x06remove\x18\x01 \x01(\tB\x0b\xfa\x42\x08r\x06\xc0\x01\x02\xc8\x01\x00H\x00\x12\x39\n\x06\x61ppend\x18\x02 \x01(\x0b\x32\'.envoy.config.core.v3.HeaderValueOptionH\x00\x12^\n\x0fremove_on_match\x18\x03 \x01(\x0b\x32\x43.envoy.config.common.mutation_rules.v3.HeaderMutation.RemoveOnMatchH\x00\x1aT\n\rRemoveOnMatch\x12\x43\n\x0bkey_matcher\x18\x01 \x01(\x0b\x32$.envoy.type.matcher.v3.StringMatcherB\x08\xfa\x42\x05\x8a\x01\x02\x10\x01\x42\r\n\x06\x61\x63tion\x12\x03\xf8\x42\x01\x42\xb2\x01\n3io.envoyproxy.envoy.config.common.mutation_rules.v3B\x12MutationRulesProtoP\x01Z]github.com/envoyproxy/go-control-plane/envoy/config/common/mutation_rules/v3;mutation_rulesv3\xba\x80\xc8\xd1\x06\x02\x10\x02\x62\x06proto3"
17
+
18
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
19
+ pool.add_serialized_file(descriptor_data)
20
+
21
+ module Envoy
22
+ module Config
23
+ module Common
24
+ module MutationRules
25
+ module V3
26
+ HeaderMutationRules = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("envoy.config.common.mutation_rules.v3.HeaderMutationRules").msgclass
27
+ HeaderMutation = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("envoy.config.common.mutation_rules.v3.HeaderMutation").msgclass
28
+ HeaderMutation::RemoveOnMatch = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("envoy.config.common.mutation_rules.v3.HeaderMutation.RemoveOnMatch").msgclass
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end