nonnative 3.6.0 → 3.9.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: 0fb25075ea94da70b097c6b60e7a47fea1a73b7e80d4903f32b2a2d00675b50e
4
- data.tar.gz: 33c1c4310fa48a1a8dacf792d9ce6af73f9a406f67185f61fb292f07d06d2345
3
+ metadata.gz: ef654d45571b49eaf151e67b60a8eaa5c7b6df2b1a2e877d774a30ec80cc3281
4
+ data.tar.gz: 3526ed68b510ee72ac7b2d92764109bbd729653b5e1ba06b8b7a0f1f60b44c07
5
5
  SHA512:
6
- metadata.gz: c1c7e788eacbf1ed02100d4fc954f3ab7781c17a4e8c46c8ca8764becea615d54c6dee3be60404901228b37397533a809873c3ae27a157ac53463ff048ce8d50
7
- data.tar.gz: fc73665d0141f943beca8dd9d43adbee088a214d1d31dad757fed1502df707f8ae599f10f4d5b3eb0b0c7e8c7db4b43b860cdd655e90d49c194320148df65439
6
+ metadata.gz: f28ca3884eef23d8db41bcef405205e5afca8369622627f415a845340579e54b17102cbdcabd3d44933ae7630a26eb3c6b3ba05c3e50da08092b84ec873a1629
7
+ data.tar.gz: 69166708a716e13658f5f25c86ccea83c201cccdeeec11296249fc318e7cabbec78816048ca266f9c890538b920d091968bff3275888a2c7bf7d9853343313f6
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- nonnative (3.6.0)
4
+ nonnative (3.9.0)
5
5
  concurrent-ruby (>= 1, < 2)
6
6
  config (>= 5, < 6)
7
7
  cucumber (>= 7, < 12)
data/README.md CHANGED
@@ -66,13 +66,16 @@ Process/server fields:
66
66
  - `wait`: small sleep (seconds) between lifecycle steps.
67
67
  - `log`: per-runner log file used by process output redirection or server implementations.
68
68
 
69
+ Process-only fields:
70
+ - `readiness`: optional HTTP startup readiness check with explicit `port` and `path`.
71
+
69
72
  Service fields:
70
73
  - `port`: client-facing service port. Services do not get TCP readiness/shutdown checks from Nonnative.
71
74
 
72
- Nonnative readiness and shutdown checks are TCP-only. Configure process/server ports that are dedicated to the test run; if another process is already listening on the same endpoint, results are undefined.
75
+ 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 an HTTP readiness check that runs after TCP readiness succeeds.
73
76
 
74
77
  > [!WARNING]
75
- > Readiness and shutdown checks only prove that a TCP port opened or closed. They do not verify HTTP status, gRPC health, schema readiness, migrations, or application-specific health.
78
+ > TCP readiness and shutdown checks only prove that a TCP port opened or closed. HTTP readiness is process-only, checks for a 2xx response, and does not verify gRPC health, schema readiness, migrations, or other application-specific health.
76
79
 
77
80
  Start and stop Nonnative around the test scope that should own the configured runners:
78
81
 
@@ -170,6 +173,7 @@ Nonnative.configure do |config|
170
173
  p.ports = [12_321]
171
174
  p.log = '12_321.log'
172
175
  p.signal = 'INT' # Possible values are described in Signal.list.keys.
176
+ p.readiness = { port: 12_321, path: '/test/readyz' }
173
177
  p.environment = { # Pass environment variables to process.
174
178
  'TEST' => 'true'
175
179
  }
@@ -205,6 +209,9 @@ processes:
205
209
  - 12321
206
210
  log: 12_321.log
207
211
  signal: INT # Possible values are described in Signal.list.keys.
212
+ readiness:
213
+ port: 12321
214
+ path: /test/readyz
208
215
  environment: # Pass environment variables to process.
209
216
  TEST: true
210
217
  -
@@ -612,7 +619,7 @@ These proxies can simulate different situations. Available proxy kinds are:
612
619
  - `fault_injection`
613
620
 
614
621
  > [!WARNING]
615
- > Unknown proxy kinds fall back to `none`. If fault injection is not taking effect, check the `kind` spelling or register the custom kind before loading the configuration.
622
+ > Unknown proxy kinds raise an error. If fault injection is not taking effect, check the `kind` spelling or register the custom kind before starting the system.
616
623
 
