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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/fixtures/async/grpc/test_interface.rb +79 -0
- data/fixtures/async/grpc/test_message.rb +56 -0
- data/lib/async/grpc/xds/ads_stream.rb +70 -0
- data/lib/async/grpc/xds/client.rb +255 -0
- data/lib/async/grpc/xds/context.rb +201 -0
- data/lib/async/grpc/xds/control_plane.rb +143 -0
- data/lib/async/grpc/xds/discovery_client.rb +356 -0
- data/lib/async/grpc/xds/health_checker.rb +88 -0
- data/lib/async/grpc/xds/load_balancer.rb +196 -0
- data/lib/async/grpc/xds/resource_builder.rb +138 -0
- data/lib/async/grpc/xds/resource_cache.rb +55 -0
- data/lib/async/grpc/xds/resources.rb +270 -0
- data/lib/async/grpc/xds/server.rb +34 -0
- data/lib/async/grpc/xds/service.rb +117 -0
- data/lib/async/grpc/xds/version.rb +12 -0
- data/lib/async/grpc/xds.rb +42 -0
- data/lib/envoy/annotations/deprecation_pb.rb +19 -0
- data/lib/envoy/config/cluster/v3/circuit_breaker_pb.rb +31 -0
- data/lib/envoy/config/cluster/v3/cluster_pb.rb +80 -0
- data/lib/envoy/config/cluster/v3/filter_pb.rb +28 -0
- data/lib/envoy/config/cluster/v3/outlier_detection_pb.rb +29 -0
- data/lib/envoy/config/core/v3/address_pb.rb +38 -0
- data/lib/envoy/config/core/v3/backoff_pb.rb +27 -0
- data/lib/envoy/config/core/v3/base_pb.rb +68 -0
- data/lib/envoy/config/core/v3/cel_pb.rb +24 -0
- data/lib/envoy/config/core/v3/config_source_pb.rb +42 -0
- data/lib/envoy/config/core/v3/event_service_config_pb.rb +27 -0
- data/lib/envoy/config/core/v3/extension_pb.rb +26 -0
- data/lib/envoy/config/core/v3/grpc_method_list_pb.rb +27 -0
- data/lib/envoy/config/core/v3/grpc_service_pb.rb +45 -0
- data/lib/envoy/config/core/v3/health_check_pb.rb +47 -0
- data/lib/envoy/config/core/v3/http_service_pb.rb +27 -0
- data/lib/envoy/config/core/v3/http_uri_pb.rb +27 -0
- data/lib/envoy/config/core/v3/protocol_pb.rb +51 -0
- data/lib/envoy/config/core/v3/proxy_protocol_pb.rb +31 -0
- data/lib/envoy/config/core/v3/resolver_pb.rb +27 -0
- data/lib/envoy/config/core/v3/socket_cmsg_headers_pb.rb +25 -0
- data/lib/envoy/config/core/v3/socket_option_pb.rb +31 -0
- data/lib/envoy/config/core/v3/substitution_format_string_pb.rb +30 -0
- data/lib/envoy/config/core/v3/udp_socket_config_pb.rb +26 -0
- data/lib/envoy/config/endpoint/v3/endpoint_components_pb.rb +40 -0
- data/lib/envoy/config/endpoint/v3/endpoint_pb.rb +32 -0
- data/lib/envoy/config/endpoint/v3/load_report_pb.rb +36 -0
- data/lib/envoy/service/discovery/v3/ads_pb.rb +26 -0
- data/lib/envoy/service/discovery/v3/aggregated_discovery_service.rb +64 -0
- data/lib/envoy/service/discovery/v3/discovery_pb.rb +42 -0
- data/lib/envoy/type/matcher/v3/address_pb.rb +25 -0
- data/lib/envoy/type/matcher/v3/filter_state_pb.rb +27 -0
- data/lib/envoy/type/matcher/v3/http_inputs_pb.rb +29 -0
- data/lib/envoy/type/matcher/v3/metadata_pb.rb +28 -0
- data/lib/envoy/type/matcher/v3/node_pb.rb +27 -0
- data/lib/envoy/type/matcher/v3/number_pb.rb +27 -0
- data/lib/envoy/type/matcher/v3/path_pb.rb +27 -0
- data/lib/envoy/type/matcher/v3/regex_pb.rb +30 -0
- data/lib/envoy/type/matcher/v3/status_code_input_pb.rb +25 -0
- data/lib/envoy/type/matcher/v3/string_pb.rb +29 -0
- data/lib/envoy/type/matcher/v3/struct_pb.rb +28 -0
- data/lib/envoy/type/matcher/v3/value_pb.rb +31 -0
- data/lib/envoy/type/metadata/v3/metadata_pb.rb +32 -0
- data/lib/envoy/type/v3/hash_policy_pb.rb +26 -0
- data/lib/envoy/type/v3/http_pb.rb +22 -0
- data/lib/envoy/type/v3/http_status_pb.rb +25 -0
- data/lib/envoy/type/v3/percent_pb.rb +26 -0
- data/lib/envoy/type/v3/range_pb.rb +25 -0
- data/lib/envoy/type/v3/ratelimit_strategy_pb.rb +28 -0
- data/lib/envoy/type/v3/ratelimit_unit_pb.rb +22 -0
- data/lib/envoy/type/v3/semantic_version_pb.rb +23 -0
- data/lib/envoy/type/v3/token_bucket_pb.rb +26 -0
- data/lib/envoy.rb +83 -0
- data/lib/google/protobuf/any_pb.rb +18 -0
- data/lib/google/protobuf/duration_pb.rb +18 -0
- data/lib/google/protobuf/empty_pb.rb +18 -0
- data/lib/google/protobuf/struct_pb.rb +21 -0
- data/lib/google/protobuf/timestamp_pb.rb +18 -0
- data/lib/google/protobuf/wrappers_pb.rb +26 -0
- data/lib/google/rpc/status_pb.rb +20 -0
- data/lib/udpa/annotations/migrate_pb.rb +22 -0
- data/lib/udpa/annotations/security_pb.rb +23 -0
- data/lib/udpa/annotations/sensitive_pb.rb +19 -0
- data/lib/udpa/annotations/status_pb.rb +21 -0
- data/lib/udpa/annotations/versioning_pb.rb +20 -0
- data/lib/validate/validate_pb.rb +43 -0
- data/lib/xds/annotations/v3/status_pb.rb +26 -0
- data/lib/xds/core/v3/authority_pb.rb +23 -0
- data/lib/xds/core/v3/cidr_pb.rb +24 -0
- data/lib/xds/core/v3/collection_entry_pb.rb +26 -0
- data/lib/xds/core/v3/context_params_pb.rb +22 -0
- data/lib/xds/core/v3/extension_pb.rb +23 -0
- data/lib/xds/core/v3/resource_locator_pb.rb +26 -0
- data/lib/xds/core/v3/resource_name_pb.rb +24 -0
- data/lib/xds/core/v3/resource_pb.rb +24 -0
- data/lib/xds/type/matcher/v3/domain_pb.rb +27 -0
- data/lib/xds/type/matcher/v3/http_inputs_pb.rb +22 -0
- data/lib/xds/type/matcher/v3/ip_pb.rb +28 -0
- data/lib/xds/type/matcher/v3/matcher_pb.rb +34 -0
- data/lib/xds/type/matcher/v3/range_pb.rb +31 -0
- data/lib/xds/type/matcher/v3/regex_pb.rb +25 -0
- data/lib/xds/type/matcher/v3/string_pb.rb +27 -0
- data/license.md +21 -0
- data/plan.md +156 -0
- data/proto/envoy/annotations/deprecation.proto +34 -0
- data/proto/envoy/annotations/resource.proto +19 -0
- data/proto/envoy/config/README.md +3 -0
- data/proto/envoy/config/cluster/v3/BUILD +18 -0
- data/proto/envoy/config/cluster/v3/circuit_breaker.proto +121 -0
- data/proto/envoy/config/cluster/v3/cluster.proto +1407 -0
- data/proto/envoy/config/cluster/v3/filter.proto +40 -0
- data/proto/envoy/config/cluster/v3/outlier_detection.proto +180 -0
- data/proto/envoy/config/core/v3/BUILD +16 -0
- data/proto/envoy/config/core/v3/address.proto +214 -0
- data/proto/envoy/config/core/v3/backoff.proto +37 -0
- data/proto/envoy/config/core/v3/base.proto +662 -0
- data/proto/envoy/config/core/v3/cel.proto +63 -0
- data/proto/envoy/config/core/v3/config_source.proto +283 -0
- data/proto/envoy/config/core/v3/event_service_config.proto +29 -0
- data/proto/envoy/config/core/v3/extension.proto +32 -0
- data/proto/envoy/config/core/v3/grpc_method_list.proto +33 -0
- data/proto/envoy/config/core/v3/grpc_service.proto +355 -0
- data/proto/envoy/config/core/v3/health_check.proto +443 -0
- data/proto/envoy/config/core/v3/http_service.proto +35 -0
- data/proto/envoy/config/core/v3/http_uri.proto +58 -0
- data/proto/envoy/config/core/v3/protocol.proto +807 -0
- data/proto/envoy/config/core/v3/proxy_protocol.proto +114 -0
- data/proto/envoy/config/core/v3/resolver.proto +36 -0
- data/proto/envoy/config/core/v3/socket_cmsg_headers.proto +28 -0
- data/proto/envoy/config/core/v3/socket_option.proto +108 -0
- data/proto/envoy/config/core/v3/substitution_format_string.proto +136 -0
- data/proto/envoy/config/core/v3/udp_socket_config.proto +32 -0
- data/proto/envoy/config/endpoint/v3/BUILD +16 -0
- data/proto/envoy/config/endpoint/v3/endpoint.proto +137 -0
- data/proto/envoy/config/endpoint/v3/endpoint_components.proto +229 -0
- data/proto/envoy/config/endpoint/v3/load_report.proto +220 -0
- data/proto/envoy/config/listener/v3/BUILD +18 -0
- data/proto/envoy/config/listener/v3/api_listener.proto +34 -0
- data/proto/envoy/config/listener/v3/listener.proto +455 -0
- data/proto/envoy/config/listener/v3/listener_components.proto +353 -0
- data/proto/envoy/config/listener/v3/quic_config.proto +108 -0
- data/proto/envoy/config/listener/v3/udp_listener_config.proto +52 -0
- data/proto/envoy/config/route/v3/BUILD +19 -0
- data/proto/envoy/config/route/v3/route.proto +172 -0
- data/proto/envoy/config/route/v3/route_components.proto +2918 -0
- data/proto/envoy/config/route/v3/scoped_route.proto +133 -0
- data/proto/envoy/extensions/transport_sockets/tls/v3/BUILD +14 -0
- data/proto/envoy/extensions/transport_sockets/tls/v3/cert.proto +12 -0
- data/proto/envoy/extensions/transport_sockets/tls/v3/common.proto +597 -0
- data/proto/envoy/extensions/transport_sockets/tls/v3/secret.proto +61 -0
- data/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto +366 -0
- data/proto/envoy/extensions/transport_sockets/tls/v3/tls_spiffe_validator_config.proto +67 -0
- data/proto/envoy/service/README.md +3 -0
- data/proto/envoy/service/discovery/v3/BUILD +13 -0
- data/proto/envoy/service/discovery/v3/ads.proto +44 -0
- data/proto/envoy/service/discovery/v3/discovery.proto +443 -0
- data/proto/envoy/type/BUILD +9 -0
- data/proto/envoy/type/hash_policy.proto +28 -0
- data/proto/envoy/type/http.proto +24 -0
- data/proto/envoy/type/http_status.proto +140 -0
- data/proto/envoy/type/matcher/v3/address.proto +22 -0
- data/proto/envoy/type/matcher/v3/filter_state.proto +33 -0
- data/proto/envoy/type/matcher/v3/http_inputs.proto +71 -0
- data/proto/envoy/type/matcher/v3/metadata.proto +110 -0
- data/proto/envoy/type/matcher/v3/node.proto +29 -0
- data/proto/envoy/type/matcher/v3/number.proto +33 -0
- data/proto/envoy/type/matcher/v3/path.proto +31 -0
- data/proto/envoy/type/matcher/v3/regex.proto +97 -0
- data/proto/envoy/type/matcher/v3/status_code_input.proto +23 -0
- data/proto/envoy/type/matcher/v3/string.proto +94 -0
- data/proto/envoy/type/matcher/v3/struct.proto +91 -0
- data/proto/envoy/type/matcher/v3/value.proto +80 -0
- data/proto/envoy/type/metadata/v3/metadata.proto +117 -0
- data/proto/envoy/type/percent.proto +52 -0
- data/proto/envoy/type/range.proto +43 -0
- data/proto/envoy/type/semantic_version.proto +24 -0
- data/proto/envoy/type/token_bucket.proto +36 -0
- data/proto/envoy/type/v3/BUILD +12 -0
- data/proto/envoy/type/v3/hash_policy.proto +43 -0
- data/proto/envoy/type/v3/http.proto +24 -0
- data/proto/envoy/type/v3/http_status.proto +199 -0
- data/proto/envoy/type/v3/percent.proto +57 -0
- data/proto/envoy/type/v3/range.proto +50 -0
- data/proto/envoy/type/v3/ratelimit_strategy.proto +79 -0
- data/proto/envoy/type/v3/ratelimit_unit.proto +37 -0
- data/proto/envoy/type/v3/semantic_version.proto +27 -0
- data/proto/envoy/type/v3/token_bucket.proto +39 -0
- data/proto/google/protobuf/any.proto +162 -0
- data/proto/google/protobuf/duration.proto +115 -0
- data/proto/google/protobuf/empty.proto +51 -0
- data/proto/google/protobuf/struct.proto +95 -0
- data/proto/google/protobuf/timestamp.proto +145 -0
- data/proto/google/protobuf/wrappers.proto +157 -0
- data/proto/google/rpc/status.proto +47 -0
- data/proto/readme.md +70 -0
- data/proto/udpa/annotations/migrate.proto +49 -0
- data/proto/udpa/annotations/security.proto +31 -0
- data/proto/udpa/annotations/sensitive.proto +14 -0
- data/proto/udpa/annotations/status.proto +34 -0
- data/proto/udpa/annotations/versioning.proto +17 -0
- data/proto/validate/validate.proto +862 -0
- data/proto/xds/annotations/v3/migrate.proto +46 -0
- data/proto/xds/annotations/v3/security.proto +30 -0
- data/proto/xds/annotations/v3/sensitive.proto +16 -0
- data/proto/xds/annotations/v3/status.proto +59 -0
- data/proto/xds/annotations/v3/versioning.proto +20 -0
- data/proto/xds/core/v3/authority.proto +22 -0
- data/proto/xds/core/v3/cidr.proto +25 -0
- data/proto/xds/core/v3/collection_entry.proto +55 -0
- data/proto/xds/core/v3/context_params.proto +23 -0
- data/proto/xds/core/v3/extension.proto +26 -0
- data/proto/xds/core/v3/resource.proto +29 -0
- data/proto/xds/core/v3/resource_locator.proto +118 -0
- data/proto/xds/core/v3/resource_name.proto +42 -0
- data/proto/xds/type/matcher/v3/cel.proto +37 -0
- data/proto/xds/type/matcher/v3/domain.proto +46 -0
- data/proto/xds/type/matcher/v3/http_inputs.proto +23 -0
- data/proto/xds/type/matcher/v3/ip.proto +53 -0
- data/proto/xds/type/matcher/v3/matcher.proto +144 -0
- data/proto/xds/type/matcher/v3/range.proto +69 -0
- data/proto/xds/type/matcher/v3/regex.proto +46 -0
- data/proto/xds/type/matcher/v3/string.proto +71 -0
- data/proto/xds/type/v3/cel.proto +77 -0
- data/proto/xds/type/v3/range.proto +40 -0
- data/proto/xds/type/v3/typed_struct.proto +44 -0
- data/readme.md +37 -0
- data/releases.md +5 -0
- data/xds/Dockerfile.backend +24 -0
- data/xds/Dockerfile.control-plane +22 -0
- data/xds/backend_server.rb +68 -0
- data/xds/docker-compose.yaml +89 -0
- data/xds/go.mod +22 -0
- data/xds/go.sum +82 -0
- data/xds/readme.md +122 -0
- data/xds/test/async/grpc/xds/client.rb +294 -0
- data/xds/test/async/grpc/xds/control_plane.rb +94 -0
- data/xds/test_server.go +355 -0
- data/xds/update_protos.sh +123 -0
- data.tar.gz.sig +0 -0
- metadata +386 -0
- metadata.gz.sig +2 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: fba6859fd43ebb2de75d228d4511125d1c7b36f08321fdda99eabf573fea900a
|
|
4
|
+
data.tar.gz: 2bca6eb3ec749a434703f0296f6a12c14e6d75f5518930618355e3abd8b37d83
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 78f056024e2e5c0cfbf1e1ca67acf9edd9b6ca357659911cbd733eeb2fde2ff0d73c169724a2caebd1ea1860bfaa09ce07784d950a5e9908c9c1a07c2021e81d
|
|
7
|
+
data.tar.gz: 4e9a3490cfb4e8918994301ef300a2780929dffd31dec5278bf690f976e37681e9476f58899fc668f9799d3d9398fbc740654d42599a7ee8b01c265a47a70e7b
|
checksums.yaml.gz.sig
ADDED
|
Binary file
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "protocol/grpc/interface"
|
|
7
|
+
require_relative "test_message"
|
|
8
|
+
|
|
9
|
+
module Async
|
|
10
|
+
module GRPC
|
|
11
|
+
module Fixtures
|
|
12
|
+
# Test interface for unit tests
|
|
13
|
+
# RPC names use PascalCase to match .proto files
|
|
14
|
+
class TestInterface < Protocol::GRPC::Interface
|
|
15
|
+
rpc :UnaryCall, request_class: Protocol::GRPC::Fixtures::TestMessage,
|
|
16
|
+
response_class: Protocol::GRPC::Fixtures::TestMessage, streaming: :unary
|
|
17
|
+
rpc :ServerStreamingCall, request_class: Protocol::GRPC::Fixtures::TestMessage,
|
|
18
|
+
response_class: Protocol::GRPC::Fixtures::TestMessage, streaming: :server_streaming
|
|
19
|
+
rpc :SayHello, request_class: Protocol::GRPC::Fixtures::TestMessage,
|
|
20
|
+
response_class: Protocol::GRPC::Fixtures::TestMessage, streaming: :unary
|
|
21
|
+
rpc :BidirectionalCall, request_class: Protocol::GRPC::Fixtures::TestMessage,
|
|
22
|
+
response_class: Protocol::GRPC::Fixtures::TestMessage, streaming: :bidirectional
|
|
23
|
+
rpc :ClientStreamingCall, request_class: Protocol::GRPC::Fixtures::TestMessage,
|
|
24
|
+
response_class: Protocol::GRPC::Fixtures::TestMessage, streaming: :client_streaming
|
|
25
|
+
rpc :SlowCall
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Test service implementation
|
|
29
|
+
# Method names use snake_case (Ruby convention)
|
|
30
|
+
class TestService < Async::GRPC::Service
|
|
31
|
+
def unary_call(input, output, _call)
|
|
32
|
+
request = input.read
|
|
33
|
+
response = Protocol::GRPC::Fixtures::TestMessage.new(value: "Response: #{request.value}")
|
|
34
|
+
output.write(response)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def server_streaming_call(input, output, _call)
|
|
38
|
+
request = input.read
|
|
39
|
+
3.times do |i|
|
|
40
|
+
response = Protocol::GRPC::Fixtures::TestMessage.new(value: "Response #{i}: #{request.value}")
|
|
41
|
+
output.write(response)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def say_hello(input, output, _call)
|
|
46
|
+
request = input.read
|
|
47
|
+
response = Protocol::GRPC::Fixtures::TestMessage.new(value: "Hello, #{request.value}!")
|
|
48
|
+
output.write(response)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def bidirectional_call(input, output, _call)
|
|
52
|
+
# Read all input messages and echo them back with a prefix
|
|
53
|
+
input.each do |request|
|
|
54
|
+
response = Protocol::GRPC::Fixtures::TestMessage.new(value: "Echo: #{request.value}")
|
|
55
|
+
output.write(response)
|
|
56
|
+
end
|
|
57
|
+
output.close_write
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def client_streaming_call(input, output, _call)
|
|
61
|
+
# Read all input messages and return a summary
|
|
62
|
+
values = []
|
|
63
|
+
input.each do |request|
|
|
64
|
+
values << request.value
|
|
65
|
+
end
|
|
66
|
+
response = Protocol::GRPC::Fixtures::TestMessage.new(value: "Received: #{values.join(', ')}")
|
|
67
|
+
output.write(response)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def slow_call(input, output, _call)
|
|
71
|
+
request = input.read
|
|
72
|
+
sleep 1 # Simulate a slow operation
|
|
73
|
+
response = Protocol::GRPC::Fixtures::TestMessage.new(value: "Slow response: #{request.value}")
|
|
74
|
+
output.write(response)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
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
|
+
module Protocol
|
|
7
|
+
module GRPC
|
|
8
|
+
module Fixtures
|
|
9
|
+
# Simple test message for unit tests.
|
|
10
|
+
# Provides a basic protobuf-like message structure.
|
|
11
|
+
class TestMessage
|
|
12
|
+
# @attribute [String | Nil] The message value.
|
|
13
|
+
attr_accessor :value
|
|
14
|
+
|
|
15
|
+
# Initialize a new test message.
|
|
16
|
+
# @parameter value [String | Nil] The message value
|
|
17
|
+
def initialize(value: nil)
|
|
18
|
+
@value = value
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Serialize the message to a binary string.
|
|
22
|
+
# @returns [String] Binary representation of the message
|
|
23
|
+
def to_proto
|
|
24
|
+
# Simple serialization: length-prefixed value
|
|
25
|
+
value_data = (@value || "").dup.force_encoding(Encoding::BINARY)
|
|
26
|
+
[value_data.bytesize].pack("N") + value_data
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
alias encode to_proto
|
|
30
|
+
|
|
31
|
+
# Deserialize a binary string into a message.
|
|
32
|
+
# @parameter data [String] Binary representation of the message
|
|
33
|
+
# @returns [TestMessage] Deserialized message instance
|
|
34
|
+
def self.decode(data)
|
|
35
|
+
# Simple binary format: first 4 bytes are length, rest is value
|
|
36
|
+
length = data[0...4].unpack1("N")
|
|
37
|
+
value = data[4...(4 + length)].dup.force_encoding(Encoding::UTF_8)
|
|
38
|
+
new(value: value)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Check equality with another message.
|
|
42
|
+
# @parameter other [Object] The object to compare
|
|
43
|
+
# @returns [Boolean] `true` if values are equal
|
|
44
|
+
def ==(other)
|
|
45
|
+
other.is_a?(TestMessage) && @value == other.value
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Get a string representation of the message.
|
|
49
|
+
# @returns [String] String representation
|
|
50
|
+
def inspect
|
|
51
|
+
"#<#{self.class.name} value=#{@value.inspect}>"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
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/client"
|
|
8
|
+
require "envoy/service/discovery/v3/aggregated_discovery_service"
|
|
9
|
+
require "envoy/service/discovery/v3/discovery_pb"
|
|
10
|
+
require "envoy/config/core/v3/base_pb"
|
|
11
|
+
|
|
12
|
+
module Async
|
|
13
|
+
module GRPC
|
|
14
|
+
module XDS
|
|
15
|
+
# Encapsulates a single ADS (Aggregated Discovery Service) bidirectional stream.
|
|
16
|
+
# Owns the stream lifecycle and delegates events to a delegate object.
|
|
17
|
+
class ADSStream
|
|
18
|
+
# Interface for ADSStream delegates. Implement these methods to receive stream events.
|
|
19
|
+
module Delegate
|
|
20
|
+
# Called when a DiscoveryResponse is received from the server.
|
|
21
|
+
# @parameter response [Envoy::Service::Discovery::V3::DiscoveryResponse] The discovery response
|
|
22
|
+
# @parameter stream [ADSStream] The stream instance; use stream.send(request) to send ACKs or new requests
|
|
23
|
+
def discovery_response(response, stream)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(client, node, delegate:)
|
|
28
|
+
@client = client
|
|
29
|
+
@node = node
|
|
30
|
+
@delegate = delegate
|
|
31
|
+
@body = nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Send a DiscoveryRequest on the stream. Call from within discovery_response to send ACKs.
|
|
35
|
+
# @parameter request [Envoy::Service::Discovery::V3::DiscoveryRequest] The request to send
|
|
36
|
+
def send(request)
|
|
37
|
+
@body&.write(request)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Run the ADS stream. Blocks until the stream completes or errors.
|
|
41
|
+
# @parameter initial [Object | Array | Nil] Initial message(s) to send (defaults to node-only request if nil/empty)
|
|
42
|
+
def run(initial: nil)
|
|
43
|
+
service = Envoy::Service::Discovery::V3::AggregatedDiscoveryService.new(
|
|
44
|
+
"envoy.service.discovery.v3.AggregatedDiscoveryService"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
initial = Array(initial).any? ? initial : [Envoy::Service::Discovery::V3::DiscoveryRequest.new(node: @node)]
|
|
48
|
+
|
|
49
|
+
@client.invoke(service, :StreamAggregatedResources, nil, initial: initial) do |body, readable_body|
|
|
50
|
+
@body = body
|
|
51
|
+
@delegate.stream_opened(self) if @delegate.respond_to?(:stream_opened)
|
|
52
|
+
|
|
53
|
+
begin
|
|
54
|
+
readable_body.each do |response|
|
|
55
|
+
@delegate.discovery_response(response, self)
|
|
56
|
+
end
|
|
57
|
+
ensure
|
|
58
|
+
@delegate.stream_closed(self) if @delegate.respond_to?(:stream_closed)
|
|
59
|
+
@body = nil
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
rescue => error
|
|
63
|
+
@delegate.stream_error(self, error) if @delegate.respond_to?(:stream_error)
|
|
64
|
+
Console.error(self, "Failed while streaming updates!", exception: error)
|
|
65
|
+
raise
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,255 @@
|
|
|
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/http/client"
|
|
8
|
+
require "async/http/endpoint"
|
|
9
|
+
require "protocol/http"
|
|
10
|
+
require "protocol/grpc"
|
|
11
|
+
require "async/grpc/client"
|
|
12
|
+
require "async/grpc/stub"
|
|
13
|
+
require_relative "context"
|
|
14
|
+
require_relative "load_balancer"
|
|
15
|
+
|
|
16
|
+
module Async
|
|
17
|
+
module GRPC
|
|
18
|
+
module XDS
|
|
19
|
+
# Wrapper client for xDS-enabled gRPC connections
|
|
20
|
+
# Follows the same pattern as Async::Redis::SentinelClient and ClusterClient
|
|
21
|
+
class Client < Protocol::HTTP::Middleware
|
|
22
|
+
# Raised when xDS configuration cannot be loaded
|
|
23
|
+
ConfigurationError = Context::ConfigurationError
|
|
24
|
+
|
|
25
|
+
# Raised when no endpoints are available
|
|
26
|
+
class NoEndpointsError < StandardError
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Raised when cluster configuration cannot be reloaded
|
|
30
|
+
ReloadError = Context::ReloadError
|
|
31
|
+
|
|
32
|
+
# Create a new xDS client
|
|
33
|
+
# @parameter service_name [String] Target service name (e.g., "myservice")
|
|
34
|
+
# @parameter bootstrap [Hash, String, nil] Bootstrap config (hash, file path, or nil for default)
|
|
35
|
+
# @parameter headers [Protocol::HTTP::Headers] Default headers
|
|
36
|
+
# @parameter options [Hash] Additional options passed to underlying clients
|
|
37
|
+
def initialize(service_name, bootstrap: nil, headers: Protocol::HTTP::Headers.new, node: nil, **options)
|
|
38
|
+
@service_name = service_name
|
|
39
|
+
@bootstrap = load_bootstrap(bootstrap)
|
|
40
|
+
@headers = headers
|
|
41
|
+
@options = options
|
|
42
|
+
|
|
43
|
+
@context = Context.new(@bootstrap, node: node || @bootstrap[:node])
|
|
44
|
+
@load_balancer = nil
|
|
45
|
+
@clients = {} # Cache clients per endpoint (like ClusterClient caches node.client)
|
|
46
|
+
@mutex = Mutex.new
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Resolve endpoints lazily (like SentinelClient.resolve_address)
|
|
50
|
+
# @returns [Array<Async::HTTP::Endpoint>] Available endpoints
|
|
51
|
+
def resolve_endpoints
|
|
52
|
+
@mutex.synchronize do
|
|
53
|
+
unless @load_balancer
|
|
54
|
+
# Discover cluster via CDS
|
|
55
|
+
cluster = @context.discover_cluster(@service_name)
|
|
56
|
+
|
|
57
|
+
# Discover endpoints via EDS
|
|
58
|
+
endpoints = @context.discover_endpoints(cluster)
|
|
59
|
+
|
|
60
|
+
raise NoEndpointsError, "No endpoints discovered for #{@service_name}" if endpoints.empty?
|
|
61
|
+
|
|
62
|
+
# Create load balancer
|
|
63
|
+
@load_balancer = LoadBalancer.new(cluster, endpoints)
|
|
64
|
+
|
|
65
|
+
# Set load balancer reference in context for endpoint updates
|
|
66
|
+
@context.load_balancer = @load_balancer
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
@load_balancer.healthy_endpoints
|
|
70
|
+
end
|
|
71
|
+
rescue Context::ReloadError => error
|
|
72
|
+
raise NoEndpointsError, "No endpoints discovered for #{@service_name}", cause: error
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Get a client for making calls (like ClusterClient.client_for)
|
|
76
|
+
# Resolves endpoints lazily and picks one via load balancer
|
|
77
|
+
# @returns [Array(Async::GRPC::Client, Async::HTTP::Endpoint)] Client and endpoint for request tracking
|
|
78
|
+
def client_for_call
|
|
79
|
+
endpoints = resolve_endpoints
|
|
80
|
+
raise NoEndpointsError, "No endpoints available for #{@service_name}" if endpoints.empty?
|
|
81
|
+
|
|
82
|
+
# Pick endpoint via load balancer
|
|
83
|
+
endpoint = @load_balancer.pick
|
|
84
|
+
raise NoEndpointsError, "No healthy endpoints available" unless endpoint
|
|
85
|
+
|
|
86
|
+
# Cache client per endpoint (like ClusterClient caches node.client)
|
|
87
|
+
client = @clients[endpoint] ||= begin
|
|
88
|
+
http_client = Async::HTTP::Client.new(endpoint, **@options)
|
|
89
|
+
Async::GRPC::Client.new(http_client, headers: @headers)
|
|
90
|
+
end
|
|
91
|
+
[client, endpoint]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Implement Protocol::HTTP::Middleware interface
|
|
95
|
+
# This allows XDS::Client to be used anywhere Async::GRPC::Client is used
|
|
96
|
+
# @parameter request [Protocol::HTTP::Request] The HTTP request
|
|
97
|
+
# @returns [Protocol::HTTP::Response] The HTTP response
|
|
98
|
+
def call(request, attempts: 3)
|
|
99
|
+
client, endpoint = client_for_call
|
|
100
|
+
@load_balancer.record_request_start(endpoint)
|
|
101
|
+
begin
|
|
102
|
+
client.call(request)
|
|
103
|
+
rescue Protocol::GRPC::Error => error
|
|
104
|
+
# Handle endpoint changes (like ClusterClient handles MOVED/ASK)
|
|
105
|
+
if error.status_code == Protocol::GRPC::Status::UNAVAILABLE
|
|
106
|
+
Console.warn(self, error)
|
|
107
|
+
|
|
108
|
+
# Invalidate cache, reload configuration
|
|
109
|
+
invalidate_cache!
|
|
110
|
+
|
|
111
|
+
attempts -= 1
|
|
112
|
+
retry if attempts > 0
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
raise
|
|
116
|
+
rescue => error
|
|
117
|
+
# Network errors might indicate endpoint failure
|
|
118
|
+
Console.warn(self, error)
|
|
119
|
+
|
|
120
|
+
# Invalidate this specific endpoint
|
|
121
|
+
invalidate_endpoint(client)
|
|
122
|
+
|
|
123
|
+
attempts -= 1
|
|
124
|
+
retry if attempts > 0
|
|
125
|
+
|
|
126
|
+
raise
|
|
127
|
+
end
|
|
128
|
+
ensure
|
|
129
|
+
@load_balancer&.record_request_end(endpoint)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Create a stub for the given interface.
|
|
133
|
+
# Same API as Async::GRPC::Client - load balancing happens per RPC call.
|
|
134
|
+
# @parameter interface_class [Class] Interface class (subclass of Protocol::GRPC::Interface)
|
|
135
|
+
# @parameter service_name [String] Service name (e.g., "hello.Greeter")
|
|
136
|
+
# @returns [Async::GRPC::Stub] Stub object with methods for each RPC
|
|
137
|
+
def stub(interface_class, service_name)
|
|
138
|
+
interface = interface_class.new(service_name)
|
|
139
|
+
Stub.new(self, interface)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Invoke an RPC (called by Stub). Load balances per call.
|
|
143
|
+
# @parameter service [Protocol::GRPC::Interface] Interface instance
|
|
144
|
+
# @parameter method [Symbol, String] Method name
|
|
145
|
+
# @parameter request [Object | Nil] Request message
|
|
146
|
+
# @parameter metadata [Hash] Custom metadata headers
|
|
147
|
+
# @parameter timeout [Numeric | Nil] Optional timeout in seconds
|
|
148
|
+
# @parameter encoding [String | Nil] Optional compression encoding
|
|
149
|
+
# @parameter initial [Object | Array] Optional initial message(s) for bidirectional streaming
|
|
150
|
+
# @yields {|input, output| ...} Block for streaming calls
|
|
151
|
+
# @returns [Object | Protocol::GRPC::Body::ReadableBody] Response message or readable body
|
|
152
|
+
def invoke(service, method, request = nil, metadata: {}, timeout: nil, encoding: nil, initial: nil, attempts: 3, &block)
|
|
153
|
+
client, endpoint = client_for_call
|
|
154
|
+
@load_balancer.record_request_start(endpoint)
|
|
155
|
+
begin
|
|
156
|
+
client.invoke(service, method, request, metadata: metadata, timeout: timeout, encoding: encoding, initial: initial, &block)
|
|
157
|
+
rescue Protocol::GRPC::Error => error
|
|
158
|
+
if error.status_code == Protocol::GRPC::Status::UNAVAILABLE
|
|
159
|
+
Console.warn(self, error)
|
|
160
|
+
invalidate_cache!
|
|
161
|
+
attempts -= 1
|
|
162
|
+
retry if attempts > 0
|
|
163
|
+
end
|
|
164
|
+
raise
|
|
165
|
+
rescue => error
|
|
166
|
+
Console.warn(self, error)
|
|
167
|
+
invalidate_endpoint(client)
|
|
168
|
+
attempts -= 1
|
|
169
|
+
retry if attempts > 0
|
|
170
|
+
raise
|
|
171
|
+
end
|
|
172
|
+
ensure
|
|
173
|
+
@load_balancer&.record_request_end(endpoint)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Close xDS client and all connections
|
|
177
|
+
def close
|
|
178
|
+
@clients.each_value(&:close)
|
|
179
|
+
@clients.clear
|
|
180
|
+
@context.close
|
|
181
|
+
@load_balancer&.close
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private
|
|
185
|
+
|
|
186
|
+
def load_bootstrap(bootstrap)
|
|
187
|
+
case bootstrap
|
|
188
|
+
when Hash
|
|
189
|
+
normalize_keys(bootstrap)
|
|
190
|
+
when String
|
|
191
|
+
load_bootstrap_file(bootstrap)
|
|
192
|
+
when nil
|
|
193
|
+
load_default_bootstrap
|
|
194
|
+
else
|
|
195
|
+
raise ArgumentError, "Invalid bootstrap: #{bootstrap.inspect}"
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def load_bootstrap_file(path)
|
|
200
|
+
raise ConfigurationError, "Bootstrap file not found: #{path}" unless File.exist?(path)
|
|
201
|
+
|
|
202
|
+
require "json"
|
|
203
|
+
normalize_keys(JSON.parse(File.read(path), symbolize_names: true))
|
|
204
|
+
rescue JSON::ParserError => error
|
|
205
|
+
raise ConfigurationError, "Invalid bootstrap JSON: #{error.message}"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def normalize_keys(object)
|
|
209
|
+
case object
|
|
210
|
+
when Hash
|
|
211
|
+
object.each_with_object({}) do |(key, value), result|
|
|
212
|
+
result[key.to_sym] = normalize_keys(value)
|
|
213
|
+
end
|
|
214
|
+
when Array
|
|
215
|
+
object.map{|value| normalize_keys(value)}
|
|
216
|
+
else
|
|
217
|
+
object
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def load_default_bootstrap
|
|
222
|
+
# Try environment variable first
|
|
223
|
+
if path = ENV["GRPC_XDS_BOOTSTRAP"]
|
|
224
|
+
return load_bootstrap_file(path)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Try default location
|
|
228
|
+
default_path = File.expand_path("~/.config/grpc/bootstrap.json")
|
|
229
|
+
if File.exist?(default_path)
|
|
230
|
+
return load_bootstrap_file(default_path)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
raise ConfigurationError, "No bootstrap configuration found"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def invalidate_cache!
|
|
237
|
+
@mutex.synchronize do
|
|
238
|
+
@clients.each_value(&:close)
|
|
239
|
+
@clients.clear
|
|
240
|
+
@load_balancer = nil
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def invalidate_endpoint(client)
|
|
245
|
+
@mutex.synchronize do
|
|
246
|
+
endpoint = @clients.key(client)
|
|
247
|
+
@load_balancer&.mark_unhealthy(endpoint) if endpoint
|
|
248
|
+
@clients.delete_if{|_, cached_client| cached_client == client}
|
|
249
|
+
client.close
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "async"
|
|
7
|
+
require_relative "discovery_client"
|
|
8
|
+
require_relative "resource_cache"
|
|
9
|
+
require_relative "resources"
|
|
10
|
+
|
|
11
|
+
module Async
|
|
12
|
+
module GRPC
|
|
13
|
+
module XDS
|
|
14
|
+
# Manages xDS subscriptions and maintains discovered resource state
|
|
15
|
+
class Context
|
|
16
|
+
# Raised when configuration is invalid
|
|
17
|
+
class ConfigurationError < StandardError
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Raised when cluster configuration cannot be reloaded
|
|
21
|
+
class ReloadError < StandardError
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Initialize xDS context
|
|
25
|
+
# @parameter bootstrap [Hash] Bootstrap configuration
|
|
26
|
+
# @parameter node [Hash] Node information (id, cluster, metadata, locality)
|
|
27
|
+
def initialize(bootstrap, node: nil)
|
|
28
|
+
@bootstrap = bootstrap
|
|
29
|
+
xds_server = bootstrap[:xds_servers]&.first
|
|
30
|
+
raise ConfigurationError, "No xds_servers in bootstrap" unless xds_server
|
|
31
|
+
|
|
32
|
+
@discovery_client = DiscoveryClient.new(xds_server, node: node)
|
|
33
|
+
@cache = ResourceCache.new
|
|
34
|
+
@subscriptions = {} # Track active subscriptions
|
|
35
|
+
@load_balancer = nil # Will be set by Client
|
|
36
|
+
@mutex = Mutex.new
|
|
37
|
+
@cluster_promises = {} # service_name -> Async::Promise (level-triggered: resolved value persists)
|
|
38
|
+
@endpoint_promises = {} # cluster_name -> Async::Promise
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Set load balancer reference (called by Client)
|
|
42
|
+
# @parameter load_balancer [LoadBalancer] Load balancer instance
|
|
43
|
+
def load_balancer=(load_balancer)
|
|
44
|
+
@load_balancer = load_balancer
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Discover cluster for service (like ClusterClient.reload_cluster!)
|
|
48
|
+
# @parameter service_name [String] Service to discover
|
|
49
|
+
# @returns [Resources::Cluster] Cluster configuration
|
|
50
|
+
def discover_cluster(service_name)
|
|
51
|
+
@mutex.synchronize do
|
|
52
|
+
# Check cache first
|
|
53
|
+
if cluster = @cache.get_cluster(service_name)
|
|
54
|
+
return cluster
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Subscribe to CDS if not already subscribed
|
|
58
|
+
unless @subscriptions[:cds]
|
|
59
|
+
@subscriptions[:cds] = subscribe_cds(service_name)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Subscribe to EDS for same name up front (EDS clusters use service name as cluster name)
|
|
63
|
+
# This avoids 10s delay between CDS and EDS - both requests go out together
|
|
64
|
+
subscription_key = :"eds_#{service_name}"
|
|
65
|
+
unless @subscriptions[subscription_key]
|
|
66
|
+
@subscriptions[subscription_key] = subscribe_eds(service_name)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
return @cache.get_cluster(service_name) if @cache.get_cluster(service_name)
|
|
70
|
+
|
|
71
|
+
# Wait for cluster (CDS response)
|
|
72
|
+
cluster = wait_for_cluster(service_name, timeout: 10)
|
|
73
|
+
raise ReloadError, "Failed to discover cluster: #{service_name}" unless cluster
|
|
74
|
+
cluster
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Discover endpoints for cluster (like ClusterClient discovers nodes)
|
|
78
|
+
# @parameter cluster [Resources::Cluster] Cluster configuration
|
|
79
|
+
# @returns [Array<Async::HTTP::Endpoint>] Discovered endpoints
|
|
80
|
+
def discover_endpoints(cluster)
|
|
81
|
+
cluster_name = cluster.name
|
|
82
|
+
@mutex.synchronize do
|
|
83
|
+
# Check cache first
|
|
84
|
+
if endpoints = @cache.get_endpoints(cluster_name)
|
|
85
|
+
return endpoints
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Subscribe to EDS if not already subscribed
|
|
89
|
+
subscription_key = :"eds_#{cluster_name}"
|
|
90
|
+
unless @subscriptions[subscription_key]
|
|
91
|
+
@subscriptions[subscription_key] = subscribe_eds(cluster_name)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
return @cache.get_endpoints(cluster_name) if @cache.get_endpoints(cluster_name)
|
|
95
|
+
|
|
96
|
+
# Wait outside mutex so EDS callback can run and update cache
|
|
97
|
+
endpoints = wait_for_endpoints(cluster_name, timeout: 10)
|
|
98
|
+
raise ReloadError, "Failed to discover endpoints for cluster: #{cluster_name}" unless endpoints
|
|
99
|
+
endpoints
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Subscribe to CDS (Cluster Discovery Service)
|
|
103
|
+
# @parameter service_name [String] Service name
|
|
104
|
+
# @returns [Async::Task] Subscription task
|
|
105
|
+
def subscribe_cds(service_name)
|
|
106
|
+
@discovery_client.subscribe(
|
|
107
|
+
DiscoveryClient::CLUSTER_TYPE,
|
|
108
|
+
[service_name]
|
|
109
|
+
) do |resources|
|
|
110
|
+
resources.each do |resource|
|
|
111
|
+
cluster = resource.is_a?(Resources::Cluster) ? resource : Resources::Cluster.from_proto(resource)
|
|
112
|
+
@cache.update_cluster(cluster)
|
|
113
|
+
resolve_cluster_promise(cluster.name, cluster)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Subscribe to EDS (Endpoint Discovery Service)
|
|
119
|
+
# @parameter cluster_name [String] Cluster name
|
|
120
|
+
# @returns [Async::Task] Subscription task
|
|
121
|
+
def subscribe_eds(cluster_name)
|
|
122
|
+
@discovery_client.subscribe(
|
|
123
|
+
DiscoveryClient::ENDPOINT_TYPE,
|
|
124
|
+
[cluster_name]
|
|
125
|
+
) do |resources|
|
|
126
|
+
resources.each do |resource|
|
|
127
|
+
assignment = resource.is_a?(Resources::ClusterLoadAssignment) ? resource : Resources::ClusterLoadAssignment.from_proto(resource)
|
|
128
|
+
endpoints = assignment.endpoints.select(&:healthy?).map do |endpoint|
|
|
129
|
+
Async::HTTP::Endpoint.parse(endpoint.uri, protocol: Async::HTTP::Protocol::HTTP2)
|
|
130
|
+
end
|
|
131
|
+
@cache.update_endpoints(cluster_name, endpoints)
|
|
132
|
+
resolve_endpoint_promise(cluster_name, endpoints) unless endpoints.empty?
|
|
133
|
+
@load_balancer&.update_endpoints(endpoints)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Close all subscriptions
|
|
139
|
+
def close
|
|
140
|
+
@mutex.synchronize do
|
|
141
|
+
@subscriptions.each_value do |task|
|
|
142
|
+
task.stop if task.respond_to?(:stop)
|
|
143
|
+
end
|
|
144
|
+
@subscriptions.clear
|
|
145
|
+
@cluster_promises.clear
|
|
146
|
+
@endpoint_promises.clear
|
|
147
|
+
end
|
|
148
|
+
@discovery_client.close
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def wait_for_cluster(service_name, timeout:)
|
|
154
|
+
promise = cluster_promise_for(service_name)
|
|
155
|
+
return promise.value if promise.completed?
|
|
156
|
+
|
|
157
|
+
begin
|
|
158
|
+
promise.wait(timeout: timeout)
|
|
159
|
+
promise.completed? ? promise.value : nil
|
|
160
|
+
rescue Async::TimeoutError
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def wait_for_endpoints(cluster_name, timeout:)
|
|
166
|
+
promise = endpoint_promise_for(cluster_name)
|
|
167
|
+
return promise.value if promise.completed?
|
|
168
|
+
|
|
169
|
+
begin
|
|
170
|
+
promise.wait(timeout: timeout)
|
|
171
|
+
promise.completed? ? promise.value : nil
|
|
172
|
+
rescue Async::TimeoutError
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def cluster_promise_for(service_name)
|
|
178
|
+
@mutex.synchronize do
|
|
179
|
+
@cluster_promises[service_name] ||= Async::Promise.new
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def endpoint_promise_for(cluster_name)
|
|
184
|
+
@mutex.synchronize do
|
|
185
|
+
@endpoint_promises[cluster_name] ||= Async::Promise.new
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def resolve_cluster_promise(service_name, cluster)
|
|
190
|
+
cluster_promise_for(service_name).resolve(cluster)
|
|
191
|
+
@mutex.synchronize{@cluster_promises.delete(service_name)}
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def resolve_endpoint_promise(cluster_name, endpoints)
|
|
195
|
+
endpoint_promise_for(cluster_name).resolve(endpoints)
|
|
196
|
+
@mutex.synchronize{@endpoint_promises.delete(cluster_name)}
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|