async-service-supervisor-envoy 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 554c82adfbbf77fc5fe7e0ee5a0bbe9a67de5161cc7699eab8b98cf4c0cb5847
4
- data.tar.gz: 44a9f2a1488f5108dc18b859b43f0ad3ec2709490784e0500587c240790d9749
3
+ metadata.gz: 531e6d8c0e35b0122bcec0c0234afe37be776bb8b1e56e8ee92f5d026f5739f5
4
+ data.tar.gz: 6b9d8f77dd753fbf1a26b076f88fecc37a1856a69ac4a0e82bc4611370fb90c8
5
5
  SHA512:
6
- metadata.gz: 7752e3f6af3a6795a5f294867cb4cf11523cb4192d464ea29834278f4d7f799969c18c4a9cc9bbd70ddd035b34dc532378333be2aad12488b7822af761284954
7
- data.tar.gz: a9abeeb77d2eb1ca67d219fdb9ef75820c841d7bf9c15b86ee012e4296702524ecacdd2780d9b21b1d2e3520873048741b960b1d92c5a0a0b91761bc8845ef97
6
+ metadata.gz: 4708ac696da72097ebf6888cf144b3076c72392e1a695cb8311d6b2046869b1522a47850ec59464beff2f588720d760ae427d7c556a75d25e36bb353535a9311
7
+ data.tar.gz: 3ffad268b3785b3625fc3cec1cde6dfb50d68904547ab531a19a99317cef32baa29d52ebb1d6795cebe302885598c742a9eaa18cbde2fb7ecd95c01dcfe4b04c
checksums.yaml.gz.sig CHANGED
Binary file
@@ -41,16 +41,18 @@ state = {
41
41
 
42
42
  Workers without endpoint state are ignored by the Envoy monitor.
43
43
 
44
- Falcon cluster workers can register their concrete post-bind listener automatically:
44
+ Falcon server workers can register their concrete bound listener automatically:
45
45
 
46
46
  ``` ruby
47
47
  service "application" do
48
- include Falcon::Environment::Cluster
48
+ include Falcon::Environment::Server
49
49
  include Async::Service::Supervisor::Envoy::Supervised
50
50
  end
51
51
  ```
52
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.
53
+ `Falcon::Environment::Server` reports one listener shared by all of its workers, while `Falcon::Environment::Cluster` reports the listener bound independently by each worker. Falcon describes either 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.
54
+
55
+ Addresses belonging to one listener remain grouped as one Envoy load-balancer endpoint. When several workers report the same shared listener, the monitor publishes it once and keeps it available while any reporting worker is healthy.
54
56
 
55
57
  ## Monitor Usage
56
58
 
@@ -7,8 +7,23 @@ module Async
7
7
  module Service
8
8
  module Supervisor
9
9
  module Envoy
10
- # Represents one upstream endpoint published to Envoy EDS.
10
+ # Represents an upstream endpoint published to Envoy EDS.
11
11
  class Endpoint
12
+ # Build an immutable endpoint.
13
+ # @parameter name [String] The upstream cluster name.
14
+ # @parameter scheme [String | Symbol] The upstream application scheme.
15
+ # @parameter protocols [Array(String)] The supported upstream HTTP protocol names.
16
+ # @parameter addresses [Array(Hash)] The grouped concrete addresses.
17
+ # @returns [Endpoint] The immutable endpoint.
18
+ def self.build(name:, scheme:, protocols:, addresses:)
19
+ new(
20
+ name.to_s,
21
+ scheme.to_sym,
22
+ protocols.map(&:to_s).uniq,
23
+ addresses,
24
+ ).tap(&:freeze)
25
+ end
26
+
12
27
  # Wrap serialized endpoint state.
13
28
  # @parameter value [Endpoint | Hash] The value to wrap.
14
29
  # @returns [Endpoint] The endpoint value.
@@ -17,39 +32,26 @@ module Async
17
32
  when self
18
33
  value
19
34
  when Hash
20
- new(**value)
35
+ build(**value)
21
36
  else
22
37
  raise ArgumentError, "Invalid Envoy endpoint: #{value.inspect}"
23
38
  end
24
39
  end
25
40
 
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
41
  # Initialize an endpoint.
42
42
  # @parameter name [String] The upstream cluster name.
43
- # @parameter scheme [String | Symbol] The upstream application scheme.
43
+ # @parameter scheme [Symbol] The upstream application scheme.
44
44
  # @parameter protocols [Array(String)] The supported upstream HTTP protocol names.
45
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
46
+ def initialize(name, scheme, protocols, addresses)
47
+ @name = name
48
+ @scheme = scheme
49
+ @protocols = protocols
50
50
  raise ArgumentError, "An endpoint requires at least one protocol!" if @protocols.empty?
51
- @addresses = addresses.map{|value| self.class.normalize_address(value)}.freeze
51
+ @addresses = addresses
52
52
  raise ArgumentError, "An endpoint requires at least one address!" if @addresses.empty?
53
+
54
+ @hash = nil
53
55
  end
54
56
 
55
57
  # @attribute [String] The upstream cluster name.
@@ -64,6 +66,39 @@ module Async
64
66
  # @attribute [Array(Hash)] The grouped concrete addresses.
65
67
  attr :addresses
66
68
 
69
+ # Freeze this endpoint and cache its value hash.
70
+ def freeze
71
+ return self if frozen?
72
+
73
+ @name.freeze
74
+ @protocols.each(&:freeze).freeze
75
+ @addresses.each do |address|
76
+ address.each_value(&:freeze)
77
+ address.freeze
78
+ end.freeze
79
+ @hash = self.hash
80
+
81
+ super
82
+ end
83
+
84
+ # Compare this endpoint with another endpoint by value.
85
+ # @parameter other [Object] The object to compare.
86
+ # @returns [Boolean] Whether both endpoints have identical values.
87
+ def eql?(other)
88
+ other.instance_of?(self.class) &&
89
+ @name == other.name &&
90
+ @scheme == other.scheme &&
91
+ @protocols == other.protocols &&
92
+ @addresses == other.addresses
93
+ end
94
+
95
+ alias == eql?
96
+
97
+ # Compute the value hash used when grouping endpoints.
98
+ # @returns [Integer] The endpoint value hash.
99
+ def hash
100
+ @hash || [self.class, @name, @scheme, @protocols, @addresses].hash
101
+ end
67
102
  end
68
103
  end
69
104
  end
@@ -0,0 +1,63 @@
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
+ # Groups the workers reporting a single upstream endpoint.
11
+ class EndpointGroup
12
+ # Initialize an endpoint group.
13
+ # @parameter endpoint [Endpoint] The endpoint shared by the workers.
14
+ def initialize(endpoint)
15
+ @endpoint = endpoint
16
+ @workers = {}
17
+ end
18
+
19
+ # @attribute [Endpoint] The endpoint shared by the workers.
20
+ attr :endpoint
21
+
22
+ # Add or update a worker report.
23
+ # @parameter worker [SupervisorController] The worker reporting the endpoint.
24
+ # @parameter healthy [Boolean] Whether the worker considers the endpoint healthy.
25
+ def add(worker, healthy:)
26
+ @workers[worker.id] = healthy
27
+ end
28
+
29
+ # Remove a worker report.
30
+ # @parameter worker [SupervisorController] The worker to remove.
31
+ # @returns [Boolean | Nil] The removed health value, if present.
32
+ def remove(worker)
33
+ @workers.delete(worker.id)
34
+ end
35
+
36
+ # Count the workers reporting this endpoint.
37
+ # @returns [Integer] The number of reporting workers.
38
+ def size
39
+ @workers.size
40
+ end
41
+
42
+ # Determine whether any workers report this endpoint.
43
+ # @returns [Boolean] Whether the group has no workers.
44
+ def empty?
45
+ @workers.empty?
46
+ end
47
+
48
+ # Determine the aggregate endpoint health.
49
+ # @returns [Boolean] Whether any reporting worker is healthy.
50
+ def healthy?
51
+ @workers.each_value.any?
52
+ end
53
+
54
+ # Convert the group to an xDS endpoint description.
55
+ # @returns [Hash] The endpoint addresses and aggregate health.
56
+ def as_json
57
+ {addresses: @endpoint.addresses, healthy: healthy?}
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -10,6 +10,7 @@ require "async/grpc/xds/server"
10
10
 
11
11
  require_relative "delegate"
12
12
  require_relative "endpoint"
13
+ require_relative "endpoint_group"
13
14
 
14
15
  module Async
15
16
  module Service
@@ -108,6 +109,7 @@ module Async
108
109
  {
109
110
  cluster: cluster.to_s,
110
111
  endpoint: endpoint,
112
+ worker: supervisor_controller,
111
113
  healthy: @delegate.healthy?(supervisor_controller, endpoint),
112
114
  }
113
115
  end
@@ -146,9 +148,14 @@ module Async
146
148
 
147
149
  def build_clusters(records_by_cluster = build_records_by_cluster)
148
150
  records_by_cluster.transform_values do |records|
149
- records.map do |record|
150
- {addresses: record[:endpoint].addresses, healthy: record[:healthy]}
151
+ groups = {}
152
+
153
+ records.each do |record|
154
+ group = groups[record[:endpoint]] ||= EndpointGroup.new(record[:endpoint])
155
+ group.add(record[:worker], healthy: record[:healthy])
151
156
  end
157
+
158
+ groups.each_value.map(&:as_json)
152
159
  end
153
160
  end
154
161
 
@@ -9,7 +9,7 @@ module Async
9
9
  module Service
10
10
  module Supervisor
11
11
  module Envoy
12
- # Registers post-bind cluster listeners with the supervisor.
12
+ # Registers bound Falcon listeners with the supervisor.
13
13
  module Supervised
14
14
  include Async::Service::Supervisor::Supervised
15
15
 
@@ -11,7 +11,7 @@ module Async
11
11
  module Supervisor
12
12
  # @namespace
13
13
  module Envoy
14
- VERSION = "0.0.1"
14
+ VERSION = "0.1.0"
15
15
  end
16
16
  end
17
17
  end
data/readme.md CHANGED
@@ -24,6 +24,10 @@ Please see the [project documentation](https://socketry.github.io/async-service-
24
24
 
25
25
  Please see the [project releases](https://socketry.github.io/async-service-supervisor-envoy/releases/index) for all releases.
26
26
 
27
+ ### v0.1.0
28
+
29
+ - Deduplicate immutable endpoint values reported by multiple supervised workers and aggregate their health.
30
+
27
31
  ### v0.0.1
28
32
 
29
33
  - Register concrete Falcon cluster listeners as Envoy upstream endpoint state after binding.
@@ -32,7 +36,7 @@ Please see the [project releases](https://socketry.github.io/async-service-super
32
36
 
33
37
  ### v0.1.0
34
38
 
35
- - Initial release.
39
+ - Deduplicate immutable endpoint values reported by multiple supervised workers and aggregate their health.
36
40
 
37
41
  ## See Also
38
42
 
data/releases.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Releases
2
2
 
3
+ ## v0.1.0
4
+
5
+ - Deduplicate immutable endpoint values reported by multiple supervised workers and aggregate their health.
6
+
3
7
  ## v0.0.1
4
8
 
5
9
  - Register concrete Falcon cluster listeners as Envoy upstream endpoint state after binding.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-service-supervisor-envoy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -104,6 +104,7 @@ files:
104
104
  - lib/async/service/supervisor/envoy.rb
105
105
  - lib/async/service/supervisor/envoy/delegate.rb
106
106
  - lib/async/service/supervisor/envoy/endpoint.rb
107
+ - lib/async/service/supervisor/envoy/endpoint_group.rb
107
108
  - lib/async/service/supervisor/envoy/monitor.rb
108
109
  - lib/async/service/supervisor/envoy/supervised.rb
109
110
  - lib/async/service/supervisor/envoy/version.rb
metadata.gz.sig CHANGED
Binary file