617
624
  Custom proxy kinds can be registered through `Nonnative.proxies`:
618
625
 
@@ -22,6 +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
26
  # end
26
27
  # end
27
28
  #
@@ -142,6 +143,7 @@ module Nonnative
142
143
  process_config.command = command(loaded_process)
143
144
  process_config.signal = loaded_process.signal
144
145
  process_config.environment = loaded_process.environment
146
+ process_config.readiness = loaded_process.readiness if loaded_process.readiness
145
147
  runner_attributes(process_config, loaded_process)
146
148
  end
147
149
  end
@@ -163,6 +165,7 @@ module Nonnative
163
165
  servers = cfg.servers || []
164
166
  servers.each do |loaded_server|
165
167
  reject_proxy(loaded_server, 'servers')
168
+ reject_readiness(loaded_server, 'servers')
166
169
 
167
170
  server do |server_config|
168
171
  server_config.klass = Object.const_get(server_class_name(loaded_server))
@@ -174,6 +177,8 @@ module Nonnative
174
177
  def add_services(cfg)
175
178
  services = cfg.services || []
176
179
  services.each do |loaded_service|
180
+ reject_readiness(loaded_service, 'services')
181
+
177
182
  service do |service_config|
178
183
  service_config.name = loaded_service.name
179
184
  service_config.host = loaded_service.host if loaded_service.host
@@ -220,6 +225,13 @@ module Nonnative
220
225
  raise ArgumentError, "Use 'services' for proxy configuration; #{kind} do not support 'proxy'"
221
226
  end
222
227
 
228
+ def reject_readiness(loaded, kind)
229
+ values = loaded.to_h
230
+ return unless values.key?(:readiness) || values.key?('readiness')
231
+
232
+ raise ArgumentError, "Use 'processes' for readiness configuration; #{kind} do not support 'readiness'"
233
+ end
234
+
223
235
  def assign_proxy(service, loaded_proxy)
224
236
  return unless loaded_proxy
225
237
 
@@ -28,6 +28,9 @@ 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, nil] optional HTTP readiness check
32
+ attr_reader :readiness
33
+
31
34
  # Creates a process configuration with bounded lifecycle defaults.
32
35
  #
33
36
  # Defaults:
@@ -39,5 +42,13 @@ module Nonnative
39
42
 
40
43
  self.timeout = DEFAULT_TIMEOUT
41
44
  end
45
+
46
+ # Sets optional HTTP readiness configuration.
47
+ #
48
+ # @param value [Hash, #to_h, nil] readiness attributes with required `port` and `path`
49
+ # @return [void]
50
+ def readiness=(value)
51
+ @readiness = value.nil? ? nil : Nonnative::ConfigurationReadiness.new(value)
52
+ end
42
53
  end
