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
|
@@ -0,0 +1,196 @@
|
|
|
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/endpoint"
|
|
8
|
+
require_relative "health_checker"
|
|
9
|
+
require_relative "resources"
|
|
10
|
+
|
|
11
|
+
module Async
|
|
12
|
+
module GRPC
|
|
13
|
+
module XDS
|
|
14
|
+
# Client-side load balancing with health checking.
|
|
15
|
+
# RING_HASH and MAGLEV fall back to round-robin (require request context to hash).
|
|
16
|
+
class LoadBalancer
|
|
17
|
+
# Load balancing policies.
|
|
18
|
+
ROUND_ROBIN = :round_robin
|
|
19
|
+
LEAST_REQUEST = :least_request
|
|
20
|
+
RANDOM = :random
|
|
21
|
+
RING_HASH = :ring_hash
|
|
22
|
+
MAGLEV = :maglev
|
|
23
|
+
|
|
24
|
+
# Initialize load balancer
|
|
25
|
+
# @parameter cluster [Resources::Cluster] Cluster configuration
|
|
26
|
+
# @parameter endpoints [Array<Async::HTTP::Endpoint>] Initial endpoints
|
|
27
|
+
def initialize(cluster, endpoints)
|
|
28
|
+
@cluster = cluster
|
|
29
|
+
@endpoints = endpoints
|
|
30
|
+
@policy = parse_policy(cluster.load_balancer_policy)
|
|
31
|
+
@health_status = {} # Track health per endpoint
|
|
32
|
+
@health_checker = HealthChecker.new(cluster.health_checks)
|
|
33
|
+
@current_index = 0
|
|
34
|
+
@in_flight_requests = {} # Track in-flight requests per endpoint
|
|
35
|
+
@health_check_task = nil # Transient task for health check loop
|
|
36
|
+
|
|
37
|
+
# Initialize health status
|
|
38
|
+
@endpoints.each do |endpoint|
|
|
39
|
+
@health_status[endpoint] = :unknown
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Start health checking if configured
|
|
43
|
+
start_health_checks if cluster.health_checks.any?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get healthy endpoints
|
|
47
|
+
# @returns [Array<Async::HTTP::Endpoint>] Healthy endpoints
|
|
48
|
+
def healthy_endpoints
|
|
49
|
+
@endpoints.select{|endpoint| healthy?(endpoint)}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Pick next endpoint using load balancing policy
|
|
53
|
+
# @returns [Async::HTTP::Endpoint, nil] Selected endpoint
|
|
54
|
+
def pick
|
|
55
|
+
healthy = healthy_endpoints
|
|
56
|
+
return nil if healthy.empty?
|
|
57
|
+
|
|
58
|
+
case @policy
|
|
59
|
+
when ROUND_ROBIN
|
|
60
|
+
pick_round_robin(healthy)
|
|
61
|
+
when LEAST_REQUEST
|
|
62
|
+
pick_least_request(healthy)
|
|
63
|
+
when RANDOM
|
|
64
|
+
pick_random(healthy)
|
|
65
|
+
when RING_HASH
|
|
66
|
+
pick_ring_hash(healthy)
|
|
67
|
+
when MAGLEV
|
|
68
|
+
pick_maglev(healthy)
|
|
69
|
+
else
|
|
70
|
+
healthy.first
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Update endpoints from EDS
|
|
75
|
+
# @parameter endpoints [Array<Async::HTTP::Endpoint>] New endpoints
|
|
76
|
+
def update_endpoints(endpoints)
|
|
77
|
+
old_endpoints = @endpoints
|
|
78
|
+
@endpoints = endpoints
|
|
79
|
+
|
|
80
|
+
# Update health checker
|
|
81
|
+
@health_checker.update_endpoints(endpoints)
|
|
82
|
+
|
|
83
|
+
# Initialize health status for new endpoints
|
|
84
|
+
endpoints.each do |endpoint|
|
|
85
|
+
@health_status[endpoint] ||= :unknown
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Remove state for old endpoints
|
|
89
|
+
(old_endpoints - endpoints).each do |endpoint|
|
|
90
|
+
@health_status.delete(endpoint)
|
|
91
|
+
@in_flight_requests.delete(endpoint)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Record that a request has started for the given endpoint.
|
|
96
|
+
# Used by LEAST_REQUEST policy. Call from Client when a call begins.
|
|
97
|
+
# @parameter endpoint [Async::HTTP::Endpoint] The endpoint handling the request
|
|
98
|
+
def record_request_start(endpoint)
|
|
99
|
+
@in_flight_requests[endpoint] ||= 0
|
|
100
|
+
@in_flight_requests[endpoint] += 1
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Record that a request has finished for the given endpoint.
|
|
104
|
+
# Must be called in ensure to decrement even on error/retry.
|
|
105
|
+
# @parameter endpoint [Async::HTTP::Endpoint] The endpoint that handled the request
|
|
106
|
+
def record_request_end(endpoint)
|
|
107
|
+
return unless endpoint
|
|
108
|
+
current = @in_flight_requests[endpoint]
|
|
109
|
+
return unless current && current > 0
|
|
110
|
+
@in_flight_requests[endpoint] = current - 1
|
|
111
|
+
@in_flight_requests.delete(endpoint) if @in_flight_requests[endpoint] == 0
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Mark endpoint as unhealthy (e.g. after connection failure).
|
|
115
|
+
# Health checker may restore it on next successful check.
|
|
116
|
+
# @parameter endpoint [Async::HTTP::Endpoint] The endpoint to mark unhealthy
|
|
117
|
+
def mark_unhealthy(endpoint)
|
|
118
|
+
@health_status[endpoint] = :unhealthy
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Close load balancer
|
|
122
|
+
def close
|
|
123
|
+
if health_check_task = @health_check_task
|
|
124
|
+
@health_check_task = nil
|
|
125
|
+
health_check_task.stop
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
@health_checker.close
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def healthy?(endpoint)
|
|
134
|
+
status = @health_status[endpoint]
|
|
135
|
+
status == :healthy || status == :unknown
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def pick_round_robin(endpoints)
|
|
139
|
+
@current_index = (@current_index + 1) % endpoints.size
|
|
140
|
+
endpoints[@current_index]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def pick_least_request(endpoints)
|
|
144
|
+
# Track in-flight requests and pick endpoint with fewest
|
|
145
|
+
endpoints.min_by{|endpoint| @in_flight_requests[endpoint] || 0}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def pick_random(endpoints)
|
|
149
|
+
endpoints.sample
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def pick_ring_hash(endpoints)
|
|
153
|
+
pick_round_robin(endpoints) # Fallback; requires request context for consistent hashing
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def pick_maglev(endpoints)
|
|
157
|
+
pick_round_robin(endpoints) # Fallback; requires request context for Maglev hashing
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def parse_policy(load_balancer_policy)
|
|
161
|
+
# Parse cluster load balancer policy to our constants
|
|
162
|
+
case load_balancer_policy
|
|
163
|
+
when :ROUND_ROBIN, "ROUND_ROBIN"
|
|
164
|
+
ROUND_ROBIN
|
|
165
|
+
when :LEAST_REQUEST, "LEAST_REQUEST"
|
|
166
|
+
LEAST_REQUEST
|
|
167
|
+
when :RANDOM, "RANDOM"
|
|
168
|
+
RANDOM
|
|
169
|
+
when :RING_HASH, "RING_HASH"
|
|
170
|
+
RING_HASH
|
|
171
|
+
when :MAGLEV, "MAGLEV"
|
|
172
|
+
MAGLEV
|
|
173
|
+
else
|
|
174
|
+
ROUND_ROBIN # Default
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def start_health_checks
|
|
179
|
+
return unless @cluster.health_checks.any?
|
|
180
|
+
|
|
181
|
+
@health_check_task = Async(transient: true) do
|
|
182
|
+
loop do
|
|
183
|
+
@endpoints.each do |endpoint|
|
|
184
|
+
@health_status[endpoint] = @health_checker.check(endpoint)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Sleep for health check interval
|
|
188
|
+
interval = @cluster.health_checks.first[:interval] || 30
|
|
189
|
+
sleep(interval)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "google/protobuf/any_pb"
|
|
7
|
+
require "google/protobuf/duration_pb"
|
|
8
|
+
|
|
9
|
+
require "envoy/config/cluster/v3/cluster_pb"
|
|
10
|
+
require "envoy/config/core/v3/address_pb"
|
|
11
|
+
require "envoy/config/core/v3/config_source_pb"
|
|
12
|
+
require "envoy/config/core/v3/health_check_pb"
|
|
13
|
+
require "envoy/config/core/v3/protocol_pb"
|
|
14
|
+
require "envoy/config/endpoint/v3/endpoint_pb"
|
|
15
|
+
require "envoy/config/endpoint/v3/endpoint_components_pb"
|
|
16
|
+
|
|
17
|
+
module Async
|
|
18
|
+
module GRPC
|
|
19
|
+
module XDS
|
|
20
|
+
# Builds Envoy xDS resource protobufs.
|
|
21
|
+
module ResourceBuilder
|
|
22
|
+
TYPE_URL_PREFIX = "type.googleapis.com"
|
|
23
|
+
|
|
24
|
+
CLUSTER_TYPE = "#{TYPE_URL_PREFIX}/envoy.config.cluster.v3.Cluster"
|
|
25
|
+
ENDPOINT_TYPE = "#{TYPE_URL_PREFIX}/envoy.config.endpoint.v3.ClusterLoadAssignment"
|
|
26
|
+
|
|
27
|
+
def self.pack(resource)
|
|
28
|
+
Google::Protobuf::Any.new(
|
|
29
|
+
type_url: "#{TYPE_URL_PREFIX}/#{resource.class.descriptor.name}",
|
|
30
|
+
value: resource.to_proto
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.cluster(name, service_name: name, load_balancer_policy: :round_robin, connect_timeout: 5)
|
|
35
|
+
Envoy::Config::Cluster::V3::Cluster.new(
|
|
36
|
+
name: name.to_s,
|
|
37
|
+
type: Envoy::Config::Cluster::V3::Cluster::DiscoveryType::EDS,
|
|
38
|
+
eds_cluster_config: Envoy::Config::Cluster::V3::Cluster::EdsClusterConfig.new(
|
|
39
|
+
service_name: service_name.to_s,
|
|
40
|
+
eds_config: Envoy::Config::Core::V3::ConfigSource.new(
|
|
41
|
+
ads: Envoy::Config::Core::V3::AggregatedConfigSource.new
|
|
42
|
+
)
|
|
43
|
+
),
|
|
44
|
+
connect_timeout: duration(connect_timeout),
|
|
45
|
+
lb_policy: load_balancer_policy_value(load_balancer_policy),
|
|
46
|
+
http2_protocol_options: Envoy::Config::Core::V3::Http2ProtocolOptions.new
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.cluster_load_assignment(cluster_name, endpoints)
|
|
51
|
+
Envoy::Config::Endpoint::V3::ClusterLoadAssignment.new(
|
|
52
|
+
cluster_name: cluster_name.to_s,
|
|
53
|
+
endpoints: [
|
|
54
|
+
Envoy::Config::Endpoint::V3::LocalityLbEndpoints.new(
|
|
55
|
+
lb_endpoints: endpoints.map{|endpoint| load_balancer_endpoint(endpoint)}
|
|
56
|
+
)
|
|
57
|
+
]
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.load_balancer_endpoint(endpoint)
|
|
62
|
+
endpoint = normalize_endpoint(endpoint)
|
|
63
|
+
|
|
64
|
+
Envoy::Config::Endpoint::V3::LbEndpoint.new(
|
|
65
|
+
endpoint: Envoy::Config::Endpoint::V3::Endpoint.new(
|
|
66
|
+
address: Envoy::Config::Core::V3::Address.new(
|
|
67
|
+
socket_address: Envoy::Config::Core::V3::SocketAddress.new(
|
|
68
|
+
protocol: Envoy::Config::Core::V3::SocketAddress::Protocol::TCP,
|
|
69
|
+
address: endpoint[:address],
|
|
70
|
+
port_value: endpoint[:port]
|
|
71
|
+
)
|
|
72
|
+
),
|
|
73
|
+
hostname: endpoint[:hostname].to_s
|
|
74
|
+
),
|
|
75
|
+
health_status: health_status_value(endpoint.fetch(:healthy, true))
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.normalize_endpoint(endpoint)
|
|
80
|
+
case endpoint
|
|
81
|
+
when Hash
|
|
82
|
+
{
|
|
83
|
+
address: endpoint.fetch(:address){endpoint.fetch("address")},
|
|
84
|
+
port: endpoint.fetch(:port){endpoint.fetch("port")}.to_i,
|
|
85
|
+
hostname: endpoint[:hostname] || endpoint["hostname"],
|
|
86
|
+
healthy: endpoint.key?(:healthy) ? endpoint[:healthy] : endpoint.fetch("healthy", true)
|
|
87
|
+
}
|
|
88
|
+
else
|
|
89
|
+
if endpoint.respond_to?(:address) && endpoint.respond_to?(:port)
|
|
90
|
+
{
|
|
91
|
+
address: endpoint.address,
|
|
92
|
+
port: endpoint.port.to_i,
|
|
93
|
+
hostname: endpoint.respond_to?(:hostname) ? endpoint.hostname : nil,
|
|
94
|
+
healthy: endpoint.respond_to?(:healthy?) ? endpoint.healthy? : true
|
|
95
|
+
}
|
|
96
|
+
else
|
|
97
|
+
raise ArgumentError, "Invalid endpoint: #{endpoint.inspect}"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.duration(seconds)
|
|
103
|
+
whole_seconds = seconds.to_i
|
|
104
|
+
nanos = ((seconds.to_f - whole_seconds) * 1_000_000_000).to_i
|
|
105
|
+
|
|
106
|
+
Google::Protobuf::Duration.new(seconds: whole_seconds, nanos: nanos)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def self.load_balancer_policy_value(load_balancer_policy)
|
|
110
|
+
case load_balancer_policy
|
|
111
|
+
when :round_robin, :ROUND_ROBIN, "round_robin", "ROUND_ROBIN"
|
|
112
|
+
Envoy::Config::Cluster::V3::Cluster::LbPolicy::ROUND_ROBIN
|
|
113
|
+
when :least_request, :LEAST_REQUEST, "least_request", "LEAST_REQUEST"
|
|
114
|
+
Envoy::Config::Cluster::V3::Cluster::LbPolicy::LEAST_REQUEST
|
|
115
|
+
when :random, :RANDOM, "random", "RANDOM"
|
|
116
|
+
Envoy::Config::Cluster::V3::Cluster::LbPolicy::RANDOM
|
|
117
|
+
else
|
|
118
|
+
load_balancer_policy
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def self.health_status_value(healthy)
|
|
123
|
+
case healthy
|
|
124
|
+
when :healthy, :HEALTHY, "healthy", "HEALTHY", true
|
|
125
|
+
Envoy::Config::Core::V3::HealthStatus::HEALTHY
|
|
126
|
+
when :unhealthy, :UNHEALTHY, "unhealthy", "UNHEALTHY", false
|
|
127
|
+
Envoy::Config::Core::V3::HealthStatus::UNHEALTHY
|
|
128
|
+
when :degraded, :DEGRADED, "degraded", "DEGRADED"
|
|
129
|
+
Envoy::Config::Core::V3::HealthStatus::DEGRADED
|
|
130
|
+
else
|
|
131
|
+
Envoy::Config::Core::V3::HealthStatus::UNKNOWN
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Async
|
|
7
|
+
module GRPC
|
|
8
|
+
module XDS
|
|
9
|
+
# Caches discovered xDS resources
|
|
10
|
+
# Thread-safe cache for clusters and endpoints
|
|
11
|
+
class ResourceCache
|
|
12
|
+
def initialize
|
|
13
|
+
@clusters = {}
|
|
14
|
+
@endpoints = {}
|
|
15
|
+
@mutex = Mutex.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Get cluster by name
|
|
19
|
+
# @parameter name [String] Cluster name
|
|
20
|
+
# @returns [Resources::Cluster, nil] Cached cluster or nil
|
|
21
|
+
def get_cluster(name)
|
|
22
|
+
@mutex.synchronize{@clusters[name]}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Update cluster in cache
|
|
26
|
+
# @parameter cluster [Resources::Cluster] Cluster to cache
|
|
27
|
+
def update_cluster(cluster)
|
|
28
|
+
@mutex.synchronize{@clusters[cluster.name] = cluster}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Get endpoints for cluster
|
|
32
|
+
# @parameter cluster_name [String] Cluster name
|
|
33
|
+
# @returns [Array<Async::HTTP::Endpoint>, nil] Cached endpoints or nil
|
|
34
|
+
def get_endpoints(cluster_name)
|
|
35
|
+
@mutex.synchronize{@endpoints[cluster_name]}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Update endpoints for cluster
|
|
39
|
+
# @parameter cluster_name [String] Cluster name
|
|
40
|
+
# @parameter endpoints [Array<Async::HTTP::Endpoint>] Endpoints to cache
|
|
41
|
+
def update_endpoints(cluster_name, endpoints)
|
|
42
|
+
@mutex.synchronize{@endpoints[cluster_name] = endpoints}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Clear all cached resources
|
|
46
|
+
def clear
|
|
47
|
+
@mutex.synchronize do
|
|
48
|
+
@clusters.clear
|
|
49
|
+
@endpoints.clear
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Async
|
|
7
|
+
module GRPC
|
|
8
|
+
module XDS
|
|
9
|
+
module Resources
|
|
10
|
+
# Represents a discovered cluster
|
|
11
|
+
# Based on envoy.config.cluster.v3.Cluster
|
|
12
|
+
class Cluster
|
|
13
|
+
attr_reader :name, :type, :load_balancer_policy, :health_checks, :circuit_breakers, :eds_cluster_config
|
|
14
|
+
|
|
15
|
+
# Initialize cluster from protobuf or hash
|
|
16
|
+
# @parameter data [Object, Hash] Cluster protobuf or hash representation
|
|
17
|
+
def initialize(data)
|
|
18
|
+
if data.is_a?(Hash)
|
|
19
|
+
@name = data[:name]
|
|
20
|
+
@type = parse_type(data[:type])
|
|
21
|
+
@load_balancer_policy = parse_load_balancer_policy(data[:load_balancer_policy] || data[:lb_policy])
|
|
22
|
+
@health_checks = parse_health_checks(data[:health_checks] || [])
|
|
23
|
+
@circuit_breakers = data[:circuit_breakers]
|
|
24
|
+
@eds_cluster_config = data[:eds_cluster_config]
|
|
25
|
+
else
|
|
26
|
+
# Assume protobuf object
|
|
27
|
+
@name = data.name
|
|
28
|
+
@type = parse_type(data.type)
|
|
29
|
+
@load_balancer_policy = parse_load_balancer_policy(data.lb_policy)
|
|
30
|
+
@health_checks = parse_health_checks(data.health_checks || [])
|
|
31
|
+
@circuit_breakers = data.circuit_breakers
|
|
32
|
+
@eds_cluster_config = data.eds_cluster_config
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Create Cluster from protobuf message
|
|
37
|
+
# @parameter proto [Envoy::Config::Cluster::V3::Cluster] Protobuf cluster
|
|
38
|
+
# @returns [Cluster] Cluster instance
|
|
39
|
+
def self.from_proto(proto)
|
|
40
|
+
new(proto)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def eds_cluster?
|
|
44
|
+
@type == :EDS
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def parse_type(type)
|
|
50
|
+
# Handle protobuf enum values (integers) or symbols/strings
|
|
51
|
+
case type
|
|
52
|
+
when :EDS, "EDS", "envoy.config.cluster.v3.Cluster.EDS", 3
|
|
53
|
+
:EDS
|
|
54
|
+
when :STATIC, "STATIC", "envoy.config.cluster.v3.Cluster.STATIC", 0
|
|
55
|
+
:STATIC
|
|
56
|
+
when :LOGICAL_DNS, "LOGICAL_DNS", "envoy.config.cluster.v3.Cluster.LOGICAL_DNS", 2
|
|
57
|
+
:LOGICAL_DNS
|
|
58
|
+
when :STRICT_DNS, "STRICT_DNS", "envoy.config.cluster.v3.Cluster.STRICT_DNS", 1
|
|
59
|
+
:STRICT_DNS
|
|
60
|
+
else
|
|
61
|
+
# Default to EDS for unknown types
|
|
62
|
+
:EDS
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def parse_load_balancer_policy(policy)
|
|
67
|
+
# Handle protobuf enum values (integers) or symbols/strings
|
|
68
|
+
case policy
|
|
69
|
+
when :ROUND_ROBIN, "ROUND_ROBIN", "envoy.config.cluster.v3.Cluster.ROUND_ROBIN", 0
|
|
70
|
+
:ROUND_ROBIN
|
|
71
|
+
when :LEAST_REQUEST, "LEAST_REQUEST", "envoy.config.cluster.v3.Cluster.LEAST_REQUEST", 1
|
|
72
|
+
:LEAST_REQUEST
|
|
73
|
+
when :RANDOM, "RANDOM", "envoy.config.cluster.v3.Cluster.RANDOM", 3
|
|
74
|
+
:RANDOM
|
|
75
|
+
when :RING_HASH, "RING_HASH", "envoy.config.cluster.v3.Cluster.RING_HASH", 2
|
|
76
|
+
:RING_HASH
|
|
77
|
+
when :MAGLEV, "MAGLEV", "envoy.config.cluster.v3.Cluster.MAGLEV", 5
|
|
78
|
+
:MAGLEV
|
|
79
|
+
else
|
|
80
|
+
# Default to ROUND_ROBIN
|
|
81
|
+
:ROUND_ROBIN
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def parse_health_checks(checks)
|
|
86
|
+
Array(checks).map do |check|
|
|
87
|
+
if check.is_a?(Hash)
|
|
88
|
+
{
|
|
89
|
+
type: parse_health_check_type(check[:health_checker] || check[:type] || :HTTP),
|
|
90
|
+
timeout: parse_duration(check[:timeout]),
|
|
91
|
+
interval: parse_duration(check[:interval] || 30),
|
|
92
|
+
path: extract_http_path(check)
|
|
93
|
+
}
|
|
94
|
+
else
|
|
95
|
+
# Protobuf HealthCheck object
|
|
96
|
+
{
|
|
97
|
+
type: parse_health_check_type(check.health_checker),
|
|
98
|
+
timeout: parse_duration(check.timeout),
|
|
99
|
+
interval: parse_duration(check.interval),
|
|
100
|
+
path: extract_http_path_from_proto(check)
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def parse_health_check_type(checker)
|
|
107
|
+
# Handle protobuf HealthCheck.health_checker oneof
|
|
108
|
+
# checker is the health_checker field from HealthCheck protobuf
|
|
109
|
+
return :HTTP if checker.nil?
|
|
110
|
+
|
|
111
|
+
case checker
|
|
112
|
+
when :http_health_check
|
|
113
|
+
:HTTP
|
|
114
|
+
when :grpc_health_check
|
|
115
|
+
:gRPC
|
|
116
|
+
when :tcp_health_check
|
|
117
|
+
:TCP
|
|
118
|
+
when Hash
|
|
119
|
+
checker_type = checker[:type]
|
|
120
|
+
case checker_type
|
|
121
|
+
when :HTTP, "HTTP", "envoy.config.core.v3.HealthCheck.HttpHealthCheck"
|
|
122
|
+
:HTTP
|
|
123
|
+
when :gRPC, "gRPC", "envoy.config.core.v3.HealthCheck.GrpcHealthCheck"
|
|
124
|
+
:gRPC
|
|
125
|
+
when :TCP, "TCP", "envoy.config.core.v3.HealthCheck.TcpHealthCheck"
|
|
126
|
+
:TCP
|
|
127
|
+
else
|
|
128
|
+
:HTTP # Default
|
|
129
|
+
end
|
|
130
|
+
else
|
|
131
|
+
# Protobuf HealthCheck object - check which oneof field is set
|
|
132
|
+
# The health_checker is a oneof, so we check which field is populated
|
|
133
|
+
if checker.respond_to?(:http_health_check) && checker.http_health_check
|
|
134
|
+
:HTTP
|
|
135
|
+
elsif checker.respond_to?(:grpc_health_check) && checker.grpc_health_check
|
|
136
|
+
:gRPC
|
|
137
|
+
elsif checker.respond_to?(:tcp_health_check) && checker.tcp_health_check
|
|
138
|
+
:TCP
|
|
139
|
+
else
|
|
140
|
+
:HTTP # Default
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def parse_duration(duration)
|
|
146
|
+
# Convert protobuf Duration to seconds (float)
|
|
147
|
+
return duration if duration.is_a?(Numeric)
|
|
148
|
+
return nil unless duration
|
|
149
|
+
|
|
150
|
+
# If it's a protobuf Duration, convert to seconds
|
|
151
|
+
if duration.respond_to?(:seconds) && duration.respond_to?(:nanos)
|
|
152
|
+
duration.seconds + (duration.nanos.to_f / 1_000_000_000)
|
|
153
|
+
else
|
|
154
|
+
duration.to_f
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def extract_http_path(check)
|
|
159
|
+
# Extract HTTP path from hash
|
|
160
|
+
return nil unless check.is_a?(Hash)
|
|
161
|
+
|
|
162
|
+
http_check = check[:http_health_check] || {}
|
|
163
|
+
http_check[:path] || "/health"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def extract_http_path_from_proto(check)
|
|
167
|
+
# Extract HTTP path from protobuf HealthCheck
|
|
168
|
+
return nil unless check.respond_to?(:http_health_check)
|
|
169
|
+
|
|
170
|
+
http_check = check.http_health_check
|
|
171
|
+
return nil unless http_check
|
|
172
|
+
|
|
173
|
+
http_check.path || "/health"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Represents endpoint assignment (ClusterLoadAssignment)
|
|
178
|
+
# Based on envoy.config.endpoint.v3.ClusterLoadAssignment
|
|
179
|
+
class ClusterLoadAssignment
|
|
180
|
+
attr_reader :cluster_name, :endpoints
|
|
181
|
+
|
|
182
|
+
# Initialize from protobuf or hash
|
|
183
|
+
# @parameter data [Object, Hash] ClusterLoadAssignment protobuf or hash
|
|
184
|
+
def initialize(data)
|
|
185
|
+
if data.is_a?(Hash)
|
|
186
|
+
@cluster_name = data[:cluster_name]
|
|
187
|
+
@endpoints = parse_endpoints(data[:endpoints] || [])
|
|
188
|
+
else
|
|
189
|
+
@cluster_name = data.cluster_name
|
|
190
|
+
@endpoints = parse_endpoints(data.endpoints || [])
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Create ClusterLoadAssignment from protobuf message
|
|
195
|
+
# @parameter proto [Envoy::Config::Endpoint::V3::ClusterLoadAssignment] Protobuf assignment
|
|
196
|
+
# @returns [ClusterLoadAssignment] Assignment instance
|
|
197
|
+
def self.from_proto(proto)
|
|
198
|
+
new(proto)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
private
|
|
202
|
+
|
|
203
|
+
def parse_endpoints(endpoints_data)
|
|
204
|
+
Array(endpoints_data).flat_map do |locality_endpoints|
|
|
205
|
+
load_balancer_endpoints = locality_endpoints.is_a?(Hash) ?
|
|
206
|
+
(locality_endpoints[:lb_endpoints] || []) :
|
|
207
|
+
(locality_endpoints.lb_endpoints || [])
|
|
208
|
+
|
|
209
|
+
load_balancer_endpoints.map{|load_balancer_endpoint| Endpoint.new(load_balancer_endpoint)}
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Represents a single endpoint
|
|
215
|
+
# Based on envoy.config.endpoint.v3.LbEndpoint
|
|
216
|
+
class Endpoint
|
|
217
|
+
attr_reader :address, :port, :health_status, :metadata
|
|
218
|
+
|
|
219
|
+
def initialize(load_balancer_endpoint)
|
|
220
|
+
if load_balancer_endpoint.is_a?(Hash)
|
|
221
|
+
endpoint_data = load_balancer_endpoint[:endpoint] || {}
|
|
222
|
+
address_data = endpoint_data[:address] || {}
|
|
223
|
+
socket_address = address_data[:socket_address] || {}
|
|
224
|
+
|
|
225
|
+
@address = socket_address[:address] || "localhost"
|
|
226
|
+
@port = socket_address[:port_value] || 50051
|
|
227
|
+
@health_status = parse_health_status(load_balancer_endpoint[:health_status])
|
|
228
|
+
@metadata = load_balancer_endpoint[:metadata] || {}
|
|
229
|
+
else
|
|
230
|
+
socket_address = load_balancer_endpoint.endpoint.address.socket_address
|
|
231
|
+
@address = socket_address.address
|
|
232
|
+
@port = socket_address.port_value
|
|
233
|
+
@health_status = parse_health_status(load_balancer_endpoint.health_status)
|
|
234
|
+
@metadata = load_balancer_endpoint.metadata || {}
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def healthy?
|
|
239
|
+
@health_status == :HEALTHY || @health_status == :UNKNOWN
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def uri
|
|
243
|
+
# Use http for insecure/docker environments (gRPC h2c)
|
|
244
|
+
scheme = ENV["XDS_ENDPOINT_SCHEME"] || "http"
|
|
245
|
+
"#{scheme}://#{@address}:#{@port}"
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
private
|
|
249
|
+
|
|
250
|
+
def parse_health_status(status)
|
|
251
|
+
# Handle protobuf enum values (integers) or symbols/strings
|
|
252
|
+
# HealthStatus is defined in envoy.config.endpoint.v3.Endpoint
|
|
253
|
+
case status
|
|
254
|
+
when :HEALTHY, "HEALTHY", 0
|
|
255
|
+
:HEALTHY
|
|
256
|
+
when :UNHEALTHY, "UNHEALTHY", 1
|
|
257
|
+
:UNHEALTHY
|
|
258
|
+
when :DEGRADED, "DEGRADED", 2
|
|
259
|
+
:DEGRADED
|
|
260
|
+
when :UNKNOWN, "UNKNOWN", 3, nil
|
|
261
|
+
:UNKNOWN
|
|
262
|
+
else
|
|
263
|
+
:UNKNOWN
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "async/grpc/dispatcher"
|
|
7
|
+
require "async/http/server"
|
|
8
|
+
|
|
9
|
+
require_relative "control_plane"
|
|
10
|
+
require_relative "service"
|
|
11
|
+
|
|
12
|
+
module Async
|
|
13
|
+
module GRPC
|
|
14
|
+
module XDS
|
|
15
|
+
# Convenience wrapper for serving an xDS control plane over gRPC.
|
|
16
|
+
class Server
|
|
17
|
+
def initialize(control_plane = ControlPlane.new, **options)
|
|
18
|
+
@control_plane = control_plane
|
|
19
|
+
@dispatcher = Async::GRPC::Dispatcher.new
|
|
20
|
+
@dispatcher.register(Service.new(@control_plane))
|
|
21
|
+
@options = options
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
attr :control_plane
|
|
25
|
+
attr :dispatcher
|
|
26
|
+
|
|
27
|
+
def run(endpoint, **options)
|
|
28
|
+
server = Async::HTTP::Server.new(@dispatcher, endpoint, **@options, **options)
|
|
29
|
+
server.run
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|