async-grpc-xds 0.0.1

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 (239) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/fixtures/async/grpc/test_interface.rb +79 -0
  4. data/fixtures/async/grpc/test_message.rb +56 -0
  5. data/lib/async/grpc/xds/ads_stream.rb +70 -0
  6. data/lib/async/grpc/xds/client.rb +255 -0
  7. data/lib/async/grpc/xds/context.rb +201 -0
  8. data/lib/async/grpc/xds/control_plane.rb +143 -0
  9. data/lib/async/grpc/xds/discovery_client.rb +356 -0
  10. data/lib/async/grpc/xds/health_checker.rb +88 -0
  11. data/lib/async/grpc/xds/load_balancer.rb +196 -0
  12. data/lib/async/grpc/xds/resource_builder.rb +138 -0
  13. data/lib/async/grpc/xds/resource_cache.rb +55 -0
  14. data/lib/async/grpc/xds/resources.rb +270 -0
  15. data/lib/async/grpc/xds/server.rb +34 -0
  16. data/lib/async/grpc/xds/service.rb +117 -0
  17. data/lib/async/grpc/xds/version.rb +12 -0
  18. data/lib/async/grpc/xds.rb +42 -0
  19. data/lib/envoy/annotations/deprecation_pb.rb +19 -0
  20. data/lib/envoy/config/cluster/v3/circuit_breaker_pb.rb +31 -0
  21. data/lib/envoy/config/cluster/v3/cluster_pb.rb +80 -0
  22. data/lib/envoy/config/cluster/v3/filter_pb.rb +28 -0
  23. data/lib/envoy/config/cluster/v3/outlier_detection_pb.rb +29 -0
  24. data/lib/envoy/config/core/v3/address_pb.rb +38 -0
  25. data/lib/envoy/config/core/v3/backoff_pb.rb +27 -0
  26. data/lib/envoy/config/core/v3/base_pb.rb +68 -0
  27. data/lib/envoy/config/core/v3/cel_pb.rb +24 -0
  28. data/lib/envoy/config/core/v3/config_source_pb.rb +42 -0
  29. data/lib/envoy/config/core/v3/event_service_config_pb.rb +27 -0
  30. data/lib/envoy/config/core/v3/extension_pb.rb +26 -0
  31. data/lib/envoy/config/core/v3/grpc_method_list_pb.rb +27 -0
  32. data/lib/envoy/config/core/v3/grpc_service_pb.rb +45 -0
  33. data/lib/envoy/config/core/v3/health_check_pb.rb +47 -0
  34. data/lib/envoy/config/core/v3/http_service_pb.rb +27 -0
  35. data/lib/envoy/config/core/v3/http_uri_pb.rb +27 -0
  36. data/lib/envoy/config/core/v3/protocol_pb.rb +51 -0
  37. data/lib/envoy/config/core/v3/proxy_protocol_pb.rb +31 -0
  38. data/lib/envoy/config/core/v3/resolver_pb.rb +27 -0
  39. data/lib/envoy/config/core/v3/socket_cmsg_headers_pb.rb +25 -0
  40. data/lib/envoy/config/core/v3/socket_option_pb.rb +31 -0
  41. data/lib/envoy/config/core/v3/substitution_format_string_pb.rb +30 -0
  42. data/lib/envoy/config/core/v3/udp_socket_config_pb.rb +26 -0
  43. data/lib/envoy/config/endpoint/v3/endpoint_components_pb.rb +40 -0
  44. data/lib/envoy/config/endpoint/v3/endpoint_pb.rb +32 -0
  45. data/lib/envoy/config/endpoint/v3/load_report_pb.rb +36 -0
  46. data/lib/envoy/service/discovery/v3/ads_pb.rb +26 -0
  47. data/lib/envoy/service/discovery/v3/aggregated_discovery_service.rb +64 -0
  48. data/lib/envoy/service/discovery/v3/discovery_pb.rb +42 -0
  49. data/lib/envoy/type/matcher/v3/address_pb.rb +25 -0
  50. data/lib/envoy/type/matcher/v3/filter_state_pb.rb +27 -0
  51. data/lib/envoy/type/matcher/v3/http_inputs_pb.rb +29 -0
  52. data/lib/envoy/type/matcher/v3/metadata_pb.rb +28 -0
  53. data/lib/envoy/type/matcher/v3/node_pb.rb +27 -0
  54. data/lib/envoy/type/matcher/v3/number_pb.rb +27 -0
  55. data/lib/envoy/type/matcher/v3/path_pb.rb +27 -0
  56. data/lib/envoy/type/matcher/v3/regex_pb.rb +30 -0
  57. data/lib/envoy/type/matcher/v3/status_code_input_pb.rb +25 -0
  58. data/lib/envoy/type/matcher/v3/string_pb.rb +29 -0
  59. data/lib/envoy/type/matcher/v3/struct_pb.rb +28 -0
  60. data/lib/envoy/type/matcher/v3/value_pb.rb +31 -0
  61. data/lib/envoy/type/metadata/v3/metadata_pb.rb +32 -0
  62. data/lib/envoy/type/v3/hash_policy_pb.rb +26 -0
  63. data/lib/envoy/type/v3/http_pb.rb +22 -0
  64. data/lib/envoy/type/v3/http_status_pb.rb +25 -0
  65. data/lib/envoy/type/v3/percent_pb.rb +26 -0
  66. data/lib/envoy/type/v3/range_pb.rb +25 -0
  67. data/lib/envoy/type/v3/ratelimit_strategy_pb.rb +28 -0
  68. data/lib/envoy/type/v3/ratelimit_unit_pb.rb +22 -0
  69. data/lib/envoy/type/v3/semantic_version_pb.rb +23 -0
  70. data/lib/envoy/type/v3/token_bucket_pb.rb +26 -0
  71. data/lib/envoy.rb +83 -0
  72. data/lib/google/protobuf/any_pb.rb +18 -0
  73. data/lib/google/protobuf/duration_pb.rb +18 -0
  74. data/lib/google/protobuf/empty_pb.rb +18 -0
  75. data/lib/google/protobuf/struct_pb.rb +21 -0
  76. data/lib/google/protobuf/timestamp_pb.rb +18 -0
  77. data/lib/google/protobuf/wrappers_pb.rb +26 -0
  78. data/lib/google/rpc/status_pb.rb +20 -0
  79. data/lib/udpa/annotations/migrate_pb.rb +22 -0
  80. data/lib/udpa/annotations/security_pb.rb +23 -0
  81. data/lib/udpa/annotations/sensitive_pb.rb +19 -0
  82. data/lib/udpa/annotations/status_pb.rb +21 -0
  83. data/lib/udpa/annotations/versioning_pb.rb +20 -0
  84. data/lib/validate/validate_pb.rb +43 -0
  85. data/lib/xds/annotations/v3/status_pb.rb +26 -0
  86. data/lib/xds/core/v3/authority_pb.rb +23 -0
  87. data/lib/xds/core/v3/cidr_pb.rb +24 -0
  88. data/lib/xds/core/v3/collection_entry_pb.rb +26 -0
  89. data/lib/xds/core/v3/context_params_pb.rb +22 -0
  90. data/lib/xds/core/v3/extension_pb.rb +23 -0
  91. data/lib/xds/core/v3/resource_locator_pb.rb +26 -0
  92. data/lib/xds/core/v3/resource_name_pb.rb +24 -0
  93. data/lib/xds/core/v3/resource_pb.rb +24 -0
  94. data/lib/xds/type/matcher/v3/domain_pb.rb +27 -0
  95. data/lib/xds/type/matcher/v3/http_inputs_pb.rb +22 -0
  96. data/lib/xds/type/matcher/v3/ip_pb.rb +28 -0
  97. data/lib/xds/type/matcher/v3/matcher_pb.rb +34 -0
  98. data/lib/xds/type/matcher/v3/range_pb.rb +31 -0
  99. data/lib/xds/type/matcher/v3/regex_pb.rb +25 -0
  100. data/lib/xds/type/matcher/v3/string_pb.rb +27 -0
  101. data/license.md +21 -0
  102. data/plan.md +156 -0
  103. data/proto/envoy/annotations/deprecation.proto +34 -0
  104. data/proto/envoy/annotations/resource.proto +19 -0
  105. data/proto/envoy/config/README.md +3 -0
  106. data/proto/envoy/config/cluster/v3/BUILD +18 -0
  107. data/proto/envoy/config/cluster/v3/circuit_breaker.proto +121 -0
  108. data/proto/envoy/config/cluster/v3/cluster.proto +1407 -0
  109. data/proto/envoy/config/cluster/v3/filter.proto +40 -0
  110. data/proto/envoy/config/cluster/v3/outlier_detection.proto +180 -0
  111. data/proto/envoy/config/core/v3/BUILD +16 -0
  112. data/proto/envoy/config/core/v3/address.proto +214 -0
  113. data/proto/envoy/config/core/v3/backoff.proto +37 -0
  114. data/proto/envoy/config/core/v3/base.proto +662 -0
  115. data/proto/envoy/config/core/v3/cel.proto +63 -0
  116. data/proto/envoy/config/core/v3/config_source.proto +283 -0
  117. data/proto/envoy/config/core/v3/event_service_config.proto +29 -0
  118. data/proto/envoy/config/core/v3/extension.proto +32 -0
  119. data/proto/envoy/config/core/v3/grpc_method_list.proto +33 -0
  120. data/proto/envoy/config/core/v3/grpc_service.proto +355 -0
  121. data/proto/envoy/config/core/v3/health_check.proto +443 -0
  122. data/proto/envoy/config/core/v3/http_service.proto +35 -0
  123. data/proto/envoy/config/core/v3/http_uri.proto +58 -0
  124. data/proto/envoy/config/core/v3/protocol.proto +807 -0
  125. data/proto/envoy/config/core/v3/proxy_protocol.proto +114 -0
  126. data/proto/envoy/config/core/v3/resolver.proto +36 -0
  127. data/proto/envoy/config/core/v3/socket_cmsg_headers.proto +28 -0
  128. data/proto/envoy/config/core/v3/socket_option.proto +108 -0
  129. data/proto/envoy/config/core/v3/substitution_format_string.proto +136 -0
  130. data/proto/envoy/config/core/v3/udp_socket_config.proto +32 -0
  131. data/proto/envoy/config/endpoint/v3/BUILD +16 -0
  132. data/proto/envoy/config/endpoint/v3/endpoint.proto +137 -0
  133. data/proto/envoy/config/endpoint/v3/endpoint_components.proto +229 -0
  134. data/proto/envoy/config/endpoint/v3/load_report.proto +220 -0
  135. data/proto/envoy/config/listener/v3/BUILD +18 -0
  136. data/proto/envoy/config/listener/v3/api_listener.proto +34 -0
  137. data/proto/envoy/config/listener/v3/listener.proto +455 -0
  138. data/proto/envoy/config/listener/v3/listener_components.proto +353 -0
  139. data/proto/envoy/config/listener/v3/quic_config.proto +108 -0
  140. data/proto/envoy/config/listener/v3/udp_listener_config.proto +52 -0
  141. data/proto/envoy/config/route/v3/BUILD +19 -0
  142. data/proto/envoy/config/route/v3/route.proto +172 -0
  143. data/proto/envoy/config/route/v3/route_components.proto +2918 -0
  144. data/proto/envoy/config/route/v3/scoped_route.proto +133 -0
  145. data/proto/envoy/extensions/transport_sockets/tls/v3/BUILD +14 -0
  146. data/proto/envoy/extensions/transport_sockets/tls/v3/cert.proto +12 -0
  147. data/proto/envoy/extensions/transport_sockets/tls/v3/common.proto +597 -0
  148. data/proto/envoy/extensions/transport_sockets/tls/v3/secret.proto +61 -0
  149. data/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto +366 -0
  150. data/proto/envoy/extensions/transport_sockets/tls/v3/tls_spiffe_validator_config.proto +67 -0
  151. data/proto/envoy/service/README.md +3 -0
  152. data/proto/envoy/service/discovery/v3/BUILD +13 -0
  153. data/proto/envoy/service/discovery/v3/ads.proto +44 -0
  154. data/proto/envoy/service/discovery/v3/discovery.proto +443 -0
  155. data/proto/envoy/type/BUILD +9 -0
  156. data/proto/envoy/type/hash_policy.proto +28 -0
  157. data/proto/envoy/type/http.proto +24 -0
  158. data/proto/envoy/type/http_status.proto +140 -0
  159. data/proto/envoy/type/matcher/v3/address.proto +22 -0
  160. data/proto/envoy/type/matcher/v3/filter_state.proto +33 -0
  161. data/proto/envoy/type/matcher/v3/http_inputs.proto +71 -0
  162. data/proto/envoy/type/matcher/v3/metadata.proto +110 -0
  163. data/proto/envoy/type/matcher/v3/node.proto +29 -0
  164. data/proto/envoy/type/matcher/v3/number.proto +33 -0
  165. data/proto/envoy/type/matcher/v3/path.proto +31 -0
  166. data/proto/envoy/type/matcher/v3/regex.proto +97 -0
  167. data/proto/envoy/type/matcher/v3/status_code_input.proto +23 -0
  168. data/proto/envoy/type/matcher/v3/string.proto +94 -0
  169. data/proto/envoy/type/matcher/v3/struct.proto +91 -0
  170. data/proto/envoy/type/matcher/v3/value.proto +80 -0
  171. data/proto/envoy/type/metadata/v3/metadata.proto +117 -0
  172. data/proto/envoy/type/percent.proto +52 -0
  173. data/proto/envoy/type/range.proto +43 -0
  174. data/proto/envoy/type/semantic_version.proto +24 -0
  175. data/proto/envoy/type/token_bucket.proto +36 -0
  176. data/proto/envoy/type/v3/BUILD +12 -0
  177. data/proto/envoy/type/v3/hash_policy.proto +43 -0
  178. data/proto/envoy/type/v3/http.proto +24 -0
  179. data/proto/envoy/type/v3/http_status.proto +199 -0
  180. data/proto/envoy/type/v3/percent.proto +57 -0
  181. data/proto/envoy/type/v3/range.proto +50 -0
  182. data/proto/envoy/type/v3/ratelimit_strategy.proto +79 -0
  183. data/proto/envoy/type/v3/ratelimit_unit.proto +37 -0
  184. data/proto/envoy/type/v3/semantic_version.proto +27 -0
  185. data/proto/envoy/type/v3/token_bucket.proto +39 -0
  186. data/proto/google/protobuf/any.proto +162 -0
  187. data/proto/google/protobuf/duration.proto +115 -0
  188. data/proto/google/protobuf/empty.proto +51 -0
  189. data/proto/google/protobuf/struct.proto +95 -0
  190. data/proto/google/protobuf/timestamp.proto +145 -0
  191. data/proto/google/protobuf/wrappers.proto +157 -0
  192. data/proto/google/rpc/status.proto +47 -0
  193. data/proto/readme.md +70 -0
  194. data/proto/udpa/annotations/migrate.proto +49 -0
  195. data/proto/udpa/annotations/security.proto +31 -0
  196. data/proto/udpa/annotations/sensitive.proto +14 -0
  197. data/proto/udpa/annotations/status.proto +34 -0
  198. data/proto/udpa/annotations/versioning.proto +17 -0
  199. data/proto/validate/validate.proto +862 -0
  200. data/proto/xds/annotations/v3/migrate.proto +46 -0
  201. data/proto/xds/annotations/v3/security.proto +30 -0
  202. data/proto/xds/annotations/v3/sensitive.proto +16 -0
  203. data/proto/xds/annotations/v3/status.proto +59 -0
  204. data/proto/xds/annotations/v3/versioning.proto +20 -0
  205. data/proto/xds/core/v3/authority.proto +22 -0
  206. data/proto/xds/core/v3/cidr.proto +25 -0
  207. data/proto/xds/core/v3/collection_entry.proto +55 -0
  208. data/proto/xds/core/v3/context_params.proto +23 -0
  209. data/proto/xds/core/v3/extension.proto +26 -0
  210. data/proto/xds/core/v3/resource.proto +29 -0
  211. data/proto/xds/core/v3/resource_locator.proto +118 -0
  212. data/proto/xds/core/v3/resource_name.proto +42 -0
  213. data/proto/xds/type/matcher/v3/cel.proto +37 -0
  214. data/proto/xds/type/matcher/v3/domain.proto +46 -0
  215. data/proto/xds/type/matcher/v3/http_inputs.proto +23 -0
  216. data/proto/xds/type/matcher/v3/ip.proto +53 -0
  217. data/proto/xds/type/matcher/v3/matcher.proto +144 -0
  218. data/proto/xds/type/matcher/v3/range.proto +69 -0
  219. data/proto/xds/type/matcher/v3/regex.proto +46 -0
  220. data/proto/xds/type/matcher/v3/string.proto +71 -0
  221. data/proto/xds/type/v3/cel.proto +77 -0
  222. data/proto/xds/type/v3/range.proto +40 -0
  223. data/proto/xds/type/v3/typed_struct.proto +44 -0
  224. data/readme.md +37 -0
  225. data/releases.md +5 -0
  226. data/xds/Dockerfile.backend +24 -0
  227. data/xds/Dockerfile.control-plane +22 -0
  228. data/xds/backend_server.rb +68 -0
  229. data/xds/docker-compose.yaml +89 -0
  230. data/xds/go.mod +22 -0
  231. data/xds/go.sum +82 -0
  232. data/xds/readme.md +122 -0
  233. data/xds/test/async/grpc/xds/client.rb +294 -0
  234. data/xds/test/async/grpc/xds/control_plane.rb +94 -0
  235. data/xds/test_server.go +355 -0
  236. data/xds/update_protos.sh +123 -0
  237. data.tar.gz.sig +0 -0
  238. metadata +386 -0
  239. metadata.gz.sig +2 -0
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Released under the MIT License.
5
+ # Copyright, 2026, by Samuel Williams.
6
+
7
+ require "async"
8
+ require "async/http/server"
9
+ require "async/http/endpoint"
10
+ require "async/grpc/dispatcher"
11
+ require "async/grpc/service"
12
+ require_relative "../fixtures/async/grpc/test_interface"
13
+
14
+ class TestBackendService < Async::GRPC::Service
15
+ def initialize(interface_class, service_name, backend_id)
16
+ super(interface_class, service_name)
17
+ @backend_id = backend_id
18
+ end
19
+
20
+ def unary_call(input, output, call)
21
+ request = input.read
22
+
23
+ # Include backend ID in response metadata (trailers)
24
+ call.response.headers["backend-id"] = @backend_id
25
+
26
+ response = Protocol::GRPC::Fixtures::TestMessage.new(
27
+ value: "Response from #{@backend_id}: #{request&.value || 'no value'}"
28
+ )
29
+
30
+ output.write(response)
31
+ end
32
+
33
+ def say_hello(input, output, call)
34
+ request = input.read
35
+
36
+ call.response.headers["backend-id"] = @backend_id
37
+
38
+ response = Protocol::GRPC::Fixtures::TestMessage.new(
39
+ value: "Hello from #{@backend_id}, #{request.value}!"
40
+ )
41
+
42
+ output.write(response)
43
+ end
44
+ end
45
+
46
+ port = ENV["PORT"] || "50051"
47
+ backend_id = ENV["BACKEND_ID"] || "backend-unknown"
48
+ service_name = ENV["SERVICE_NAME"] || "test.Service"
49
+
50
+ Async do
51
+ # Create gRPC dispatcher
52
+ dispatcher = Async::GRPC::Dispatcher.new
53
+ service = TestBackendService.new(Async::GRPC::Fixtures::TestInterface, service_name, backend_id)
54
+ dispatcher.register(service)
55
+
56
+ # Create endpoint (http for h2c - gRPC without TLS in docker)
57
+ endpoint = Async::HTTP::Endpoint.parse(
58
+ "http://0.0.0.0:#{port}",
59
+ protocol: Async::HTTP::Protocol::HTTP2
60
+ )
61
+
62
+ # Start server
63
+ server = Async::HTTP::Server.new(dispatcher, endpoint)
64
+
65
+ Console.info{"Starting backend server #{backend_id} on port #{port}"}
66
+
67
+ server.run
68
+ end
@@ -0,0 +1,89 @@
1
+ services:
2
+ # xDS control plane (using custom test server built with go-control-plane)
3
+ # This provides a simple xDS server for testing
4
+ xds-control-plane:
5
+ build:
6
+ context: .
7
+ dockerfile: Dockerfile.control-plane
8
+ ports:
9
+ - "18000:18000"
10
+ - "18001:18001"
11
+ environment:
12
+ - UPSTREAM=backend-1:50051,backend-2:50052,backend-3:50053
13
+ healthcheck:
14
+ test: ["CMD", "nc", "-z", "localhost", "18000"]
15
+ interval: 1s
16
+ timeout: 3s
17
+ retries: 30
18
+
19
+ # Backend gRPC server 1
20
+ backend-1:
21
+ build:
22
+ context: ..
23
+ dockerfile: xds/Dockerfile.backend
24
+ command: bundle exec ruby xds/backend_server.rb
25
+ environment:
26
+ - BUNDLE_WITHOUT=maintenance
27
+ - PORT=50051
28
+ - SERVICE_NAME=myservice
29
+ - BACKEND_ID=backend-1
30
+ ports:
31
+ - "50051:50051"
32
+ depends_on:
33
+ xds-control-plane:
34
+ condition: service_healthy
35
+
36
+ # Backend gRPC server 2
37
+ backend-2:
38
+ build:
39
+ context: ..
40
+ dockerfile: xds/Dockerfile.backend
41
+ command: bundle exec ruby xds/backend_server.rb
42
+ environment:
43
+ - BUNDLE_WITHOUT=maintenance
44
+ - PORT=50052
45
+ - SERVICE_NAME=myservice
46
+ - BACKEND_ID=backend-2
47
+ ports:
48
+ - "50052:50052"
49
+ depends_on:
50
+ xds-control-plane:
51
+ condition: service_healthy
52
+
53
+ # Backend gRPC server 3
54
+ backend-3:
55
+ build:
56
+ context: ..
57
+ dockerfile: xds/Dockerfile.backend
58
+ command: bundle exec ruby xds/backend_server.rb
59
+ environment:
60
+ - BUNDLE_WITHOUT=maintenance
61
+ - PORT=50053
62
+ - SERVICE_NAME=myservice
63
+ - BACKEND_ID=backend-3
64
+ ports:
65
+ - "50053:50053"
66
+ depends_on:
67
+ xds-control-plane:
68
+ condition: service_healthy
69
+
70
+ # Test runner
71
+ tests:
72
+ image: ruby:${RUBY_VERSION:-latest}
73
+ volumes:
74
+ - ../:/code
75
+ working_dir: /code
76
+ command: bash -c "bundle install && bundle exec sus xds/test"
77
+ environment:
78
+ - BUNDLE_GEMFILE=/code/gems.rb
79
+ - BUNDLE_WITHOUT=maintenance
80
+ - CONSOLE_OUTPUT=XTerm
81
+ - COVERAGE=${COVERAGE}
82
+ - XDS_ADMIN_URI=http://xds-control-plane:18001
83
+ - XDS_SERVER_URI=xds-control-plane:18000
84
+ - XDS_ENDPOINT_SCHEME=http
85
+ depends_on:
86
+ - xds-control-plane
87
+ - backend-1
88
+ - backend-2
89
+ - backend-3
data/xds/go.mod ADDED
@@ -0,0 +1,22 @@
1
+ module xds-test-server
2
+
3
+ go 1.21
4
+
5
+ require (
6
+ github.com/envoyproxy/go-control-plane v0.12.0
7
+ google.golang.org/grpc v1.60.0
8
+ )
9
+
10
+ require (
11
+ github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
12
+ github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 // indirect
13
+ github.com/envoyproxy/protoc-gen-validate v1.0.2 // indirect
14
+ github.com/golang/protobuf v1.5.3 // indirect
15
+ golang.org/x/net v0.17.0 // indirect
16
+ golang.org/x/sys v0.13.0 // indirect
17
+ golang.org/x/text v0.13.0 // indirect
18
+ google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 // indirect
19
+ google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 // indirect
20
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 // indirect
21
+ google.golang.org/protobuf v1.32.0 // indirect
22
+ )
data/xds/go.sum ADDED
@@ -0,0 +1,82 @@
1
+ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2
+ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
3
+ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
4
+ github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
5
+ github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
6
+ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
7
+ github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k=
8
+ github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
9
+ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
10
+ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
11
+ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
12
+ github.com/envoyproxy/go-control-plane v0.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjletLK6K0rbxyZI=
13
+ github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0=
14
+ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
15
+ github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA=
16
+ github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
17
+ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
18
+ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
19
+ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
20
+ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
21
+ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
22
+ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
23
+ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
24
+ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
25
+ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
26
+ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
27
+ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
28
+ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
29
+ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
30
+ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
31
+ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
32
+ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
33
+ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
34
+ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
35
+ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
36
+ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
37
+ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
38
+ golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
39
+ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
40
+ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
41
+ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
42
+ golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
43
+ golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
44
+ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
45
+ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
46
+ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
47
+ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
48
+ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
49
+ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
50
+ golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
51
+ golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
52
+ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
53
+ golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
54
+ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
55
+ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
56
+ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
57
+ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
58
+ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
59
+ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
60
+ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
61
+ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
62
+ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
63
+ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
64
+ google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 h1:SeZZZx0cP0fqUyA+oRzP9k7cSwJlvDFiROO72uwD6i0=
65
+ google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97/go.mod h1:t1VqOqqvce95G3hIDCT5FeO3YUc6Q4Oe24L/+rNMxRk=
66
+ google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 h1:W18sezcAYs+3tDZX4F80yctqa12jcP1PUS2gQu1zTPU=
67
+ google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97/go.mod h1:iargEX0SFPm3xcfMI0d1domjg0ZF4Aa0p2awqyxhvF0=
68
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0=
69
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY=
70
+ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
71
+ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
72
+ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
73
+ google.golang.org/grpc v1.60.0 h1:6FQAR0kM31P6MRdeluor2w2gPaS4SVNrD/DNTxrQ15k=
74
+ google.golang.org/grpc v1.60.0/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
75
+ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
76
+ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
77
+ google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
78
+ google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
79
+ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
80
+ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
81
+ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
82
+ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
data/xds/readme.md ADDED
@@ -0,0 +1,122 @@
1
+ # xDS Integration Tests
2
+
3
+ This directory contains Docker Compose configuration and test files for xDS integration testing, following the same pattern as `async-redis` (Sentinel and Cluster tests).
4
+
5
+ ## Setup
6
+
7
+ The Docker Compose setup includes:
8
+ - **xds-control-plane**: xDS control plane server (using go-control-plane)
9
+ - **backend-1, backend-2, backend-3**: Multiple gRPC backend servers
10
+ - **tests**: Test runner container
11
+
12
+ ## Running Tests
13
+
14
+ ### Start the environment
15
+
16
+ ```bash
17
+ cd xds
18
+ bundle install # generates gems.locked for Docker build
19
+ docker compose up -d
20
+ ```
21
+
22
+ ### Wait for services to be ready
23
+
24
+ ```bash
25
+ docker compose ps
26
+ ```
27
+
28
+ All services should show as "healthy" or "running".
29
+
30
+ ### Run tests
31
+
32
+ ```bash
33
+ # Run tests in docker compose
34
+ docker compose run --rm tests
35
+
36
+ # Or run tests locally (if services are accessible)
37
+ # Set XDS_SERVER_URI environment variable
38
+ export XDS_SERVER_URI=xds-control-plane:18000
39
+ bundle exec sus xds/test
40
+ ```
41
+
42
+ ### Cleanup
43
+
44
+ ```bash
45
+ docker compose down
46
+ ```
47
+
48
+ ## Test Structure
49
+
50
+ Tests are located in `xds/test/async/grpc/xds/` and follow the same pattern as other async-grpc tests:
51
+
52
+ - `client.rb`: Tests for `Async::GRPC::XDS::Client`
53
+ - Tests use `Sus::Fixtures::Async::ReactorContext` for async test support
54
+ - Tests connect to docker compose services using service names (e.g., `xds-control-plane:18000`)
55
+
56
+ ## Environment Variables
57
+
58
+ - `XDS_SERVER_URI`: xDS control plane server URI (default: `xds-control-plane:18000`)
59
+ - `RUBY_VERSION`: Ruby version for test container (default: `latest`)
60
+ - `COVERAGE`: Enable code coverage reporting
61
+
62
+ ## Backend Servers
63
+
64
+ The backend servers (`backend-1`, `backend-2`, `backend-3`) are simple gRPC servers that:
65
+ - Implement the test service interface
66
+ - Include backend ID in response metadata
67
+ - Can be used to test load balancing and failover
68
+
69
+ See `backend_server.rb` for implementation details.
70
+
71
+ ## Mock xDS Control Plane
72
+
73
+ For simpler unit testing, you can use a mock xDS server instead of the full Docker Compose setup. See the test files for examples of mocking xDS responses.
74
+
75
+ ## Troubleshooting
76
+
77
+ ### Services not starting
78
+
79
+ Check logs:
80
+ ```bash
81
+ docker compose logs xds-control-plane
82
+ docker compose logs backend-1
83
+ ```
84
+
85
+ ### Tests failing to connect
86
+
87
+ Ensure services are healthy:
88
+ ```bash
89
+ docker compose ps
90
+ ```
91
+
92
+ Check network connectivity:
93
+ ```bash
94
+ docker compose exec tests ping xds-control-plane
95
+ ```
96
+
97
+ ## Protobuf Setup
98
+
99
+ The xDS implementation uses Envoy protobuf definitions. Protos come from [envoyproxy/envoy](https://github.com/envoyproxy/envoy) (`api/`) or [envoyproxy/data-plane-api](https://github.com/envoyproxy/data-plane-api). Use **xDS v3** (v2 is deprecated).
100
+
101
+ ### Required protobuf files
102
+
103
+ - `envoy/service/discovery/v3/discovery.proto` - DiscoveryRequest/Response.
104
+ - `envoy/service/discovery/v3/ads.proto` - AggregatedDiscoveryService.
105
+ - `envoy/config/cluster/v3/cluster.proto` - Cluster (CDS).
106
+ - `envoy/config/endpoint/v3/endpoint.proto` - ClusterLoadAssignment (EDS).
107
+ - `envoy/config/core/v3/base.proto` - Node, Locality, etc.
108
+ - `google/protobuf/any.proto` - For Any type in DiscoveryResponse.
109
+
110
+ ### Generating Ruby code
111
+
112
+ ```bash
113
+ protoc --ruby_out=lib \
114
+ --proto_path=vendor/envoy-api \
115
+ envoy/service/discovery/v3/discovery.proto \
116
+ envoy/service/discovery/v3/ads.proto \
117
+ envoy/config/cluster/v3/cluster.proto \
118
+ envoy/config/endpoint/v3/endpoint.proto \
119
+ envoy/config/core/v3/base.proto
120
+ ```
121
+
122
+ Lock the Envoy API version (submodule tag or commit) for compatibility.
@@ -0,0 +1,294 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "async/grpc/xds/client"
7
+ require "async/grpc/xds/ads_stream"
8
+ require "async/grpc/service"
9
+ require "sus/fixtures/async"
10
+ require "async/http/endpoint"
11
+ require_relative "../../../../../fixtures/async/grpc/test_interface"
12
+ require "json"
13
+ require "net/http"
14
+ require "set"
15
+ require "uri"
16
+
17
+ describe Async::GRPC::XDS::Client do
18
+ include Sus::Fixtures::Async::ReactorContext
19
+
20
+ let(:xds_server_uri) {ENV["XDS_SERVER_URI"] || "xds-control-plane:18000"}
21
+ let(:xds_admin_uri) {ENV["XDS_ADMIN_URI"] || "http://xds-control-plane:18001"}
22
+ let(:service_name) {"myservice"}
23
+
24
+ let(:bootstrap) do
25
+ {
26
+ xds_servers: [
27
+ {
28
+ server_uri: xds_server_uri,
29
+ channel_creds: [{type: "insecure"}]
30
+ }
31
+ ],
32
+ node: {
33
+ id: "test-client-#{Process.pid}",
34
+ cluster: "test"
35
+ }
36
+ }
37
+ end
38
+
39
+ let(:client) {subject.new(service_name, bootstrap: bootstrap)}
40
+
41
+ def backend_id(response)
42
+ response.value[/Response from ([^:]+)/, 1]
43
+ end
44
+
45
+ def call_backend_ids(stub, count: 6)
46
+ count.times.map do
47
+ request = Protocol::GRPC::Fixtures::TestMessage.new(value: "test")
48
+ backend_id(stub.unary_call(request))
49
+ end
50
+ end
51
+
52
+ def post_admin(path, payload = {})
53
+ uri = URI.join("#{xds_admin_uri}/", path)
54
+ request = Net::HTTP::Post.new(uri)
55
+ request["Content-Type"] = "application/json"
56
+ request.body = JSON.dump(payload)
57
+
58
+ response = Net::HTTP.start(uri.host, uri.port) do |http|
59
+ http.request(request)
60
+ end
61
+
62
+ unless response.is_a?(Net::HTTPSuccess)
63
+ raise "Admin request failed: #{response.code} #{response.body}"
64
+ end
65
+
66
+ JSON.parse(response.body)
67
+ end
68
+
69
+ def set_control_plane_endpoints(*endpoints)
70
+ post_admin("endpoints", endpoints: endpoints)
71
+ end
72
+
73
+ def reset_control_plane_streams
74
+ post_admin("reset-streams")
75
+ end
76
+
77
+ def restore_control_plane_endpoints
78
+ set_control_plane_endpoints(
79
+ "backend-1:50051",
80
+ "backend-2:50052",
81
+ "backend-3:50053"
82
+ )
83
+ end
84
+
85
+ def eventually(timeout: 15, interval: 0.1)
86
+ deadline = Time.now + timeout
87
+ last_error = nil
88
+
89
+ while Time.now < deadline
90
+ begin
91
+ result = yield
92
+ return result if result
93
+ rescue => error
94
+ last_error = error
95
+ end
96
+
97
+ sleep(interval)
98
+ end
99
+
100
+ raise last_error if last_error
101
+ raise "Timed out waiting for condition"
102
+ end
103
+
104
+ it "can stream updates" do
105
+ skip "Requires xDS control plane (XDS_SERVER_URI)" unless ENV["XDS_SERVER_URI"]
106
+
107
+ received = []
108
+ delegate = Object.new
109
+ delegate.define_singleton_method(:discovery_response){|response, _stream| received << response}
110
+
111
+ endpoint = Async::HTTP::Endpoint.parse(
112
+ "http://#{xds_server_uri}",
113
+ protocol: Async::HTTP::Protocol::HTTP2
114
+ )
115
+ http_client = Async::HTTP::Client.new(endpoint)
116
+ grpc_client = Async::GRPC::Client.new(http_client)
117
+ node = Envoy::Config::Core::V3::Node.new(id: "test-#{Process.pid}", cluster: "test")
118
+
119
+ initial = Envoy::Service::Discovery::V3::DiscoveryRequest.new(
120
+ type_url: "type.googleapis.com/envoy.config.cluster.v3.Cluster",
121
+ resource_names: [service_name],
122
+ node: node
123
+ )
124
+ stream = Async::GRPC::XDS::ADSStream.new(grpc_client, node, delegate: delegate)
125
+
126
+ stream_task = Async{stream.run(initial: initial)}
127
+ deadline = Time.now + 10
128
+ while received.empty? && Time.now < deadline
129
+ sleep(0.1)
130
+ end
131
+ stream_task.stop
132
+
133
+ expect(received.size).to be >= 1
134
+ end
135
+
136
+ it "can resolve endpoints" do
137
+ skip "Requires docker compose environment (XDS_SERVER_URI)" unless ENV["XDS_SERVER_URI"]
138
+
139
+ endpoints = client.resolve_endpoints
140
+
141
+ expect(endpoints.size).to be >= 1
142
+ end
143
+
144
+ it "can make RPC calls through xDS" do
145
+ skip "Requires docker compose environment (XDS_SERVER_URI)" unless ENV["XDS_SERVER_URI"]
146
+
147
+ stub = client.stub(Async::GRPC::Fixtures::TestInterface, service_name)
148
+
149
+ request = Protocol::GRPC::Fixtures::TestMessage.new(value: "test")
150
+ response = stub.unary_call(request)
151
+
152
+ expect(response).to be_a(Protocol::GRPC::Fixtures::TestMessage)
153
+ expect(response.value).to be(:include?, "test")
154
+ end
155
+
156
+ it "load balances across multiple endpoints" do
157
+ skip "Requires docker compose environment (XDS_SERVER_URI)" unless ENV["XDS_SERVER_URI"]
158
+
159
+ stub = client.stub(Async::GRPC::Fixtures::TestInterface, service_name)
160
+ endpoints_used = Set.new
161
+
162
+ 10.times do
163
+ request = Protocol::GRPC::Fixtures::TestMessage.new(value: "test")
164
+ response = stub.unary_call(request)
165
+ endpoints_used << response.value
166
+ end
167
+
168
+ expect(endpoints_used.size).to be >= 1
169
+ end
170
+
171
+ it "handles bootstrap configuration errors" do
172
+ expect{subject.new(service_name, bootstrap: {invalid: "config" })}.to raise_exception(Async::GRPC::XDS::Client::ConfigurationError)
173
+ end
174
+
175
+ it "handles no endpoints available" do
176
+ skip "Requires docker compose environment (XDS_SERVER_URI)" unless ENV["XDS_SERVER_URI"]
177
+
178
+ invalid_client = subject.new("nonexistent-service", bootstrap: bootstrap)
179
+
180
+ expect{invalid_client.resolve_endpoints}.to raise_exception(Async::GRPC::XDS::Client::NoEndpointsError)
181
+ end
182
+
183
+ it "evicts resolved promises to prevent unbounded growth" do
184
+ skip "Requires docker compose environment (XDS_SERVER_URI)" unless ENV["XDS_SERVER_URI"]
185
+
186
+ xds_client = subject.new(service_name, bootstrap: bootstrap)
187
+ xds_client.resolve_endpoints
188
+
189
+ context = xds_client.instance_variable_get(:@context)
190
+ # Resolved promises are evicted immediately; hashes stay bounded
191
+ expect(context.instance_variable_get(:@cluster_promises)).to be(:empty?)
192
+ expect(context.instance_variable_get(:@endpoint_promises)).to be(:empty?)
193
+ end
194
+
195
+ it "clears promise caches on close to prevent memory growth" do
196
+ skip "Requires docker compose environment (XDS_SERVER_URI)" unless ENV["XDS_SERVER_URI"]
197
+
198
+ xds_client = subject.new(service_name, bootstrap: bootstrap)
199
+ xds_client.resolve_endpoints
200
+
201
+ context = xds_client.instance_variable_get(:@context)
202
+ xds_client.close
203
+
204
+ # Close clears any remaining promises (e.g. unresolved for nonexistent services)
205
+ expect(context.instance_variable_get(:@cluster_promises)).to be(:empty?)
206
+ expect(context.instance_variable_get(:@endpoint_promises)).to be(:empty?)
207
+ end
208
+
209
+ it "applies endpoint updates from the control plane" do
210
+ skip "Requires docker compose environment (XDS_SERVER_URI)" unless ENV["XDS_SERVER_URI"]
211
+
212
+ begin
213
+ set_control_plane_endpoints("backend-1:50051")
214
+
215
+ xds_client = subject.new(service_name, bootstrap: bootstrap)
216
+ stub = xds_client.stub(Async::GRPC::Fixtures::TestInterface, service_name)
217
+
218
+ eventually do
219
+ ids = call_backend_ids(stub, count: 3)
220
+ ids.all?{|id| id == "backend-1"}
221
+ end
222
+
223
+ set_control_plane_endpoints("backend-1:50051", "backend-2:50052")
224
+
225
+ eventually do
226
+ ids = call_backend_ids(stub, count: 6)
227
+ ids.include?("backend-2")
228
+ end
229
+ ensure
230
+ xds_client&.close
231
+ restore_control_plane_endpoints
232
+ end
233
+ end
234
+
235
+ it "removes and recovers endpoints from control plane updates" do
236
+ skip "Requires docker compose environment (XDS_SERVER_URI)" unless ENV["XDS_SERVER_URI"]
237
+
238
+ begin
239
+ set_control_plane_endpoints("backend-1:50051", "backend-2:50052")
240
+
241
+ xds_client = subject.new(service_name, bootstrap: bootstrap)
242
+ stub = xds_client.stub(Async::GRPC::Fixtures::TestInterface, service_name)
243
+
244
+ eventually do
245
+ ids = call_backend_ids(stub, count: 6)
246
+ ids.include?("backend-1") && ids.include?("backend-2")
247
+ end
248
+
249
+ set_control_plane_endpoints("backend-2:50052")
250
+
251
+ eventually do
252
+ ids = call_backend_ids(stub, count: 4)
253
+ ids.all?{|id| id == "backend-2"}
254
+ end
255
+
256
+ set_control_plane_endpoints("backend-1:50051", "backend-2:50052")
257
+
258
+ eventually do
259
+ ids = call_backend_ids(stub, count: 6)
260
+ ids.include?("backend-1")
261
+ end
262
+ ensure
263
+ xds_client&.close
264
+ restore_control_plane_endpoints
265
+ end
266
+ end
267
+
268
+ it "reconnects after the ADS stream is reset" do
269
+ skip "Requires docker compose environment (XDS_SERVER_URI)" unless ENV["XDS_SERVER_URI"]
270
+
271
+ begin
272
+ set_control_plane_endpoints("backend-1:50051")
273
+
274
+ xds_client = subject.new(service_name, bootstrap: bootstrap)
275
+ stub = xds_client.stub(Async::GRPC::Fixtures::TestInterface, service_name)
276
+
277
+ eventually do
278
+ ids = call_backend_ids(stub, count: 3)
279
+ ids.all?{|id| id == "backend-1"}
280
+ end
281
+
282
+ reset_control_plane_streams
283
+ set_control_plane_endpoints("backend-2:50052")
284
+
285
+ eventually(timeout: 20) do
286
+ ids = call_backend_ids(stub, count: 4)
287
+ ids.all?{|id| id == "backend-2"}
288
+ end
289
+ ensure
290
+ xds_client&.close
291
+ restore_control_plane_endpoints
292
+ end
293
+ end
294
+ end