43
54
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nonnative
4
+ # HTTP readiness configuration for a managed process.
5
+ #
6
+ # Readiness is optional. When present, both `port` and `path` are required so the startup
7
+ # check has an explicit application endpoint to poll after TCP readiness succeeds.
8
+ class ConfigurationReadiness
9
+ # @return [Integer] process HTTP readiness port
10
+ attr_accessor :port
11
+
12
+ # @return [String] HTTP readiness path
13
+ attr_accessor :path
14
+
15
+ # @param value [Hash, #to_h] readiness attributes
16
+ def initialize(value)
17
+ attributes = value.respond_to?(:to_h) ? value.to_h : value
18
+ self.port = attribute(attributes, :port)
19
+ self.path = attribute(attributes, :path)
20
+
21
+ validate!
22
+ end
23
+
24
+ private
25
+
26
+ def attribute(attributes, name)
27
+ attributes[name] || attributes[name.to_s]
28
+ end
29
+
30
+ def validate!
31
+ raise ArgumentError, "Process readiness requires 'port'" if port.nil?
32
+ raise ArgumentError, "Process readiness requires 'path'" if path.nil?
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nonnative
4
+ # Probes a managed process HTTP readiness endpoint.
5
+ class HTTPProbe < Nonnative::HTTPClient
6
+ NETWORK_ERRORS = [
7
+ Errno::ECONNREFUSED,
8
+ Errno::EHOSTUNREACH,
9
+ Errno::ECONNRESET,
10
+ SocketError,
11
+ RestClient::Exceptions::Timeout,
12
+ RestClient::ServerBrokeConnection
13
+ ].freeze
14
+
15
+ # @param process [Nonnative::ConfigurationProcess] process configuration with readiness attributes
16
+ def initialize(process)
17
+ @readiness = process.readiness
18
+ @base_url = "http://#{process.host}:#{readiness.port}"
19
+ @timeout = Nonnative::Timeout.new(process.timeout)
20
+
21
+ super(base_url)
22
+ end
23
+
24
+ # Returns whether the configured HTTP endpoint returns a 2xx response before timeout.
25
+ #
26
+ # @return [Boolean]
27
+ def ready?
28
+ Nonnative.logger.info "checking if readiness '#{endpoint}' is ready"
29
+
30
+ timeout.perform do
31
+ response = get(readiness.path)
32
+ raise Nonnative::Error unless ready_response?(response)
33
+
34
+ true
35
+ rescue Nonnative::Error, *NETWORK_ERRORS
36
+ sleep_interval
37
+ retry
38
+ end
39
+ end
40
+
41
+ # Returns the HTTP readiness endpoint for lifecycle diagnostics.
42
+ #
43
+ # @return [String]
44
+ def endpoint
45
+ "#{base_url}#{path}"
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :base_url, :readiness, :timeout
51
+
52
+ def path
53
+ readiness.path.start_with?('/') ? readiness.path : "/#{readiness.path}"
54
+ end
55
+
56
+ def ready_response?(response)
57
+ response.respond_to?(:code) && response.code.to_i.between?(200, 299)
58
+ end
59
+
60
+ def sleep_interval
61
+ sleep 0.01
62
+ end
63
+ end
64
+ end
@@ -3,7 +3,7 @@
3
3
  module Nonnative
4
4
  # No-op proxy implementation.
5
5
  #
6
- # This is the default proxy when `service.proxy.kind` is `"none"` (or an unknown kind is provided).
6
+ # This is the default proxy when `service.proxy.kind` is `"none"`.
7
7
  # It does not bind/listen or alter traffic; it simply exposes the underlying runner's configured
8
8
  # `host` and primary `port`.
9
9
  #
@@ -30,6 +30,7 @@ module Nonnative
30
30
  # @yieldparam name [String, nil] runner name
31
31
  # @yieldparam values [Object] runner-specific return value from `start` (e.g. `[pid, running]` for processes)
32
32
  # @yieldparam result [Boolean] result of the port readiness check (`true` if ready in time)
33
+ # @yieldparam port [Nonnative::Ports] checked port group
33
34
  # @return [Array<String>] lifecycle and readiness-check errors collected while starting
34
35
  def start(&)
35
36
  errors = []
@@ -47,6 +48,7 @@ module Nonnative
47
48
  # @yieldparam name [String, nil] runner name
48
49
  # @yieldparam id [Object] runner-specific identifier returned by `stop` (e.g. pid or object_id)
49
50
  # @yieldparam result [Boolean] result of the port shutdown check (`true` if closed in time)
51
+ # @yieldparam port [Nonnative::Ports] checked port group
50
52
  # @return [Array<String>] lifecycle and shutdown-check errors collected while stopping
51
53
  def stop(&)
52
54
  errors = []
@@ -65,6 +67,7 @@ module Nonnative
65
67
  # @yieldparam name [String, nil] runner name
66
68
  # @yieldparam id [Object] runner-specific identifier returned by `stop`
67
69
  # @yieldparam result [Boolean] result of the port shutdown check (`true` if closed in time)
70
+ # @yieldparam port [Nonnative::Ports] checked port group
68
71
  # @return [Array<String>] lifecycle and shutdown-check errors collected while rolling back
69
72
  def rollback(&)
70
73
  errors = []
@@ -183,7 +186,7 @@ module Nonnative
183
186
 
184
187
  runners.each do |runner, port|
185
188
  values = runner.send(lifecycle_method)
186
- checks << [runner, values, Thread.new { check_port(port, port_method) }]
189
+ checks << [runner, values, port, Thread.new { check_port(port, port_method) }]
187
190
  rescue StandardError => e
