async-service-supervisor-envoy 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/code.md +1 -0
- data/context/getting-started.md +94 -0
- data/context/index.yaml +12 -0
- data/lib/async/service/supervisor/envoy/delegate.rb +56 -0
- data/lib/async/service/supervisor/envoy/endpoint.rb +71 -0
- data/lib/async/service/supervisor/envoy/monitor.rb +183 -0
- data/lib/async/service/supervisor/envoy/supervised.rb +52 -0
- data/lib/async/service/supervisor/envoy/version.rb +18 -0
- data/lib/async/service/supervisor/envoy.rb +10 -0
- data/license.md +3 -0
- data/readme.md +75 -0
- data/releases.md +11 -0
- data.tar.gz.sig +0 -0
- metadata +136 -0
- metadata.gz.sig +5 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 554c82adfbbf77fc5fe7e0ee5a0bbe9a67de5161cc7699eab8b98cf4c0cb5847
|
|
4
|
+
data.tar.gz: 44a9f2a1488f5108dc18b859b43f0ad3ec2709490784e0500587c240790d9749
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7752e3f6af3a6795a5f294867cb4cf11523cb4192d464ea29834278f4d7f799969c18c4a9cc9bbd70ddd035b34dc532378333be2aad12488b7822af761284954
|
|
7
|
+
data.tar.gz: a9abeeb77d2eb1ca67d219fdb9ef75820c841d7bf9c15b86ee012e4296702524ecacdd2780d9b21b1d2e3520873048741b960b1d92c5a0a0b91761bc8845ef97
|
checksums.yaml.gz.sig
ADDED
|
Binary file
|
data/code.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
This guide explains how to use `async-service-supervisor-envoy` to publish supervised worker endpoints to Envoy using xDS.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add the gem to your project:
|
|
8
|
+
|
|
9
|
+
~~~ bash
|
|
10
|
+
$ bundle add async-service-supervisor-envoy
|
|
11
|
+
~~~
|
|
12
|
+
|
|
13
|
+
The gem depends on `async-service-supervisor` and `async-grpc-xds`.
|
|
14
|
+
|
|
15
|
+
## Core Concepts
|
|
16
|
+
|
|
17
|
+
`async-service-supervisor-envoy` provides:
|
|
18
|
+
|
|
19
|
+
- {ruby Async::Service::Supervisor::Envoy::Monitor} - A supervisor monitor that publishes worker endpoints through xDS.
|
|
20
|
+
- {ruby Async::Service::Supervisor::Envoy::Endpoint} - A small value object for endpoint state.
|
|
21
|
+
|
|
22
|
+
The monitor runs an xDS control plane endpoint. Envoy connects to it using ADS and receives CDS/EDS updates derived from supervisor worker state.
|
|
23
|
+
|
|
24
|
+
## Endpoint State
|
|
25
|
+
|
|
26
|
+
Workers are published when they register concrete endpoint state:
|
|
27
|
+
|
|
28
|
+
``` ruby
|
|
29
|
+
state = {
|
|
30
|
+
endpoint: {
|
|
31
|
+
name: "myservice",
|
|
32
|
+
scheme: :http,
|
|
33
|
+
protocols: ["http/1.1"],
|
|
34
|
+
addresses: [
|
|
35
|
+
{address: "127.0.0.1", port: 50051},
|
|
36
|
+
{path: "/run/myservice/worker.ipc"}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Workers without endpoint state are ignored by the Envoy monitor.
|
|
43
|
+
|
|
44
|
+
Falcon cluster workers can register their concrete post-bind listener automatically:
|
|
45
|
+
|
|
46
|
+
``` ruby
|
|
47
|
+
service "application" do
|
|
48
|
+
include Falcon::Environment::Cluster
|
|
49
|
+
include Async::Service::Supervisor::Envoy::Supervised
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Falcon describes its bound server resource as a listener. The integration converts that listener into Envoy upstream endpoint state, including its name, scheme, supported protocols, and every concrete IP or Unix socket address. Addresses belonging to one listener remain grouped as one Envoy load-balancer endpoint.
|
|
54
|
+
|
|
55
|
+
## Monitor Usage
|
|
56
|
+
|
|
57
|
+
Add the monitor to your supervisor environment:
|
|
58
|
+
|
|
59
|
+
``` ruby
|
|
60
|
+
require "async/service/supervisor/envoy"
|
|
61
|
+
|
|
62
|
+
Async::Service::Supervisor::Envoy::Monitor.new(
|
|
63
|
+
bind: "http://127.0.0.1:18000"
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
By default, workers are grouped into clusters by `state[:name]`.
|
|
68
|
+
|
|
69
|
+
## Custom Mapping
|
|
70
|
+
|
|
71
|
+
You can customize cluster grouping, endpoint selection, and health with a delegate:
|
|
72
|
+
|
|
73
|
+
``` ruby
|
|
74
|
+
class EnvoyDelegate < Async::Service::Supervisor::Envoy::Delegate
|
|
75
|
+
def endpoint_list(supervisor_controller)
|
|
76
|
+
super
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def cluster(supervisor_controller, endpoint)
|
|
80
|
+
super
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def healthy?(supervisor_controller, endpoint)
|
|
84
|
+
true
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
Async::Service::Supervisor::Envoy::Monitor.new(
|
|
89
|
+
bind: "http://127.0.0.1:18000",
|
|
90
|
+
delegate: EnvoyDelegate.new
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Disconnected workers are removed from EDS. Registered workers that fail the delegate health check remain in EDS with an unhealthy endpoint status.
|
data/context/index.yaml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Automatically generated context index for Utopia::Project guides.
|
|
2
|
+
# Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
|
|
3
|
+
---
|
|
4
|
+
description: Envoy xDS monitor for async-service-supervisor.
|
|
5
|
+
metadata:
|
|
6
|
+
documentation_uri: https://socketry.github.io/async-service-supervisor-envoy/
|
|
7
|
+
source_code_uri: https://github.com/socketry/async-service-supervisor-envoy.git
|
|
8
|
+
files:
|
|
9
|
+
- path: getting-started.md
|
|
10
|
+
title: Getting Started
|
|
11
|
+
description: This guide explains how to use `async-service-supervisor-envoy` to
|
|
12
|
+
publish supervised worker endpoints to Envoy using xDS.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "endpoint"
|
|
7
|
+
|
|
8
|
+
module Async
|
|
9
|
+
module Service
|
|
10
|
+
module Supervisor
|
|
11
|
+
module Envoy
|
|
12
|
+
# Maps supervisor state into Envoy upstream endpoints.
|
|
13
|
+
class Delegate
|
|
14
|
+
# Extract serialized endpoints from a supervisor controller.
|
|
15
|
+
# @parameter supervisor_controller [Object] The supervisor controller describing the worker.
|
|
16
|
+
# @returns [Hash | Array(Hash) | Nil] The serialized endpoint state.
|
|
17
|
+
def endpoints(supervisor_controller)
|
|
18
|
+
state = supervisor_controller.state
|
|
19
|
+
|
|
20
|
+
state[:endpoints] || state[:endpoint]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Convert serialized state into Envoy endpoint values.
|
|
24
|
+
# @parameter supervisor_controller [Object] The supervisor controller describing the worker.
|
|
25
|
+
# @returns [Array(Endpoint)] The upstream endpoints to publish.
|
|
26
|
+
def endpoint_list(supervisor_controller)
|
|
27
|
+
case endpoints = self.endpoints(supervisor_controller)
|
|
28
|
+
when nil
|
|
29
|
+
[]
|
|
30
|
+
when Array
|
|
31
|
+
endpoints.map{|endpoint| Endpoint.wrap(endpoint)}
|
|
32
|
+
else
|
|
33
|
+
[Endpoint.wrap(endpoints)]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Select the Envoy cluster name for an endpoint.
|
|
38
|
+
# @parameter supervisor_controller [Object] The supervisor controller describing the worker.
|
|
39
|
+
# @parameter endpoint [Endpoint] The endpoint being published.
|
|
40
|
+
# @returns [String | Nil] The cluster name, or nil to skip the endpoint.
|
|
41
|
+
def cluster(supervisor_controller, endpoint)
|
|
42
|
+
endpoint.name
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Determine whether an endpoint should be published as healthy.
|
|
46
|
+
# @parameter supervisor_controller [Object] The supervisor controller describing the worker.
|
|
47
|
+
# @parameter endpoint [Endpoint] The endpoint being published.
|
|
48
|
+
# @returns [Boolean] Whether the endpoint is healthy.
|
|
49
|
+
def healthy?(supervisor_controller, endpoint)
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Async
|
|
7
|
+
module Service
|
|
8
|
+
module Supervisor
|
|
9
|
+
module Envoy
|
|
10
|
+
# Represents one upstream endpoint published to Envoy EDS.
|
|
11
|
+
class Endpoint
|
|
12
|
+
# Wrap serialized endpoint state.
|
|
13
|
+
# @parameter value [Endpoint | Hash] The value to wrap.
|
|
14
|
+
# @returns [Endpoint] The endpoint value.
|
|
15
|
+
def self.wrap(value)
|
|
16
|
+
case value
|
|
17
|
+
when self
|
|
18
|
+
value
|
|
19
|
+
when Hash
|
|
20
|
+
new(**value)
|
|
21
|
+
else
|
|
22
|
+
raise ArgumentError, "Invalid Envoy endpoint: #{value.inspect}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Normalize a concrete endpoint address.
|
|
27
|
+
# @parameter value [Hash] The address value.
|
|
28
|
+
# @returns [Hash] A normalized IP or Unix address.
|
|
29
|
+
def self.normalize_address(value)
|
|
30
|
+
if path = value[:path]
|
|
31
|
+
raise ArgumentError, "A Unix endpoint cannot specify an IP address or port!" if value[:address] || value[:port]
|
|
32
|
+
|
|
33
|
+
{path: path.to_s}.freeze
|
|
34
|
+
elsif value[:address] && value[:port]
|
|
35
|
+
{address: value[:address].to_s, port: Integer(value[:port])}.freeze
|
|
36
|
+
else
|
|
37
|
+
raise ArgumentError, "An endpoint address requires either path, or address and port: #{value.inspect}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Initialize an endpoint.
|
|
42
|
+
# @parameter name [String] The upstream cluster name.
|
|
43
|
+
# @parameter scheme [String | Symbol] The upstream application scheme.
|
|
44
|
+
# @parameter protocols [Array(String)] The supported upstream HTTP protocol names.
|
|
45
|
+
# @parameter addresses [Array(Hash)] The grouped concrete addresses.
|
|
46
|
+
def initialize(name:, scheme:, protocols:, addresses:)
|
|
47
|
+
@name = name.to_s
|
|
48
|
+
@scheme = scheme.to_sym
|
|
49
|
+
@protocols = protocols.map(&:to_s).uniq.freeze
|
|
50
|
+
raise ArgumentError, "An endpoint requires at least one protocol!" if @protocols.empty?
|
|
51
|
+
@addresses = addresses.map{|value| self.class.normalize_address(value)}.freeze
|
|
52
|
+
raise ArgumentError, "An endpoint requires at least one address!" if @addresses.empty?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @attribute [String] The upstream cluster name.
|
|
56
|
+
attr :name
|
|
57
|
+
|
|
58
|
+
# @attribute [Symbol] The upstream application scheme.
|
|
59
|
+
attr :scheme
|
|
60
|
+
|
|
61
|
+
# @attribute [Array(String)] The supported upstream HTTP protocol names.
|
|
62
|
+
attr :protocols
|
|
63
|
+
|
|
64
|
+
# @attribute [Array(Hash)] The grouped concrete addresses.
|
|
65
|
+
attr :addresses
|
|
66
|
+
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "async/http/endpoint"
|
|
7
|
+
require "async/service/supervisor/monitor"
|
|
8
|
+
require "async/grpc/xds/control_plane"
|
|
9
|
+
require "async/grpc/xds/server"
|
|
10
|
+
|
|
11
|
+
require_relative "delegate"
|
|
12
|
+
require_relative "endpoint"
|
|
13
|
+
|
|
14
|
+
module Async
|
|
15
|
+
module Service
|
|
16
|
+
module Supervisor
|
|
17
|
+
# Provides Envoy integration for supervisor-managed services.
|
|
18
|
+
module Envoy
|
|
19
|
+
# Represents a supervisor monitor that publishes worker endpoints to Envoy using xDS.
|
|
20
|
+
class Monitor < Async::Service::Supervisor::Monitor
|
|
21
|
+
# Initialize the monitor.
|
|
22
|
+
# @parameter bind [String | Nil] The optional address for the xDS control plane server.
|
|
23
|
+
# @parameter delegate [Delegate] The delegate used to map supervisor state into Envoy endpoints.
|
|
24
|
+
# @parameter control_plane [Async::GRPC::XDS::ControlPlane] The xDS control plane to update.
|
|
25
|
+
def initialize(
|
|
26
|
+
bind: nil,
|
|
27
|
+
delegate: Delegate.new,
|
|
28
|
+
control_plane: Async::GRPC::XDS::ControlPlane.new,
|
|
29
|
+
**options
|
|
30
|
+
)
|
|
31
|
+
super(**options)
|
|
32
|
+
|
|
33
|
+
@bind = bind
|
|
34
|
+
@delegate = delegate
|
|
35
|
+
@control_plane = control_plane
|
|
36
|
+
@controllers = {}
|
|
37
|
+
@published_clusters = {}
|
|
38
|
+
@server_task = nil
|
|
39
|
+
@mutex = Mutex.new
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @attribute [Async::GRPC::XDS::ControlPlane] The xDS control plane receiving cluster and endpoint updates.
|
|
43
|
+
attr :control_plane
|
|
44
|
+
|
|
45
|
+
# @attribute [Delegate] The delegate used to map supervisor state into Envoy endpoints.
|
|
46
|
+
attr :delegate
|
|
47
|
+
|
|
48
|
+
# Register a supervisor worker with Envoy.
|
|
49
|
+
# @parameter supervisor_controller [Object] The supervisor controller describing the worker.
|
|
50
|
+
# @returns [void]
|
|
51
|
+
def register(supervisor_controller)
|
|
52
|
+
@mutex.synchronize do
|
|
53
|
+
@controllers[supervisor_controller.id] = supervisor_controller
|
|
54
|
+
reconcile
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Remove a supervisor worker from Envoy.
|
|
59
|
+
# @parameter supervisor_controller [Object] The supervisor controller describing the worker.
|
|
60
|
+
# @returns [void]
|
|
61
|
+
def remove(supervisor_controller)
|
|
62
|
+
@mutex.synchronize do
|
|
63
|
+
@controllers.delete(supervisor_controller.id)
|
|
64
|
+
reconcile
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Run the monitor and optional xDS server task.
|
|
69
|
+
# @parameter parent [Async::Task] The parent task used for the xDS server.
|
|
70
|
+
# @returns [Async::Task] The monitor task.
|
|
71
|
+
def run(parent: Async::Task.current)
|
|
72
|
+
task = super(parent: parent)
|
|
73
|
+
|
|
74
|
+
if @bind
|
|
75
|
+
@server_task = parent.async do
|
|
76
|
+
endpoint = Async::HTTP::Endpoint.parse(@bind, protocol: Async::HTTP::Protocol::HTTP2)
|
|
77
|
+
Async::GRPC::XDS::Server.new(@control_plane).run(endpoint)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
task
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Convert the currently published endpoints to JSON-compatible data.
|
|
85
|
+
# @returns [Hash] The clusters and endpoint hashes.
|
|
86
|
+
def as_json
|
|
87
|
+
@mutex.synchronize do
|
|
88
|
+
{
|
|
89
|
+
clusters: build_clusters
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Refresh endpoint health and publish updated EDS state.
|
|
95
|
+
# @returns [void]
|
|
96
|
+
def run_once
|
|
97
|
+
@mutex.synchronize do
|
|
98
|
+
reconcile
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def build_record(supervisor_controller, endpoint)
|
|
105
|
+
cluster = @delegate.cluster(supervisor_controller, endpoint)
|
|
106
|
+
return unless cluster
|
|
107
|
+
|
|
108
|
+
{
|
|
109
|
+
cluster: cluster.to_s,
|
|
110
|
+
endpoint: endpoint,
|
|
111
|
+
healthy: @delegate.healthy?(supervisor_controller, endpoint),
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def build_records(supervisor_controller)
|
|
116
|
+
@delegate.endpoint_list(supervisor_controller).filter_map do |endpoint|
|
|
117
|
+
build_record(supervisor_controller, endpoint)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def reconcile
|
|
122
|
+
records_by_cluster = build_records_by_cluster
|
|
123
|
+
clusters = build_clusters(records_by_cluster)
|
|
124
|
+
|
|
125
|
+
records_by_cluster.each do |cluster, records|
|
|
126
|
+
configuration = cluster_configuration(records)
|
|
127
|
+
|
|
128
|
+
unless @published_clusters[cluster] == configuration
|
|
129
|
+
@control_plane.update_cluster(cluster, **configuration)
|
|
130
|
+
@published_clusters[cluster] = configuration
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
(@published_clusters.keys | clusters.keys).each do |cluster|
|
|
135
|
+
@control_plane.update_endpoints(cluster, clusters.fetch(cluster, []))
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def build_records_by_cluster
|
|
140
|
+
@controllers.each_value.flat_map do |controller|
|
|
141
|
+
build_records(controller)
|
|
142
|
+
end.group_by do |record|
|
|
143
|
+
record[:cluster]
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def build_clusters(records_by_cluster = build_records_by_cluster)
|
|
148
|
+
records_by_cluster.transform_values do |records|
|
|
149
|
+
records.map do |record|
|
|
150
|
+
{addresses: record[:endpoint].addresses, healthy: record[:healthy]}
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def cluster_configuration(records)
|
|
156
|
+
schemes = records.map{|record| record[:endpoint].scheme}.uniq
|
|
157
|
+
protocols = records.map{|record| record[:endpoint].protocols}
|
|
158
|
+
common_protocols = protocols.reduce{|common, names| common & names}
|
|
159
|
+
|
|
160
|
+
raise ArgumentError, "Envoy cluster contains incompatible schemes: #{schemes.inspect}" if schemes.size > 1
|
|
161
|
+
raise ArgumentError, "Envoy cluster contains no common protocols: #{protocols.inspect}" if common_protocols.empty?
|
|
162
|
+
raise ArgumentError, "HTTPS upstream endpoints are not yet supported!" if schemes.first == :https
|
|
163
|
+
|
|
164
|
+
{protocol: envoy_protocol(common_protocols)}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def envoy_protocol(protocols)
|
|
168
|
+
protocols.each do |protocol|
|
|
169
|
+
case protocol
|
|
170
|
+
when "h2"
|
|
171
|
+
return :http2
|
|
172
|
+
when "http/1.1", "http/1.0"
|
|
173
|
+
return :http1
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
raise ArgumentError, "Envoy cluster contains no supported protocols: #{protocols.inspect}"
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "async/service/supervisor/supervised"
|
|
7
|
+
|
|
8
|
+
module Async
|
|
9
|
+
module Service
|
|
10
|
+
module Supervisor
|
|
11
|
+
module Envoy
|
|
12
|
+
# Registers post-bind cluster listeners with the supervisor.
|
|
13
|
+
module Supervised
|
|
14
|
+
include Async::Service::Supervisor::Supervised
|
|
15
|
+
|
|
16
|
+
# Prepare and register a worker after its listener has been bound.
|
|
17
|
+
# @parameter instance [Async::Container::Instance] The container instance.
|
|
18
|
+
# @parameter listener [Object] The bound listener descriptor.
|
|
19
|
+
def prepare_worker!(instance, listener:)
|
|
20
|
+
prepare!(instance, state: {endpoint: envoy_endpoint(listener)})
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Convert a bound listener into serialized Envoy endpoint state.
|
|
24
|
+
# @parameter listener [Object] The bound listener descriptor.
|
|
25
|
+
# @returns [Hash] The serialized endpoint.
|
|
26
|
+
def envoy_endpoint(listener)
|
|
27
|
+
{
|
|
28
|
+
name: listener.name,
|
|
29
|
+
scheme: listener.scheme,
|
|
30
|
+
protocols: listener.protocols,
|
|
31
|
+
addresses: listener.addresses.map{|address| envoy_address(address)},
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Convert a concrete bound address to serializable supervisor state.
|
|
36
|
+
# @parameter address [Addrinfo] The bound address.
|
|
37
|
+
# @returns [Hash] The serialized address.
|
|
38
|
+
# @raises [ArgumentError] If the address family is unsupported.
|
|
39
|
+
def envoy_address(address)
|
|
40
|
+
if address.ip?
|
|
41
|
+
{address: address.ip_address, port: address.ip_port}
|
|
42
|
+
elsif address.unix?
|
|
43
|
+
{path: address.unix_path}
|
|
44
|
+
else
|
|
45
|
+
raise ArgumentError, "Unsupported listener address: #{address.inspect}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
# @namespace
|
|
7
|
+
module Async
|
|
8
|
+
# @namespace
|
|
9
|
+
module Service
|
|
10
|
+
# @namespace
|
|
11
|
+
module Supervisor
|
|
12
|
+
# @namespace
|
|
13
|
+
module Envoy
|
|
14
|
+
VERSION = "0.0.1"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "envoy/version"
|
|
7
|
+
require_relative "envoy/delegate"
|
|
8
|
+
require_relative "envoy/endpoint"
|
|
9
|
+
require_relative "envoy/monitor"
|
|
10
|
+
require_relative "envoy/supervised"
|
data/license.md
ADDED
data/readme.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Async::Service::Supervisor::Envoy
|
|
2
|
+
|
|
3
|
+
Provides an Envoy xDS monitor for `async-service-supervisor`.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/socketry/async-service-supervisor-envoy/actions?workflow=Test)
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
`async-service-supervisor-envoy` publishes supervised worker endpoints to Envoy:
|
|
10
|
+
|
|
11
|
+
- **xDS control plane** - Runs an ADS server backed by `async-grpc-xds`.
|
|
12
|
+
- **Supervisor integration** - Registers and removes endpoints from supervisor worker lifecycle events.
|
|
13
|
+
- **Multiple clusters** - Groups workers by `state[:name]` by default.
|
|
14
|
+
- **Endpoint contract** - Converts concrete post-bind worker listeners into Envoy upstream endpoints, including grouped IP or Unix socket addresses.
|
|
15
|
+
- **Delegate mapping** - Uses a delegate object for endpoint selection, cluster grouping, and health without active probing.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
Please see the [project documentation](https://socketry.github.io/async-service-supervisor-envoy/) for more details.
|
|
20
|
+
|
|
21
|
+
- [Getting Started](https://socketry.github.io/async-service-supervisor-envoy/guides/getting-started/index) - This guide explains how to use `async-service-supervisor-envoy` to publish supervised worker endpoints to Envoy using xDS.
|
|
22
|
+
|
|
23
|
+
## Releases
|
|
24
|
+
|
|
25
|
+
Please see the [project releases](https://socketry.github.io/async-service-supervisor-envoy/releases/index) for all releases.
|
|
26
|
+
|
|
27
|
+
### v0.0.1
|
|
28
|
+
|
|
29
|
+
- Register concrete Falcon cluster listeners as Envoy upstream endpoint state after binding.
|
|
30
|
+
- Publish grouped IP and Unix-domain-socket endpoint addresses to Envoy.
|
|
31
|
+
- Configure generated clusters from each endpoint's supported HTTP protocol names.
|
|
32
|
+
|
|
33
|
+
### v0.1.0
|
|
34
|
+
|
|
35
|
+
- Initial release.
|
|
36
|
+
|
|
37
|
+
## See Also
|
|
38
|
+
|
|
39
|
+
- [async-service-supervisor](https://github.com/socketry/async-service-supervisor) - Supervisor for managed Async service workers.
|
|
40
|
+
- [async-grpc-xds](https://github.com/socketry/async-grpc-xds) - xDS support for Async::GRPC.
|
|
41
|
+
- [Envoy xDS](https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol) - Envoy dynamic configuration protocol.
|
|
42
|
+
|
|
43
|
+
## Contributing
|
|
44
|
+
|
|
45
|
+
We welcome contributions to this project.
|
|
46
|
+
|
|
47
|
+
1. Fork it.
|
|
48
|
+
2. Create your feature branch (`git checkout -b my-new-feature`).
|
|
49
|
+
3. Commit your changes (`git commit -am 'Add some feature'`).
|
|
50
|
+
4. Push to the branch (`git push origin my-new-feature`).
|
|
51
|
+
5. Create new Pull Request.
|
|
52
|
+
|
|
53
|
+
### Running Tests
|
|
54
|
+
|
|
55
|
+
To run the test suite:
|
|
56
|
+
|
|
57
|
+
``` shell
|
|
58
|
+
bundle exec sus
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Making Releases
|
|
62
|
+
|
|
63
|
+
To make a new release:
|
|
64
|
+
|
|
65
|
+
``` shell
|
|
66
|
+
bundle exec bake gem:release:patch # or minor or major
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Developer Certificate of Origin
|
|
70
|
+
|
|
71
|
+
In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
|
|
72
|
+
|
|
73
|
+
### Community Guidelines
|
|
74
|
+
|
|
75
|
+
This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
|
data/releases.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Releases
|
|
2
|
+
|
|
3
|
+
## v0.0.1
|
|
4
|
+
|
|
5
|
+
- Register concrete Falcon cluster listeners as Envoy upstream endpoint state after binding.
|
|
6
|
+
- Publish grouped IP and Unix-domain-socket endpoint addresses to Envoy.
|
|
7
|
+
- Configure generated clusters from each endpoint's supported HTTP protocol names.
|
|
8
|
+
|
|
9
|
+
## v0.1.0
|
|
10
|
+
|
|
11
|
+
- Initial release.
|
data.tar.gz.sig
ADDED
|
Binary file
|
metadata
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: async-service-supervisor-envoy
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Samuel Williams
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain:
|
|
10
|
+
- |
|
|
11
|
+
-----BEGIN CERTIFICATE-----
|
|
12
|
+
MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
|
|
13
|
+
ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
|
|
14
|
+
CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
|
|
15
|
+
MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
|
|
16
|
+
MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
|
|
17
|
+
bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
|
|
18
|
+
igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
|
|
19
|
+
9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
|
|
20
|
+
sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
|
|
21
|
+
e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
|
|
22
|
+
XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
|
|
23
|
+
RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
|
|
24
|
+
tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
|
|
25
|
+
zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
|
|
26
|
+
xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
|
|
27
|
+
BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
|
|
28
|
+
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
|
|
29
|
+
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
|
|
30
|
+
cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
|
|
31
|
+
xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
|
|
32
|
+
c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
|
|
33
|
+
8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
|
|
34
|
+
JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
|
|
35
|
+
eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
|
|
36
|
+
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
|
37
|
+
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
|
38
|
+
-----END CERTIFICATE-----
|
|
39
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
40
|
+
dependencies:
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: async
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.38'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '2.38'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: async-grpc-xds
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0.1'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0.1'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: async-http
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: async-service-supervisor
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0.18'
|
|
90
|
+
type: :runtime
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0.18'
|
|
97
|
+
executables: []
|
|
98
|
+
extensions: []
|
|
99
|
+
extra_rdoc_files: []
|
|
100
|
+
files:
|
|
101
|
+
- code.md
|
|
102
|
+
- context/getting-started.md
|
|
103
|
+
- context/index.yaml
|
|
104
|
+
- lib/async/service/supervisor/envoy.rb
|
|
105
|
+
- lib/async/service/supervisor/envoy/delegate.rb
|
|
106
|
+
- lib/async/service/supervisor/envoy/endpoint.rb
|
|
107
|
+
- lib/async/service/supervisor/envoy/monitor.rb
|
|
108
|
+
- lib/async/service/supervisor/envoy/supervised.rb
|
|
109
|
+
- lib/async/service/supervisor/envoy/version.rb
|
|
110
|
+
- license.md
|
|
111
|
+
- readme.md
|
|
112
|
+
- releases.md
|
|
113
|
+
homepage: https://github.com/socketry/async-service-supervisor-envoy
|
|
114
|
+
licenses:
|
|
115
|
+
- MIT
|
|
116
|
+
metadata:
|
|
117
|
+
documentation_uri: https://socketry.github.io/async-service-supervisor-envoy/
|
|
118
|
+
source_code_uri: https://github.com/socketry/async-service-supervisor-envoy.git
|
|
119
|
+
rdoc_options: []
|
|
120
|
+
require_paths:
|
|
121
|
+
- lib
|
|
122
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
123
|
+
requirements:
|
|
124
|
+
- - ">="
|
|
125
|
+
- !ruby/object:Gem::Version
|
|
126
|
+
version: '3.3'
|
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '0'
|
|
132
|
+
requirements: []
|
|
133
|
+
rubygems_version: 4.0.10
|
|
134
|
+
specification_version: 4
|
|
135
|
+
summary: Envoy xDS monitor for async-service-supervisor.
|
|
136
|
+
test_files: []
|