async-grpc-xds 0.2.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/getting-started.md +363 -0
- data/context/index.yaml +14 -0
- data/lib/async/grpc/xds/cluster.rb +7 -5
- data/lib/async/grpc/xds/cluster_discovery_service.rb +56 -0
- data/lib/async/grpc/xds/config_source.rb +46 -0
- data/lib/async/grpc/xds/control_plane.rb +3 -3
- data/lib/async/grpc/xds/discovery_service.rb +69 -0
- data/lib/async/grpc/xds/endpoint_discovery_service.rb +56 -0
- data/lib/async/grpc/xds/http_health_check.rb +48 -0
- data/lib/async/grpc/xds/server.rb +8 -2
- data/lib/async/grpc/xds/service.rb +5 -103
- data/lib/async/grpc/xds/stream.rb +102 -0
- data/lib/async/grpc/xds/version.rb +1 -1
- data/lib/async/grpc/xds.rb +6 -0
- data/proto/readme.md +8 -24
- data/readme.md +13 -6
- data/releases.md +9 -0
- data.tar.gz.sig +0 -0
- metadata +11 -14
- metadata.gz.sig +0 -0
- data/fixtures/async/grpc/test_interface.rb +0 -79
- data/fixtures/async/grpc/test_message.rb +0 -56
- data/xds/Dockerfile.backend +0 -24
- data/xds/Dockerfile.control-plane +0 -22
- data/xds/backend_server.rb +0 -68
- data/xds/docker-compose.yaml +0 -89
- data/xds/go.mod +0 -22
- data/xds/go.sum +0 -82
- data/xds/readme.md +0 -95
- data/xds/test/async/grpc/xds/client.rb +0 -294
- data/xds/test/async/grpc/xds/control_plane.rb +0 -93
- data/xds/test_server.go +0 -355
- data/xds/update_protos.sh +0 -123
|
@@ -1,294 +0,0 @@
|
|
|
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
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2026, by Samuel Williams.
|
|
5
|
-
|
|
6
|
-
require "async/grpc/xds/control_plane"
|
|
7
|
-
require "async/grpc/xds/client"
|
|
8
|
-
require "async/grpc/xds/server"
|
|
9
|
-
require "async/http/endpoint"
|
|
10
|
-
require "sus/fixtures/async"
|
|
11
|
-
require "socket"
|
|
12
|
-
|
|
13
|
-
describe Async::GRPC::XDS::ControlPlane do
|
|
14
|
-
include Sus::Fixtures::Async::ReactorContext
|
|
15
|
-
|
|
16
|
-
let(:control_plane) {subject.new}
|
|
17
|
-
|
|
18
|
-
it "publishes cluster resources" do
|
|
19
|
-
control_plane.update_cluster("myservice")
|
|
20
|
-
|
|
21
|
-
response = control_plane.response(
|
|
22
|
-
Async::GRPC::XDS::ControlPlane::CLUSTER_TYPE,
|
|
23
|
-
["myservice"]
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
expect(response.version_info).to be == "1"
|
|
27
|
-
expect(response.resources.size).to be == 1
|
|
28
|
-
expect(response.resources.first.type_url).to be == Async::GRPC::XDS::ControlPlane::CLUSTER_TYPE
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
it "publishes endpoint resources" do
|
|
32
|
-
control_plane.update_endpoints(
|
|
33
|
-
"myservice",
|
|
34
|
-
[
|
|
35
|
-
{addresses: [{address: "127.0.0.1", port: 50051}], healthy: true},
|
|
36
|
-
{addresses: [{address: "127.0.0.2", port: 50052}], healthy: false}
|
|
37
|
-
]
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
response = control_plane.response(
|
|
41
|
-
Async::GRPC::XDS::ControlPlane::ENDPOINT_TYPE,
|
|
42
|
-
["myservice"]
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
expect(response.version_info).to be == "1"
|
|
46
|
-
expect(response.resources.size).to be == 1
|
|
47
|
-
expect(response.resources.first.type_url).to be == Async::GRPC::XDS::ControlPlane::ENDPOINT_TYPE
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
it "increments resource versions" do
|
|
51
|
-
control_plane.update_endpoints("myservice", [{addresses: [{address: "127.0.0.1", port: 50051}], healthy: true}])
|
|
52
|
-
control_plane.update_endpoints("myservice", [{addresses: [{address: "127.0.0.2", port: 50052}], healthy: true}])
|
|
53
|
-
|
|
54
|
-
expect(control_plane.version(Async::GRPC::XDS::ControlPlane::ENDPOINT_TYPE)).to be == "2"
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
it "serves resources over ADS" do
|
|
58
|
-
control_plane.update_cluster("myservice")
|
|
59
|
-
control_plane.update_endpoints("myservice", [{addresses: [{address: "127.0.0.1", port: 50051}], healthy: true}])
|
|
60
|
-
|
|
61
|
-
port = available_port
|
|
62
|
-
endpoint = Async::HTTP::Endpoint.parse(
|
|
63
|
-
"http://127.0.0.1:#{port}",
|
|
64
|
-
protocol: Async::HTTP::Protocol::HTTP2
|
|
65
|
-
)
|
|
66
|
-
server = Async::GRPC::XDS::Server.new(control_plane)
|
|
67
|
-
server_task = Async{server.run(endpoint)}
|
|
68
|
-
|
|
69
|
-
client = Async::GRPC::XDS::Client.new("myservice", bootstrap: {
|
|
70
|
-
xds_servers: [
|
|
71
|
-
{
|
|
72
|
-
server_uri: "127.0.0.1:#{port}",
|
|
73
|
-
channel_creds: [{type: "insecure"}]
|
|
74
|
-
}
|
|
75
|
-
],
|
|
76
|
-
node: {id: "test-#{Process.pid}", cluster: "test"}
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
endpoints = client.resolve_endpoints
|
|
80
|
-
|
|
81
|
-
expect(endpoints.map(&:authority)).to be == ["127.0.0.1:50051"]
|
|
82
|
-
ensure
|
|
83
|
-
client&.close
|
|
84
|
-
server_task&.stop
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def available_port
|
|
88
|
-
server = TCPServer.new("127.0.0.1", 0)
|
|
89
|
-
server.addr[1]
|
|
90
|
-
ensure
|
|
91
|
-
server&.close
|
|
92
|
-
end
|
|
93
|
-
end
|