188
191
  errors << lifecycle_error(action, runner, e)
189
192
  end
@@ -198,12 +201,12 @@ module Nonnative
198
201
  end
199
202
 
200
203
  def yield_results(checks, action, &)
201
- checks.each_with_object([]) do |(type, values, thread), errors|
204
+ checks.each_with_object([]) do |(type, values, port, thread), errors|
202
205
  result = thread.value
203
206
  if result[:error]
204
207
  errors << port_error(action, type, result[:error])
205
208
  elsif block_given?
206
- yield type.name, values, result[:result]
209
+ yield type.name, values, result[:result], port
207
210
  end
208
211
  end
209
212
  end
@@ -61,6 +61,13 @@ module Nonnative
61
61
  end
62
62
  end
63
63
 
64
+ # Returns a human-readable endpoint for lifecycle diagnostics.
65
+ #
66
+ # @return [String]
67
+ def endpoint
68
+ "#{process.host}:#{port}"
69
+ end
70
+
64
71
  private
65
72
 
66
73
  attr_reader :process, :port, :timeout
@@ -10,14 +10,16 @@ module Nonnative
10
10
  class Ports
11
11
  # @param runner [#host, #ports, #timeout] runner configuration providing connection details
12
12
  def initialize(runner)
13
+ @runner = runner
13
14
  @ports = runner.ports.map { |port| Nonnative::Port.new(runner, port) }
15
+ @readiness = Nonnative::HTTPProbe.new(runner) if runner.respond_to?(:readiness) && runner.readiness
14
16
  end
15
17
 
16
18
  # Returns whether all configured ports become connectable before their timeouts elapse.
17
19
  #
18
20
  # @return [Boolean]
19
21
  def open?
20
- ports.all?(&:open?)
22
+ ports.all?(&:open?) && (readiness.nil? || readiness.ready?)
21
23
  end
22
24
 
23
25
  # Returns whether all configured ports become non-connectable before their timeouts elapse.
@@ -27,8 +29,27 @@ module Nonnative
27
29
  ports.all?(&:closed?)
28
30
  end
29
31
 
32
+ # Returns the checked endpoints for lifecycle diagnostics.
33
+ #
34
+ # @return [String]
35
+ def endpoints
36
+ ports.map(&:endpoint).join(', ')
37
+ end
38
+
39
+ # Returns endpoint and log context for lifecycle errors.
40
+ #
41
+ # @return [String]
42
+ def description
43
+ details = []
44
+ details << "readiness: #{readiness.endpoint}" if readiness
45
+ log = runner.log if runner.respond_to?(:log)
46
+ details << "log: #{log}" if log
47
+
48
+ details.empty? ? endpoints : "#{endpoints} (#{details.join('; ')})"
49
+ end
50
+
30
51
  private
31
52
 
32
- attr_reader :ports
53
+ attr_reader :ports, :readiness, :runner
33
54
  end
34
55
  end
@@ -6,7 +6,9 @@ module Nonnative
6
6
  # A runtime service constructs a proxy via this factory. The proxy implementation is selected by
7
7
  # `service.proxy.kind` and resolved using {Nonnative.proxy}.
8
8
  #
9
- # If the kind is unknown (or `"none"`), {Nonnative.proxy} returns {Nonnative::NoProxy}.
9
+ # If the kind is `"none"`, {Nonnative.proxy} returns {Nonnative::NoProxy}.
10
+ # Unknown non-`"none"` kinds raise an error so proxy configuration typos do not silently disable
11
+ # fault injection.
10
12
  #
11
13
  # @see Nonnative.proxy
12
14
  # @see Nonnative.proxies
@@ -4,5 +4,5 @@ module Nonnative
4
4
  # The current gem version.
5
5
  #
6
6
  # @return [String]
7
- VERSION = '3.6.0'
7
+ VERSION = '3.9.0'
8
8
  end
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 and optional process HTTP 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,6 +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
31
  # end
31
32
  # end
32
33
  #
@@ -72,6 +73,7 @@ require 'nonnative/ports'
72
73
  require 'nonnative/configuration_file'
73
74
  require 'nonnative/configuration'
74
75
  require 'nonnative/configuration_runner'
