nonnative 3.11.0 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.circleci/config.yml +2 -2
- data/Gemfile.lock +1 -1
- data/README.md +48 -9
- data/lib/nonnative/configuration.rb +3 -3
- data/lib/nonnative/configuration_process.rb +13 -4
- data/lib/nonnative/configuration_readiness.rb +35 -5
- data/lib/nonnative/configuration_service.rb +24 -0
- data/lib/nonnative/configuration_service_readiness.rb +48 -0
- data/lib/nonnative/grpc_health.rb +59 -0
- data/lib/nonnative/grpc_probe.rb +49 -0
- data/lib/nonnative/http_probe.rb +4 -3
- data/lib/nonnative/pool.rb +19 -1
- data/lib/nonnative/ports.rb +18 -3
- data/lib/nonnative/service.rb +14 -0
- data/lib/nonnative/tcp_probe.rb +32 -0
- data/lib/nonnative/version.rb +1 -1
- data/lib/nonnative.rb +17 -5
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 304a9d9d01fafb9f0822e4510c647f87095f78987feecb5a9d2eddc43317a880
|
|
4
|
+
data.tar.gz: 366157abd34f6ffa48cce159dffd49075e4a98653ea5d3f3b11b2494ad47b4a1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6fa7d92393e3a4df9f400a7f35c857998c13dc026ec85be407fa37d6e8ba2189c5992af70e001c1cdfaf9589d1b6d33a2daf11abf8361f84f07ab8bc3a72fed1
|
|
7
|
+
data.tar.gz: bf37673b803ff66338009fbbd458d707c6eed9ef0ba618bb53b97e2f5770788708343f6bd34b67758fd6d1a13e464f63edffced76c26591f8ac4fe4ec2267a3e
|
data/.circleci/config.yml
CHANGED
|
@@ -14,10 +14,10 @@ commands:
|
|
|
14
14
|
executors:
|
|
15
15
|
ruby:
|
|
16
16
|
docker:
|
|
17
|
-
- image: alexfalkowski/ruby:3.
|
|
17
|
+
- image: alexfalkowski/ruby:3.19
|
|
18
18
|
release:
|
|
19
19
|
docker:
|
|
20
|
-
- image: alexfalkowski/release:8.
|
|
20
|
+
- image: alexfalkowski/release:8.19
|
|
21
21
|
alpine:
|
|
22
22
|
docker:
|
|
23
23
|
- image: alpine:latest
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -11,7 +11,7 @@ It helps you:
|
|
|
11
11
|
- start **OS processes** (e.g. your Go/Java/Rust service binary),
|
|
12
12
|
- start **in-process Ruby servers** (e.g. small HTTP/TCP/gRPC fakes for dependencies),
|
|
13
13
|
- optionally start **service proxies** for fault-injection in front of externally managed dependencies,
|
|
14
|
-
- wait for readiness/shutdown using **TCP port checks
|
|
14
|
+
- wait for readiness/shutdown using **TCP port checks**, optional process HTTP/gRPC checks, and optional service TCP checks.
|
|
15
15
|
|
|
16
16
|
Once started, you can test however you like (TCP, HTTP, gRPC, etc).
|
|
17
17
|
|
|
@@ -79,15 +79,20 @@ Process/server fields:
|
|
|
79
79
|
- `log`: per-runner log file used by process output redirection or server implementations.
|
|
80
80
|
|
|
81
81
|
Process-only fields:
|
|
82
|
-
- `readiness`: optional
|
|
82
|
+
- `readiness`: optional list of startup readiness checks. Supported kinds are `http` and `grpc`.
|
|
83
|
+
HTTP checks require explicit `port` and path-only `path`. gRPC checks require explicit `port` and
|
|
84
|
+
health `service`.
|
|
83
85
|
|
|
84
86
|
Service fields:
|
|
85
|
-
- `port`: client-facing service port.
|
|
87
|
+
- `port`: client-facing service port.
|
|
88
|
+
- `timeout`: max time (seconds) for opt-in service readiness checks. Defaults to `1.0`.
|
|
89
|
+
- `readiness`: optional list of startup readiness checks. Supported kind is `tcp`, which requires
|
|
90
|
+
explicit `host` and `port`.
|
|
86
91
|
|
|
87
|
-
Nonnative readiness and shutdown checks are TCP port checks by default. Configure process/server ports that are dedicated to the test run; if another process is already listening on the same endpoint, results are undefined. Processes can also opt into
|
|
92
|
+
Nonnative readiness and shutdown checks are TCP port checks by default. Configure process/server ports that are dedicated to the test run; if another process is already listening on the same endpoint, results are undefined. Processes can also opt into HTTP and gRPC readiness checks that run after TCP readiness succeeds. Services do not get automatic TCP readiness/shutdown checks, but can opt into TCP startup readiness for externally managed dependencies. HTTP readiness paths must be path-only values, such as `/test/readyz`; absolute URLs and scheme-relative URLs are rejected.
|
|
88
93
|
|
|
89
94
|
> [!WARNING]
|
|
90
|
-
> TCP readiness and shutdown checks only prove that a TCP port opened or closed. HTTP readiness
|
|
95
|
+
> TCP readiness and shutdown checks only prove that a TCP port opened or closed. HTTP and gRPC readiness are process-only. Service readiness is TCP-only and should target the dependency endpoint that must be reachable before managed servers/processes start.
|
|
91
96
|
|
|
92
97
|
Start and stop Nonnative around the test scope that should own the configured runners:
|
|
93
98
|
|
|
@@ -162,6 +167,19 @@ response = Nonnative.observability.health(
|
|
|
162
167
|
expect(response.code).to eq(200)
|
|
163
168
|
```
|
|
164
169
|
|
|
170
|
+
`Nonnative.grpc_health` is a helper for the standard gRPC health checking protocol:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
health = Nonnative.grpc_health(
|
|
174
|
+
host: '127.0.0.1',
|
|
175
|
+
port: 12_322,
|
|
176
|
+
service: 'example.v1.ExampleService',
|
|
177
|
+
timeout: 2
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
expect(health.serving?).to eq(true)
|
|
181
|
+
```
|
|
182
|
+
|
|
165
183
|
### 🔁 Lifecycle strategies (Cucumber integration)
|
|
166
184
|
|
|
167
185
|
Nonnative ships Cucumber hooks (when loaded) that support these tags/strategies:
|
|
@@ -210,7 +228,10 @@ Nonnative.configure do |config|
|
|
|
210
228
|
p.ports = [12_321]
|
|
211
229
|
p.log = '12_321.log'
|
|
212
230
|
p.signal = 'INT' # Possible values are described in Signal.list.keys.
|
|
213
|
-
p.readiness =
|
|
231
|
+
p.readiness = [
|
|
232
|
+
{ kind: 'http', port: 12_321, path: '/test/readyz' },
|
|
233
|
+
{ kind: 'grpc', port: 12_322, service: 'example.v1.ExampleService' }
|
|
234
|
+
]
|
|
214
235
|
p.environment = { # Pass environment variables to process.
|
|
215
236
|
'TEST' => 'true'
|
|
216
237
|
}
|
|
@@ -247,8 +268,12 @@ processes:
|
|
|
247
268
|
log: 12_321.log
|
|
248
269
|
signal: INT # Possible values are described in Signal.list.keys.
|
|
249
270
|
readiness:
|
|
250
|
-
|
|
251
|
-
|
|
271
|
+
- kind: http
|
|
272
|
+
port: 12321
|
|
273
|
+
path: /test/readyz
|
|
274
|
+
- kind: grpc
|
|
275
|
+
port: 12322
|
|
276
|
+
service: example.v1.ExampleService
|
|
252
277
|
environment: # Pass environment variables to process.
|
|
253
278
|
TEST: true
|
|
254
279
|
-
|
|
@@ -593,7 +618,7 @@ end
|
|
|
593
618
|
|
|
594
619
|
A service is an external dependency to your system that you **do not** want Nonnative to start (no OS process, no Ruby thread).
|
|
595
620
|
|
|
596
|
-
Services do not get process lifecycle management or TCP readiness/shutdown checks from Nonnative. They provide a named endpoint for a dependency that another tool already manages.
|
|
621
|
+
Services do not get process lifecycle management or automatic TCP readiness/shutdown checks from Nonnative. They provide a named endpoint for a dependency that another tool already manages, and can opt into TCP startup readiness when the dependency must be reachable before managed servers/processes start.
|
|
597
622
|
|
|
598
623
|
Set it up programmatically:
|
|
599
624
|
|
|
@@ -610,6 +635,8 @@ Nonnative.configure do |config|
|
|
|
610
635
|
s.name = 'postgres'
|
|
611
636
|
s.host = '127.0.0.1'
|
|
612
637
|
s.port = 5432
|
|
638
|
+
s.timeout = 5
|
|
639
|
+
s.readiness = [{ kind: 'tcp', host: '127.0.0.1', port: 5432 }]
|
|
613
640
|
end
|
|
614
641
|
|
|
615
642
|
config.service do |s|
|
|
@@ -632,6 +659,11 @@ services:
|
|
|
632
659
|
name: postgres
|
|
633
660
|
host: 127.0.0.1
|
|
634
661
|
port: 5432
|
|
662
|
+
timeout: 5
|
|
663
|
+
readiness:
|
|
664
|
+
- kind: tcp
|
|
665
|
+
host: 127.0.0.1
|
|
666
|
+
port: 5432
|
|
635
667
|
-
|
|
636
668
|
name: redis
|
|
637
669
|
host: 127.0.0.1
|
|
@@ -665,6 +697,7 @@ Nonnative.proxies['custom'] = CustomProxy
|
|
|
665
697
|
```
|
|
666
698
|
|
|
667
699
|
Only services support proxies. For `fault_injection`, keep the service `host`/`port` as the client-facing proxy endpoint and use nested `proxy.host`/`proxy.port` for the upstream target behind the proxy.
|
|
700
|
+
When service readiness is configured for a proxied dependency, set the readiness `host`/`port` to the upstream dependency, not the client-facing proxy listener.
|
|
668
701
|
|
|
669
702
|
##### 🧩 Service Proxies
|
|
670
703
|
|
|
@@ -688,6 +721,8 @@ config.service do |s|
|
|
|
688
721
|
delay: 5
|
|
689
722
|
}
|
|
690
723
|
}
|
|
724
|
+
|
|
725
|
+
s.readiness = [{ kind: 'tcp', host: '127.0.0.1', port: 6379 }]
|
|
691
726
|
end
|
|
692
727
|
```
|
|
693
728
|
|
|
@@ -701,6 +736,10 @@ services:
|
|
|
701
736
|
name: redis
|
|
702
737
|
host: 127.0.0.1
|
|
703
738
|
port: 16379
|
|
739
|
+
readiness:
|
|
740
|
+
- kind: tcp
|
|
741
|
+
host: 127.0.0.1
|
|
742
|
+
port: 6379
|
|
704
743
|
proxy:
|
|
705
744
|
kind: fault_injection
|
|
706
745
|
host: 127.0.0.1
|
|
@@ -22,7 +22,7 @@ module Nonnative
|
|
|
22
22
|
# p.ports = [8080, 9090]
|
|
23
23
|
# p.timeout = 10
|
|
24
24
|
# p.log = 'api.log'
|
|
25
|
-
# p.readiness = { port: 8080, path: '/example/readyz' }
|
|
25
|
+
# p.readiness = [{ kind: 'http', port: 8080, path: '/example/readyz' }]
|
|
26
26
|
# end
|
|
27
27
|
# end
|
|
28
28
|
#
|
|
@@ -177,11 +177,11 @@ module Nonnative
|
|
|
177
177
|
def add_services(cfg)
|
|
178
178
|
services = cfg.services || []
|
|
179
179
|
services.each do |loaded_service|
|
|
180
|
-
reject_readiness(loaded_service, 'services')
|
|
181
|
-
|
|
182
180
|
service do |service_config|
|
|
183
181
|
service_config.name = loaded_service.name
|
|
184
182
|
service_config.host = loaded_service.host if loaded_service.host
|
|
183
|
+
service_config.timeout = loaded_service.timeout if loaded_service.timeout
|
|
184
|
+
service_config.readiness = loaded_service.readiness if loaded_service.readiness
|
|
185
185
|
assign_service_port(service_config, loaded_service)
|
|
186
186
|
|
|
187
187
|
assign_proxy(service_config, loaded_service.proxy)
|
|
@@ -28,7 +28,7 @@ module Nonnative
|
|
|
28
28
|
# @return [Hash, nil] environment variables to pass to the spawned process
|
|
29
29
|
attr_accessor :environment
|
|
30
30
|
|
|
31
|
-
# @return [Nonnative::ConfigurationReadiness
|
|
31
|
+
# @return [Array<Nonnative::ConfigurationReadiness>] optional readiness checks
|
|
32
32
|
attr_reader :readiness
|
|
33
33
|
|
|
34
34
|
# Creates a process configuration with bounded lifecycle defaults.
|
|
@@ -41,14 +41,23 @@ module Nonnative
|
|
|
41
41
|
super
|
|
42
42
|
|
|
43
43
|
self.timeout = DEFAULT_TIMEOUT
|
|
44
|
+
self.readiness = []
|
|
44
45
|
end
|
|
45
46
|
|
|
46
|
-
# Sets optional
|
|
47
|
+
# Sets optional process readiness checks.
|
|
47
48
|
#
|
|
48
|
-
# @param value [Hash, #to_h
|
|
49
|
+
# @param value [Array<Hash, #to_h>, nil] readiness checks with required `kind` and `port`
|
|
49
50
|
# @return [void]
|
|
50
51
|
def readiness=(value)
|
|
51
|
-
@readiness = value.nil? ?
|
|
52
|
+
@readiness = value.nil? ? [] : build_readiness(value)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def build_readiness(value)
|
|
58
|
+
raise ArgumentError, 'Process readiness must be a list of checks' unless value.is_a?(Array)
|
|
59
|
+
|
|
60
|
+
value.map { |check| Nonnative::ConfigurationReadiness.new(check) }
|
|
52
61
|
end
|
|
53
62
|
end
|
|
54
63
|
end
|
|
@@ -1,26 +1,44 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
-
#
|
|
4
|
+
# Readiness check configuration for a managed process.
|
|
5
5
|
#
|
|
6
|
-
# Readiness is optional. When present,
|
|
7
|
-
#
|
|
6
|
+
# Readiness is optional. When present, each check declares a `kind` and explicit endpoint details
|
|
7
|
+
# to poll after TCP readiness succeeds.
|
|
8
8
|
class ConfigurationReadiness
|
|
9
|
-
|
|
9
|
+
KINDS = %w[http grpc].freeze
|
|
10
|
+
|
|
11
|
+
# @return [String] readiness check kind
|
|
12
|
+
attr_accessor :kind
|
|
13
|
+
|
|
14
|
+
# @return [Integer] process readiness port
|
|
10
15
|
attr_accessor :port
|
|
11
16
|
|
|
12
17
|
# @return [String] path-only HTTP readiness path
|
|
13
18
|
attr_accessor :path
|
|
14
19
|
|
|
15
|
-
# @
|
|
20
|
+
# @return [String] gRPC health service name
|
|
21
|
+
attr_accessor :service
|
|
22
|
+
|
|
23
|
+
# @param value [Hash, #to_h] readiness check attributes
|
|
16
24
|
def initialize(value)
|
|
17
25
|
attributes = value.respond_to?(:to_h) ? value.to_h : value
|
|
26
|
+
self.kind = attribute(attributes, :kind)&.to_s
|
|
18
27
|
self.port = attribute(attributes, :port)
|
|
19
28
|
self.path = attribute(attributes, :path)
|
|
29
|
+
self.service = attribute(attributes, :service)
|
|
20
30
|
|
|
21
31
|
validate!
|
|
22
32
|
end
|
|
23
33
|
|
|
34
|
+
def http?
|
|
35
|
+
kind == 'http'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def grpc?
|
|
39
|
+
kind == 'grpc'
|
|
40
|
+
end
|
|
41
|
+
|
|
24
42
|
private
|
|
25
43
|
|
|
26
44
|
def attribute(attributes, name)
|
|
@@ -28,11 +46,23 @@ module Nonnative
|
|
|
28
46
|
end
|
|
29
47
|
|
|
30
48
|
def validate!
|
|
49
|
+
raise ArgumentError, "Process readiness requires 'kind'" if kind.nil?
|
|
50
|
+
raise ArgumentError, "Process readiness kind must be one of: #{KINDS.join(', ')}" unless KINDS.include?(kind)
|
|
31
51
|
raise ArgumentError, "Process readiness requires 'port'" if port.nil?
|
|
52
|
+
|
|
53
|
+
validate_http! if http?
|
|
54
|
+
validate_grpc! if grpc?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def validate_http!
|
|
32
58
|
raise ArgumentError, "Process readiness requires 'path'" if path.nil?
|
|
33
59
|
raise ArgumentError, 'Process readiness path must be path-only' unless path_only?
|
|
34
60
|
end
|
|
35
61
|
|
|
62
|
+
def validate_grpc!
|
|
63
|
+
raise ArgumentError, "Process readiness requires 'service'" if service.nil?
|
|
64
|
+
end
|
|
65
|
+
|
|
36
66
|
def path_only?
|
|
37
67
|
uri = URI.parse(path.to_s)
|
|
38
68
|
|
|
@@ -14,11 +14,17 @@ module Nonnative
|
|
|
14
14
|
# @return [Integer] client-facing port used by the service proxy
|
|
15
15
|
attr_accessor :port
|
|
16
16
|
|
|
17
|
+
# @return [Numeric] readiness timeout (seconds) used when waiting for service readiness
|
|
18
|
+
attr_accessor :timeout
|
|
19
|
+
|
|
17
20
|
# Proxy configuration for this service.
|
|
18
21
|
#
|
|
19
22
|
# @return [Nonnative::ConfigurationProxy]
|
|
20
23
|
attr_reader :proxy
|
|
21
24
|
|
|
25
|
+
# @return [Array<Nonnative::ConfigurationServiceReadiness>] optional service readiness checks
|
|
26
|
+
attr_reader :readiness
|
|
27
|
+
|
|
22
28
|
# Creates a service configuration with defaults.
|
|
23
29
|
#
|
|
24
30
|
# @return [void]
|
|
@@ -26,9 +32,19 @@ module Nonnative
|
|
|
26
32
|
super
|
|
27
33
|
|
|
28
34
|
self.port = 0
|
|
35
|
+
self.timeout = DEFAULT_TIMEOUT
|
|
36
|
+
@readiness = []
|
|
29
37
|
@proxy = Nonnative::ConfigurationProxy.new
|
|
30
38
|
end
|
|
31
39
|
|
|
40
|
+
# Sets optional service readiness checks.
|
|
41
|
+
#
|
|
42
|
+
# @param value [Array<Hash, #to_h>, nil] readiness checks with required `kind`, `host`, and `port`
|
|
43
|
+
# @return [void]
|
|
44
|
+
def readiness=(value)
|
|
45
|
+
@readiness = value.nil? ? [] : build_readiness(value)
|
|
46
|
+
end
|
|
47
|
+
|
|
32
48
|
# Sets proxy configuration using a hash-like value.
|
|
33
49
|
#
|
|
34
50
|
# This is primarily used when loading YAML configuration files, where proxy attributes are
|
|
@@ -67,5 +83,13 @@ module Nonnative
|
|
|
67
83
|
def ports=(_value)
|
|
68
84
|
raise ArgumentError, "Use 'port' instead of 'ports' for service '#{name}'"
|
|
69
85
|
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def build_readiness(value)
|
|
90
|
+
raise ArgumentError, 'Service readiness must be a list of checks' unless value.is_a?(Array)
|
|
91
|
+
|
|
92
|
+
value.map { |check| Nonnative::ConfigurationServiceReadiness.new(check) }
|
|
93
|
+
end
|
|
70
94
|
end
|
|
71
95
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nonnative
|
|
4
|
+
# Service readiness check configuration.
|
|
5
|
+
#
|
|
6
|
+
# Service readiness is for externally managed dependencies. The TCP target should be the dependency
|
|
7
|
+
# endpoint that must be reachable before managed servers and processes start.
|
|
8
|
+
class ConfigurationServiceReadiness
|
|
9
|
+
KINDS = %w[tcp].freeze
|
|
10
|
+
|
|
11
|
+
# @return [String] readiness check kind
|
|
12
|
+
attr_reader :kind
|
|
13
|
+
|
|
14
|
+
# @return [String] readiness target host
|
|
15
|
+
attr_reader :host
|
|
16
|
+
|
|
17
|
+
# @return [Integer] readiness target port
|
|
18
|
+
attr_reader :port
|
|
19
|
+
|
|
20
|
+
# @param value [Hash, #to_h] readiness check attributes
|
|
21
|
+
# @return [void]
|
|
22
|
+
def initialize(value)
|
|
23
|
+
attributes = value.to_h.transform_keys(&:to_sym)
|
|
24
|
+
|
|
25
|
+
@kind = attributes[:kind]&.to_s
|
|
26
|
+
@host = attributes[:host]
|
|
27
|
+
@port = attributes[:port]
|
|
28
|
+
|
|
29
|
+
validate!
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns whether this is a TCP readiness check.
|
|
33
|
+
#
|
|
34
|
+
# @return [Boolean]
|
|
35
|
+
def tcp?
|
|
36
|
+
kind == 'tcp'
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def validate!
|
|
42
|
+
raise ArgumentError, "Service readiness requires 'kind'" if kind.nil?
|
|
43
|
+
raise ArgumentError, "Service readiness kind must be one of: #{KINDS.join(', ')}" unless KINDS.include?(kind)
|
|
44
|
+
raise ArgumentError, "Service readiness requires 'host'" if host.nil?
|
|
45
|
+
raise ArgumentError, "Service readiness requires 'port'" if port.nil?
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nonnative
|
|
4
|
+
# Client helper for the standard gRPC health checking protocol.
|
|
5
|
+
class GRPCHealth
|
|
6
|
+
NETWORK_ERRORS = [
|
|
7
|
+
GRPC::BadStatus,
|
|
8
|
+
GRPC::Core::CallError
|
|
9
|
+
].freeze
|
|
10
|
+
|
|
11
|
+
# @param host [String] gRPC server host
|
|
12
|
+
# @param port [Integer] gRPC server port
|
|
13
|
+
# @param service [String] gRPC health service name
|
|
14
|
+
# @param timeout [Numeric] default call timeout in seconds
|
|
15
|
+
def initialize(host:, port:, service:, timeout: 1)
|
|
16
|
+
@host = host
|
|
17
|
+
@port = port
|
|
18
|
+
@service = service.to_s
|
|
19
|
+
@timeout = timeout
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Calls the gRPC health check endpoint.
|
|
23
|
+
#
|
|
24
|
+
# @param deadline [Time] gRPC deadline
|
|
25
|
+
# @return [Grpc::Health::V1::HealthCheckResponse]
|
|
26
|
+
def check(deadline: Time.now + timeout)
|
|
27
|
+
stub.check(request, deadline: deadline)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns true when the gRPC health endpoint reports SERVING.
|
|
31
|
+
#
|
|
32
|
+
# @param deadline [Time] gRPC deadline
|
|
33
|
+
# @return [Boolean]
|
|
34
|
+
def serving?(deadline: Time.now + timeout)
|
|
35
|
+
check(deadline: deadline).status == :SERVING
|
|
36
|
+
rescue *NETWORK_ERRORS
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns the checked gRPC health endpoint for lifecycle diagnostics.
|
|
41
|
+
#
|
|
42
|
+
# @return [String]
|
|
43
|
+
def endpoint
|
|
44
|
+
service.empty? ? "grpc://#{host}:#{port}" : "grpc://#{host}:#{port}/#{service}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
attr_reader :host, :port, :service, :timeout
|
|
50
|
+
|
|
51
|
+
def request
|
|
52
|
+
Grpc::Health::V1::HealthCheckRequest.new(service: service)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def stub
|
|
56
|
+
@stub ||= Grpc::Health::V1::Health::Stub.new("#{host}:#{port}", :this_channel_is_insecure)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nonnative
|
|
4
|
+
# Probes a managed process gRPC health endpoint.
|
|
5
|
+
class GRPCProbe
|
|
6
|
+
# @param process [Nonnative::ConfigurationProcess] process configuration
|
|
7
|
+
# @param readiness [Nonnative::ConfigurationReadiness] gRPC readiness attributes
|
|
8
|
+
def initialize(process, readiness)
|
|
9
|
+
@health = Nonnative::GRPCHealth.new(
|
|
10
|
+
host: process.host,
|
|
11
|
+
port: readiness.port,
|
|
12
|
+
service: readiness.service,
|
|
13
|
+
timeout: process.timeout
|
|
14
|
+
)
|
|
15
|
+
@timeout = Nonnative::Timeout.new(process.timeout)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Returns whether the configured gRPC health endpoint reports SERVING before timeout.
|
|
19
|
+
#
|
|
20
|
+
# @return [Boolean]
|
|
21
|
+
def ready?
|
|
22
|
+
Nonnative.logger.info "checking if readiness '#{endpoint}' is ready"
|
|
23
|
+
|
|
24
|
+
timeout.perform do
|
|
25
|
+
raise Nonnative::Error unless health.serving?
|
|
26
|
+
|
|
27
|
+
true
|
|
28
|
+
rescue Nonnative::Error
|
|
29
|
+
sleep_interval
|
|
30
|
+
retry
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns the gRPC health endpoint for lifecycle diagnostics.
|
|
35
|
+
#
|
|
36
|
+
# @return [String]
|
|
37
|
+
def endpoint
|
|
38
|
+
health.endpoint
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
attr_reader :health, :timeout
|
|
44
|
+
|
|
45
|
+
def sleep_interval
|
|
46
|
+
sleep 0.01
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/nonnative/http_probe.rb
CHANGED
|
@@ -12,9 +12,10 @@ module Nonnative
|
|
|
12
12
|
RestClient::ServerBrokeConnection
|
|
13
13
|
].freeze
|
|
14
14
|
|
|
15
|
-
# @param process [Nonnative::ConfigurationProcess] process configuration
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
# @param process [Nonnative::ConfigurationProcess] process configuration
|
|
16
|
+
# @param readiness [Nonnative::ConfigurationReadiness] HTTP readiness attributes
|
|
17
|
+
def initialize(process, readiness)
|
|
18
|
+
@readiness = readiness
|
|
18
19
|
@base_url = "http://#{process.host}:#{readiness.port}"
|
|
19
20
|
@timeout = Nonnative::Timeout.new(process.timeout)
|
|
20
21
|
|
data/lib/nonnative/pool.rb
CHANGED
|
@@ -25,7 +25,8 @@ module Nonnative
|
|
|
25
25
|
|
|
26
26
|
# Starts all configured runners and yields results for each process/server.
|
|
27
27
|
#
|
|
28
|
-
# Services are started first (proxy-only), then servers and processes are
|
|
28
|
+
# Services are started first (proxy-only) and checked for opt-in readiness, then servers and processes are
|
|
29
|
+
# started and checked for readiness.
|
|
29
30
|
#
|
|
30
31
|
# @yieldparam name [String, nil] runner name
|
|
31
32
|
# @yieldparam values [Object] runner-specific return value from `start` (e.g. `[pid, running]` for processes)
|
|
@@ -36,6 +37,10 @@ module Nonnative
|
|
|
36
37
|
errors = []
|
|
37
38
|
|
|
38
39
|
errors.concat(service_lifecycle(services, :start, :start))
|
|
40
|
+
service_readiness_errors = check_service_readiness(services)
|
|
41
|
+
errors.concat(service_readiness_errors)
|
|
42
|
+
return errors if service_readiness_errors.any?
|
|
43
|
+
|
|
39
44
|
[servers, processes].each { |runners| errors.concat(run_lifecycle_checks(runners, :start, :open?, :start, &)) }
|
|
40
45
|
|
|
41
46
|
errors
|
|
@@ -180,6 +185,15 @@ module Nonnative
|
|
|
180
185
|
end
|
|
181
186
|
end
|
|
182
187
|
|
|
188
|
+
def check_service_readiness(all)
|
|
189
|
+
all.each_with_object([]) do |service, errors|
|
|
190
|
+
next unless service.respond_to?(:readiness)
|
|
191
|
+
|
|
192
|
+
checks = service.readiness.map { |readiness| Nonnative::TCPProbe.new(readiness, timeout: service.timeout) }
|
|
193
|
+
errors << service_readiness_error(service, checks) unless checks.all?(&:ready?)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
183
197
|
def run_lifecycle_checks(runners, lifecycle_method, port_method, action, &)
|
|
184
198
|
checks = []
|
|
185
199
|
errors = []
|
|
@@ -220,6 +234,10 @@ module Nonnative
|
|
|
220
234
|
"#{check.capitalize} check failed for #{runner_name(type)}: #{error.class} - #{error.message}"
|
|
221
235
|
end
|
|
222
236
|
|
|
237
|
+
def service_readiness_error(service, checks)
|
|
238
|
+
"Started #{runner_name(service)}, though did not respond in time for readiness: #{checks.map(&:endpoint).join(', ')}"
|
|
239
|
+
end
|
|
240
|
+
|
|
223
241
|
def runner_name(type)
|
|
224
242
|
name = type.name
|
|
225
243
|
return "runner '#{name}'" if name
|
data/lib/nonnative/ports.rb
CHANGED
|
@@ -12,14 +12,14 @@ module Nonnative
|
|
|
12
12
|
def initialize(runner)
|
|
13
13
|
@runner = runner
|
|
14
14
|
@ports = runner.ports.map { |port| Nonnative::Port.new(runner, port) }
|
|
15
|
-
@readiness =
|
|
15
|
+
@readiness = readiness_probes
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
# Returns whether all configured ports become connectable before their timeouts elapse.
|
|
19
19
|
#
|
|
20
20
|
# @return [Boolean]
|
|
21
21
|
def open?
|
|
22
|
-
ports.all?(&:open?) &&
|
|
22
|
+
ports.all?(&:open?) && readiness.all?(&:ready?)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
# Returns whether all configured ports become non-connectable before their timeouts elapse.
|
|
@@ -41,7 +41,7 @@ module Nonnative
|
|
|
41
41
|
# @return [String]
|
|
42
42
|
def description
|
|
43
43
|
details = []
|
|
44
|
-
details << "readiness: #{readiness.endpoint}" if readiness
|
|
44
|
+
details << "readiness: #{readiness.map(&:endpoint).join(', ')}" if readiness.any?
|
|
45
45
|
log = runner.log if runner.respond_to?(:log)
|
|
46
46
|
details << "log: #{log}" if log
|
|
47
47
|
|
|
@@ -51,5 +51,20 @@ module Nonnative
|
|
|
51
51
|
private
|
|
52
52
|
|
|
53
53
|
attr_reader :ports, :readiness, :runner
|
|
54
|
+
|
|
55
|
+
def readiness_probes
|
|
56
|
+
return [] unless runner.respond_to?(:readiness)
|
|
57
|
+
|
|
58
|
+
runner.readiness.map { |check| readiness_probe(check) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def readiness_probe(check)
|
|
62
|
+
case check.kind
|
|
63
|
+
when 'http'
|
|
64
|
+
Nonnative::HTTPProbe.new(runner, check)
|
|
65
|
+
when 'grpc'
|
|
66
|
+
Nonnative::GRPCProbe.new(runner, check)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
54
69
|
end
|
|
55
70
|
end
|
data/lib/nonnative/service.rb
CHANGED
|
@@ -24,6 +24,20 @@ module Nonnative
|
|
|
24
24
|
@proxy = Nonnative::ProxyFactory.create(service)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Returns configured service readiness checks.
|
|
28
|
+
#
|
|
29
|
+
# @return [Array<Nonnative::ConfigurationServiceReadiness>]
|
|
30
|
+
def readiness
|
|
31
|
+
service.readiness
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns the configured service readiness timeout.
|
|
35
|
+
#
|
|
36
|
+
# @return [Numeric]
|
|
37
|
+
def timeout
|
|
38
|
+
service.timeout
|
|
39
|
+
end
|
|
40
|
+
|
|
27
41
|
# Starts the configured proxy (if any).
|
|
28
42
|
#
|
|
29
43
|
# @return [void]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nonnative
|
|
4
|
+
# Probes a TCP readiness endpoint.
|
|
5
|
+
class TCPProbe
|
|
6
|
+
Target = Struct.new(:host, :timeout, keyword_init: true)
|
|
7
|
+
|
|
8
|
+
# @param readiness [#host, #port] TCP readiness attributes
|
|
9
|
+
# @param timeout [Numeric] maximum time to wait for the TCP endpoint
|
|
10
|
+
def initialize(readiness, timeout:)
|
|
11
|
+
@port = Nonnative::Port.new(Target.new(host: readiness.host, timeout:), readiness.port)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Returns whether the TCP endpoint becomes connectable before the timeout elapses.
|
|
15
|
+
#
|
|
16
|
+
# @return [Boolean]
|
|
17
|
+
def ready?
|
|
18
|
+
port.open?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Returns the checked endpoint for lifecycle diagnostics.
|
|
22
|
+
#
|
|
23
|
+
# @return [String]
|
|
24
|
+
def endpoint
|
|
25
|
+
port.endpoint
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
attr_reader :port
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/nonnative/version.rb
CHANGED
data/lib/nonnative.rb
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# It can:
|
|
7
7
|
#
|
|
8
8
|
# - start external processes and in-process servers
|
|
9
|
-
# - wait for readiness via port checks
|
|
9
|
+
# - wait for readiness via port checks, optional process HTTP/gRPC readiness, and optional service TCP readiness
|
|
10
10
|
# - optionally run fault-injection proxies in front of services
|
|
11
11
|
#
|
|
12
12
|
# The public entry points are exposed as module-level methods on {Nonnative}.
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
# p.ports = [8080, 9090]
|
|
28
28
|
# p.timeout = 10
|
|
29
29
|
# p.log = 'api.log'
|
|
30
|
-
# p.readiness = { port: 8080, path: '/example/readyz' }
|
|
30
|
+
# p.readiness = [{ kind: 'http', port: 8080, path: '/example/readyz' }]
|
|
31
31
|
# end
|
|
32
32
|
# end
|
|
33
33
|
#
|
|
@@ -51,6 +51,7 @@ require 'shellwords'
|
|
|
51
51
|
require 'uri'
|
|
52
52
|
|
|
53
53
|
require 'grpc'
|
|
54
|
+
require 'grpc/health/v1/health_services_pb'
|
|
54
55
|
require 'sinatra'
|
|
55
56
|
require 'rest-client'
|
|
56
57
|
require 'retriable'
|
|
@@ -75,6 +76,7 @@ require 'nonnative/configuration_file'
|
|
|
75
76
|
require 'nonnative/configuration'
|
|
76
77
|
require 'nonnative/configuration_runner'
|
|
77
78
|
require 'nonnative/configuration_readiness'
|
|
79
|
+
require 'nonnative/configuration_service_readiness'
|
|
78
80
|
require 'nonnative/configuration_process'
|
|
79
81
|
require 'nonnative/configuration_server'
|
|
80
82
|
require 'nonnative/configuration_service'
|
|
@@ -85,7 +87,10 @@ require 'nonnative/server'
|
|
|
85
87
|
require 'nonnative/service'
|
|
86
88
|
require 'nonnative/pool'
|
|
87
89
|
require 'nonnative/http_client'
|
|
90
|
+
require 'nonnative/tcp_probe'
|
|
88
91
|
require 'nonnative/http_probe'
|
|
92
|
+
require 'nonnative/grpc_health'
|
|
93
|
+
require 'nonnative/grpc_probe'
|
|
89
94
|
require 'nonnative/http_server'
|
|
90
95
|
require 'nonnative/http_proxy_server'
|
|
91
96
|
require 'nonnative/grpc_server'
|
|
@@ -190,9 +195,16 @@ module Nonnative
|
|
|
190
195
|
# Returns an HTTP client for common health/readiness endpoints.
|
|
191
196
|
#
|
|
192
197
|
# @return [Nonnative::Observability]
|
|
193
|
-
def observability
|
|
194
|
-
|
|
195
|
-
|
|
198
|
+
def observability = (@observability ||= Nonnative::Observability.new(configuration.url))
|
|
199
|
+
|
|
200
|
+
# Returns a client helper for the standard gRPC health checking protocol.
|
|
201
|
+
#
|
|
202
|
+
# @param host [String] gRPC server host
|
|
203
|
+
# @param port [Integer] gRPC server port
|
|
204
|
+
# @param service [String] gRPC health service name
|
|
205
|
+
# @param timeout [Numeric] default call timeout in seconds
|
|
206
|
+
# @return [Nonnative::GRPCHealth]
|
|
207
|
+
def grpc_health(host:, port:, service:, timeout: 1) = Nonnative::GRPCHealth.new(host: host, port: port, service: service, timeout: timeout)
|
|
196
208
|
|
|
197
209
|
# Returns the configured proxy kinds mapped to proxy classes.
|
|
198
210
|
#
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nonnative
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alejandro Falkowski
|
|
@@ -296,11 +296,14 @@ files:
|
|
|
296
296
|
- lib/nonnative/configuration_runner.rb
|
|
297
297
|
- lib/nonnative/configuration_server.rb
|
|
298
298
|
- lib/nonnative/configuration_service.rb
|
|
299
|
+
- lib/nonnative/configuration_service_readiness.rb
|
|
299
300
|
- lib/nonnative/cucumber.rb
|
|
300
301
|
- lib/nonnative/delay_socket_pair.rb
|
|
301
302
|
- lib/nonnative/error.rb
|
|
302
303
|
- lib/nonnative/fault_injection_proxy.rb
|
|
303
304
|
- lib/nonnative/go_executable.rb
|
|
305
|
+
- lib/nonnative/grpc_health.rb
|
|
306
|
+
- lib/nonnative/grpc_probe.rb
|
|
304
307
|
- lib/nonnative/grpc_server.rb
|
|
305
308
|
- lib/nonnative/header.rb
|
|
306
309
|
- lib/nonnative/http_client.rb
|
|
@@ -325,6 +328,7 @@ files:
|
|
|
325
328
|
- lib/nonnative/start_error.rb
|
|
326
329
|
- lib/nonnative/startup.rb
|
|
327
330
|
- lib/nonnative/stop_error.rb
|
|
331
|
+
- lib/nonnative/tcp_probe.rb
|
|
328
332
|
- lib/nonnative/timeout.rb
|
|
329
333
|
- lib/nonnative/version.rb
|
|
330
334
|
- nonnative.gemspec
|