76
+ require 'nonnative/configuration_readiness'
75
77
  require 'nonnative/configuration_process'
76
78
  require 'nonnative/configuration_server'
77
79
  require 'nonnative/configuration_service'
@@ -82,6 +84,7 @@ require 'nonnative/server'
82
84
  require 'nonnative/service'
83
85
  require 'nonnative/pool'
84
86
  require 'nonnative/http_client'
87
+ require 'nonnative/http_probe'
85
88
  require 'nonnative/http_server'
86
89
  require 'nonnative/http_proxy_server'
87
90
  require 'nonnative/grpc_server'
@@ -204,7 +207,7 @@ module Nonnative
204
207
  # @param kind [String] proxy kind name (for example `"fault_injection"`)
205
208
  # @return [Class] a subclass of {Nonnative::Proxy}
206
209
  def proxy(kind)
207
- Nonnative.proxies[kind] || Nonnative::NoProxy
210
+ kind.nil? || kind == 'none' ? NoProxy : proxies.fetch(kind) { raise ArgumentError, "Unsupported proxy kind '#{kind}'" }
208
211
  end
209
212
 
210
213
  # Starts all configured services, servers, and processes, and waits for readiness.
@@ -216,9 +219,9 @@ module Nonnative
216
219
  def start
217
220
  @pool ||= Nonnative::Pool.new(configuration)
218
221
  errors = []
219
- errors.concat(@pool.start do |name, values, result|
222
+ errors.concat(@pool.start do |name, values, result, ports|
220
223
  id, started = values
221
- errors << "Started #{name} with id #{id}, though did not respond in time" if !started || !result
224
+ errors << "Started #{name} with id #{id}, though did not respond in time for #{ports.description}" if !started || !result
222
225
  end)
223
226
  nil
224
227
  rescue StandardError => e
@@ -239,9 +242,9 @@ module Nonnative
239
242
  errors = []
240
243
  return if @pool.nil?
241
244
 
242
- errors.concat(@pool.stop do |name, values, result|
245
+ errors.concat(@pool.stop do |name, values, result, ports|
243
246
  id, stopped = Array(values).then { |v| [v.first, v.fetch(1, true)] }
244
- errors << "Stopped #{name} with id #{id}, though did not respond in time" unless result
247
+ errors << "Stopped #{name} with id #{id}, though did not respond in time for #{ports.description}" unless result
245
248
  errors << "Stopped #{name} with id #{id}, though the process did not exit in time" unless stopped
246
249
  end)
247
250
  nil
@@ -305,9 +308,9 @@ module Nonnative
305
308
  errors = []
306
309
  return errors if @pool.nil?
307
310
 
308
- errors.concat(@pool.rollback do |name, values, result|
311
+ errors.concat(@pool.rollback do |name, values, result, ports|
309
312
  id, stopped = Array(values).then { |v| [v.first, v.fetch(1, true)] }
310
- errors << "Rollback failed for #{name} with id #{id}, because it did not stop in time" unless result
313
+ errors << "Rollback failed for #{name} with id #{id}, because it did not stop in time for #{ports.description}" unless result
311
314
  errors << "Rollback failed for #{name} with id #{id}, because the process did not exit in time" unless stopped
312
315
  end)
313
316
  rescue StandardError => e
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.6.0
4
+ version: 3.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alejandro Falkowski
@@ -291,6 +291,7 @@ files:
291
291
  - lib/nonnative/configuration_file.rb
292
292
  - lib/nonnative/configuration_process.rb
293
293
  - lib/nonnative/configuration_proxy.rb
294
+ - lib/nonnative/configuration_readiness.rb
294
295
  - lib/nonnative/configuration_runner.rb
295
296
  - lib/nonnative/configuration_server.rb
296
297
  - lib/nonnative/configuration_service.rb
@@ -302,6 +303,7 @@ files:
302
303
  - lib/nonnative/grpc_server.rb
303
304
  - lib/nonnative/header.rb
304
305
  - lib/nonnative/http_client.rb
306
+ - lib/nonnative/http_probe.rb
305
307
  - lib/nonnative/http_proxy_server.rb
306
308
  - lib/nonnative/http_server.rb
307
309
  - lib/nonnative/invalid_data_socket_pair.rb