nonnative 1.107.0 → 1.108.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +8 -6
  3. data/.rubocop.yml +3 -0
  4. data/AGENTS.md +248 -0
  5. data/CHANGELOG.md +104 -0
  6. data/Gemfile.lock +53 -51
  7. data/README.md +51 -32
  8. data/lib/nonnative/close_all_socket_pair.rb +14 -0
  9. data/lib/nonnative/configuration.rb +67 -0
  10. data/lib/nonnative/configuration_process.rb +14 -0
  11. data/lib/nonnative/configuration_proxy.rb +28 -0
  12. data/lib/nonnative/configuration_runner.rb +44 -0
  13. data/lib/nonnative/configuration_server.rb +12 -0
  14. data/lib/nonnative/configuration_service.rb +9 -0
  15. data/lib/nonnative/delay_socket_pair.rb +15 -0
  16. data/lib/nonnative/error.rb +7 -0
  17. data/lib/nonnative/fault_injection_proxy.rb +63 -0
  18. data/lib/nonnative/go_command.rb +34 -0
  19. data/lib/nonnative/grpc_server.rb +30 -0
  20. data/lib/nonnative/header.rb +44 -0
  21. data/lib/nonnative/http_client.rb +45 -0
  22. data/lib/nonnative/http_proxy_server.rb +62 -1
  23. data/lib/nonnative/http_server.rb +40 -0
  24. data/lib/nonnative/invalid_data_socket_pair.rb +15 -0
  25. data/lib/nonnative/no_proxy.rb +35 -0
  26. data/lib/nonnative/not_found_error.rb +7 -0
  27. data/lib/nonnative/observability.rb +44 -0
  28. data/lib/nonnative/pool.rb +50 -0
  29. data/lib/nonnative/port.rb +26 -0
  30. data/lib/nonnative/process.rb +29 -0
  31. data/lib/nonnative/proxy.rb +24 -0
  32. data/lib/nonnative/proxy_factory.rb +16 -0
  33. data/lib/nonnative/runner.rb +30 -0
  34. data/lib/nonnative/server.rb +28 -0
  35. data/lib/nonnative/service.rb +16 -0
  36. data/lib/nonnative/socket_pair.rb +46 -0
  37. data/lib/nonnative/socket_pair_factory.rb +21 -0
  38. data/lib/nonnative/start_error.rb +5 -0
  39. data/lib/nonnative/stop_error.rb +5 -0
  40. data/lib/nonnative/timeout.rb +20 -0
  41. data/lib/nonnative/version.rb +4 -1
  42. data/lib/nonnative.rb +128 -0
  43. metadata +3 -2
@@ -1,7 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # gRPC server runner implemented using {GRPC::RpcServer}.
5
+ #
6
+ # This is a convenience server implementation for running a gRPC service in-process under
7
+ # Nonnative's server lifecycle. It binds to the configured proxy `host`/`port` and is started/stopped
8
+ # by {Nonnative::Server} via {#perform_start} / {#perform_stop}.
9
+ #
10
+ # Important note about logging: the `grpc` gem uses a global logger. This implementation sets
11
+ # `GRPC.logger` to write to the configured `service.log`, and whichever gRPC server is initialized
12
+ # first "wins" that global logger.
13
+ #
14
+ # @see Nonnative::Server
4
15
  class GRPCServer < Nonnative::Server
16
+ # Creates a gRPC server and registers the provided service handler.
17
+ #
18
+ # @param svc [Object] a gRPC service implementation (typically a `...::Service` subclass instance)
19
+ # @param service [Nonnative::ConfigurationServer] server configuration
5
20
  def initialize(svc, service)
6
21
  @server = GRPC::RpcServer.new
7
22
  server.handle(svc)
@@ -16,21 +31,36 @@ module Nonnative
16
31
 
17
32
  protected
18
33
 
34
+ # Binds the gRPC server and begins serving requests.
35
+ #
36
+ # The server binds to the proxy host/port so that enabling a proxy results in traffic and readiness
37
+ # checks consistently targeting the proxy endpoint.
38
+ #
39
+ # @return [void]
19
40
  def perform_start
20
41
  server.add_http2_port("#{proxy.host}:#{proxy.port}", :this_port_is_insecure)
21
42
  server.run
22
43
  end
23
44
 
45
+ # Stops the gRPC server.
46
+ #
47
+ # @return [void]
24
48
  def perform_stop
25
49
  server.stop
26
50
  end
27
51
 
52
+ # Waits until the gRPC server reports it is running, or the configured timeout elapses.
53
+ #
54
+ # @return [Object, false] the last evaluated expression from the timeout block, or `false` on timeout
28
55
  def wait_start
29
56
  timeout.perform do
30
57
  super until server.running?
31
58
  end
32
59
  end
33
60
 
61
+ # Waits until the gRPC server reports it has stopped, or the configured timeout elapses.
62
+ #
63
+ # @return [Object, false] the last evaluated expression from the timeout block, or `false` on timeout
34
64
  def wait_stop
35
65
  timeout.perform do
36
66
  super until server.stopped?
@@ -1,20 +1,64 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Helper utilities for building request headers for HTTP and gRPC clients.
5
+ #
6
+ # This class returns Ruby hashes suitable for passing into client libraries (for example
7
+ # RestClient for HTTP or gRPC call metadata).
8
+ #
9
+ # @example HTTP user-agent (RestClient style)
10
+ # headers = Nonnative::Header.http_user_agent('my-client/1.0')
11
+ # # => { user_agent: "my-client/1.0" }
12
+ #
13
+ # @example gRPC user-agent (metadata)
14
+ # metadata = Nonnative::Header.grpc_user_agent('my-client/1.0')
15
+ # # => { "grpc.primary_user_agent" => "my-client/1.0" }
16
+ #
17
+ # @example Basic auth header
18
+ # headers = Nonnative::Header.auth_basic('user:pass')
19
+ # # => { authorization: "Basic dXNlcjpwYXNz" }
20
+ #
21
+ # @example Bearer auth header
22
+ # headers = Nonnative::Header.auth_bearer('token')
23
+ # # => { authorization: "Bearer token" }
24
+ #
25
+ # @see https://github.com/rest-client/rest-client RestClient
26
+ # @see https://grpc.io/docs/guides/concepts/ gRPC concepts (metadata)
4
27
  class Header
5
28
  class << self
29
+ # Builds an HTTP `User-Agent` header hash.
30
+ #
31
+ # This uses the key style commonly used by RestClient (`user_agent:`).
32
+ #
33
+ # @param user_agent [String] user agent value
34
+ # @return [Hash{Symbol=>String}] header hash containing the user agent
6
35
  def http_user_agent(user_agent)
7
36
  { user_agent: }
8
37
  end
9
38
 
39
+ # Builds gRPC metadata for setting the primary user agent.
40
+ #
41
+ # @param user_agent [String] user agent value
42
+ # @return [Hash{String=>String}] gRPC metadata hash
10
43
  def grpc_user_agent(user_agent)
11
44
  { 'grpc.primary_user_agent' => user_agent }
12
45
  end
13
46
 
47
+ # Builds an HTTP Basic Authorization header.
48
+ #
49
+ # The credentials are base64-encoded using strict encoding. The `credentials` string should
50
+ # typically be in the form `"username:password"`.
51
+ #
52
+ # @param credentials [String] basic auth credentials in `"user:pass"` form
53
+ # @return [Hash{Symbol=>String}] header hash containing the Authorization header
14
54
  def auth_basic(credentials)
15
55
  { authorization: "Basic #{Base64.strict_encode64(credentials)}" }
16
56
  end
17
57
 
58
+ # Builds an HTTP Bearer Authorization header.
59
+ #
60
+ # @param token [String] bearer token
61
+ # @return [Hash{Symbol=>String}] header hash containing the Authorization header
18
62
  def auth_bearer(token)
19
63
  { authorization: "Bearer #{token}" }
20
64
  end
@@ -1,7 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Minimal RestClient-based HTTP client with consistent exception handling.
5
+ #
6
+ # This class is intended to be subclassed by higher-level clients (for example
7
+ # {Nonnative::Observability}). It provides protected helpers for common HTTP verbs and a retry
8
+ # wrapper.
9
+ #
10
+ # Error handling behavior:
11
+ # - Timeouts and broken connections (see {#initialize}) are re-raised so callers can handle them explicitly.
12
+ # - Other `RestClient::Exception` errors return the underlying `response` object.
13
+ #
14
+ # @see Nonnative::Observability
4
15
  class HTTPClient
16
+ # @param host [String] base URL used to build request URLs (e.g. `"http://127.0.0.1:8080"`)
5
17
  def initialize(host)
6
18
  @host = host
7
19
  @exceptions = [
@@ -12,34 +24,67 @@ module Nonnative
12
24
 
13
25
  protected
14
26
 
27
+ # Executes the given block with retries for the configured network exceptions.
28
+ #
29
+ # @param tries [Integer] number of attempts
30
+ # @param wait [Numeric] base interval between retries (seconds)
31
+ # @yield the work to retry
32
+ # @return [Object] the block result
15
33
  def with_retry(tries, wait, &)
16
34
  Retriable.retriable(tries: tries, base_interval: wait, on: exceptions, &)
17
35
  end
18
36
 
37
+ # Performs a GET request.
38
+ #
39
+ # @param pathname [String] path relative to `host`
40
+ # @param opts [Hash] RestClient request options (e.g. `headers`, `read_timeout`, `open_timeout`)
41
+ # @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
19
42
  def get(pathname, opts = {})
20
43
  with_exception do
21
44
  resource(pathname, opts).get
22
45
  end
23
46
  end
24
47
 
48
+ # Performs a POST request.
49
+ #
50
+ # @param pathname [String] path relative to `host`
51
+ # @param payload [Object] request payload
52
+ # @param opts [Hash] RestClient request options
53
+ # @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
25
54
  def post(pathname, payload, opts = {})
26
55
  with_exception do
27
56
  resource(pathname, opts).post(payload)
28
57
  end
29
58
  end
30
59
 
60
+ # Performs a DELETE request.
61
+ #
62
+ # @param pathname [String] path relative to `host`
63
+ # @param opts [Hash] RestClient request options
64
+ # @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
31
65
  def delete(pathname, opts = {})
32
66
  with_exception do
33
67
  resource(pathname, opts).delete
34
68
  end
35
69
  end
36
70
 
71
+ # Performs a PUT request.
72
+ #
73
+ # @param pathname [String] path relative to `host`
74
+ # @param payload [Object] request payload
75
+ # @param opts [Hash] RestClient request options
76
+ # @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
37
77
  def put(pathname, payload, opts = {})
38
78
  with_exception do
39
79
  resource(pathname, opts).put(payload)
40
80
  end
41
81
  end
42
82
 
83
+ # Creates a RestClient resource for a relative path.
84
+ #
85
+ # @param pathname [String] path relative to `host`
86
+ # @param opts [Hash] RestClient request options
87
+ # @return [RestClient::Resource]
43
88
  def resource(pathname, opts)
44
89
  RestClient::Resource.new(URI.join(host, pathname).to_s, opts)
45
90
  end
@@ -1,8 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Adapted from https://gist.github.com/RaVbaker/d9ead3c92b915f997dab25c7f0c0ab65
3
+ # Sinatra-based HTTP forward proxy server used as an in-process Nonnative server.
4
+ #
5
+ # The proxy receives inbound HTTP requests and forwards them to an upstream host over HTTPS, returning
6
+ # the upstream response status and body.
7
+ #
8
+ # This file defines two classes:
9
+ #
10
+ # - {Nonnative::HTTPProxy}: a Sinatra application that implements the proxying behavior.
11
+ # - {Nonnative::HTTPProxyServer}: a {Nonnative::HTTPServer} wrapper that runs the proxy app under Puma.
12
+ #
13
+ # Notes:
14
+ # - This code is adapted from https://gist.github.com/RaVbaker/d9ead3c92b915f997dab25c7f0c0ab65
15
+ # - Only a subset of request headers are forwarded; certain headers are removed to avoid conflicts.
16
+ #
17
+ # @see Nonnative::HTTPServer
18
+ # @see Nonnative::Server
4
19
  module Nonnative
20
+ # Sinatra application implementing a simple forward proxy.
21
+ #
22
+ # The upstream host is configured via Sinatra settings (see {Nonnative::HTTPProxyServer}).
23
+ #
24
+ # Supported HTTP verbs: GET, POST, PUT, PATCH, DELETE.
5
25
  class HTTPProxy < Sinatra::Application
26
+ # Extracts request headers from the Rack environment and normalizes them to standard HTTP names.
27
+ #
28
+ # Certain hop-by-hop or proxy-specific headers are removed.
29
+ #
30
+ # @param request [Sinatra::Request] the incoming request
31
+ # @return [Hash{String=>String}] headers to forward to the upstream
6
32
  def retrieve_headers(request)
7
33
  headers = request.env.map do |header, value|
8
34
  [header[5..].split('_').map(&:capitalize).join('-'), value] if header.start_with?('HTTP_')
@@ -12,10 +38,21 @@ module Nonnative
12
38
  headers.except('Host', 'Accept-Encoding', 'Version')
13
39
  end
14
40
 
41
+ # Builds the upstream URL for the given request.
42
+ #
43
+ # @param request [Sinatra::Request] the incoming request
44
+ # @param settings [Sinatra::Base] Sinatra settings, expected to include `host`
45
+ # @return [String] HTTPS URL for the upstream request
15
46
  def build_url(request, settings)
16
47
  URI::HTTPS.build(host: settings.host, path: request.path_info, query: request.query_string).to_s
17
48
  end
18
49
 
50
+ # Executes the upstream request and returns the response.
51
+ #
52
+ # @param verb [String] HTTP verb name (e.g. `"get"`)
53
+ # @param uri [String] upstream URI
54
+ # @param opts [Hash] RestClient options (e.g. headers)
55
+ # @return [RestClient::Response] response for error statuses, otherwise RestClient return value
19
56
  def api_response(verb, uri, opts)
20
57
  client = RestClient::Resource.new(uri, opts)
21
58
 
@@ -36,7 +73,31 @@ module Nonnative
36
73
  end
37
74
  end
38
75
 
76
+ # Runs {Nonnative::HTTPProxy} as a Puma-based in-process server under Nonnative.
77
+ #
78
+ # @example
79
+ # Nonnative.configure do |config|
80
+ # config.server do |s|
81
+ # s.name = 'github-proxy'
82
+ # s.klass = Nonnative::Features::HTTPProxyServer
83
+ # s.timeout = 2
84
+ # s.host = '127.0.0.1'
85
+ # s.port = 4567
86
+ # s.log = 'proxy.log'
87
+ # end
88
+ # end
89
+ #
90
+ # # In your server subclass:
91
+ # # class HTTPProxyServer < Nonnative::HTTPProxyServer
92
+ # # def initialize(service)
93
+ # # super('api.github.com', service)
94
+ # # end
95
+ # # end
96
+ #
97
+ # @see Nonnative::HTTPServer
39
98
  class HTTPProxyServer < Nonnative::HTTPServer
99
+ # @param host [String] upstream host to proxy to (HTTPS)
100
+ # @param service [Nonnative::ConfigurationServer] server configuration
40
101
  def initialize(host, service)
41
102
  app = Sinatra.new(Nonnative::HTTPProxy) do
42
103
  configure do
@@ -1,7 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Puma-based HTTP server runner.
5
+ #
6
+ # This is a convenience server implementation for running a Rack/Sinatra application in-process
7
+ # under Nonnative's server lifecycle. It binds to the configured proxy `host`/`port` (so it works
8
+ # consistently with proxy configuration) and uses Puma for HTTP serving.
9
+ #
10
+ # The server is started and stopped by {Nonnative::Server} via {#perform_start} / {#perform_stop}.
11
+ #
12
+ # @example Running a Sinatra app
13
+ # app = Sinatra.new do
14
+ # get('/hello') { 'Hello World!' }
15
+ # end
16
+ #
17
+ # Nonnative.configure do |config|
18
+ # config.server do |s|
19
+ # s.name = 'http'
20
+ # s.klass = ->(service) { Nonnative::HTTPServer.new(app, service) }
21
+ # s.timeout = 2
22
+ # s.host = '127.0.0.1'
23
+ # s.port = 4567
24
+ # s.log = 'http.log'
25
+ # end
26
+ # end
27
+ #
28
+ # Note: In YAML configuration you typically set `class` to a concrete subclass that calls `super(app, service)`.
29
+ #
30
+ # @see Nonnative::Server
4
31
  class HTTPServer < Nonnative::Server
32
+ # Creates a Puma server for the given Rack app and runner configuration.
33
+ #
34
+ # @param app [#call] a Rack-compatible application (e.g. Sinatra/Rack app)
35
+ # @param service [Nonnative::ConfigurationServer] server configuration
5
36
  def initialize(app, service)
6
37
  log = File.open(service.log, 'a')
7
38
  options = {
@@ -15,11 +46,20 @@ module Nonnative
15
46
 
16
47
  protected
17
48
 
49
+ # Binds the Puma server and begins serving.
50
+ #
51
+ # The listener binds to the proxy host/port so that enabling a proxy results in traffic and
52
+ # readiness checks consistently targeting the proxy endpoint.
53
+ #
54
+ # @return [void]
18
55
  def perform_start
19
56
  server.add_tcp_listener proxy.host, proxy.port
20
57
  server.run false
21
58
  end
22
59
 
60
+ # Gracefully shuts down the Puma server.
61
+ #
62
+ # @return [void]
23
63
  def perform_stop
24
64
  server.graceful_shutdown
25
65
  end
@@ -1,7 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Socket-pair variant used by the fault-injection proxy to simulate corrupted/incoherent traffic.
5
+ #
6
+ # When active, data written to the upstream socket is corrupted by shuffling the characters in the
7
+ # payload before forwarding.
8
+ #
9
+ # This behavior is enabled by calling {Nonnative::FaultInjectionProxy#invalid_data}.
10
+ #
11
+ # @see Nonnative::FaultInjectionProxy
12
+ # @see Nonnative::SocketPairFactory
13
+ # @see Nonnative::SocketPair
4
14
  class InvalidDataSocketPair < SocketPair
15
+ # Writes corrupted data to the socket by shuffling characters.
16
+ #
17
+ # @param socket [IO] the socket to write to
18
+ # @param data [String] the original payload
19
+ # @return [Integer] number of bytes written
5
20
  def write(socket, data)
6
21
  Nonnative.logger.info "shuffling socket data '#{socket.inspect}' for 'invalid_data' pair"
7
22
 
@@ -1,23 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # No-op proxy implementation.
5
+ #
6
+ # This is the default proxy when `service.proxy.kind` is `"none"` (or an unknown kind is provided).
7
+ # It does not bind/listen or alter traffic; it simply exposes the underlying runner's configured
8
+ # `host` and `port`.
9
+ #
10
+ # Runners can always call `start`, `stop`, and `reset` safely on this proxy.
11
+ #
12
+ # @see Nonnative.proxy
13
+ # @see Nonnative::Proxy
4
14
  class NoProxy < Proxy
15
+ # Starts the proxy.
16
+ #
17
+ # This implementation does nothing.
18
+ #
19
+ # @return [void]
5
20
  def start
6
21
  # Do nothing.
7
22
  end
8
23
 
24
+ # Stops the proxy.
25
+ #
26
+ # This implementation does nothing.
27
+ #
28
+ # @return [void]
9
29
  def stop
10
30
  # Do nothing.
11
31
  end
12
32
 
33
+ # Resets the proxy state.
34
+ #
35
+ # This implementation does nothing.
36
+ #
37
+ # @return [void]
13
38
  def reset
14
39
  # Do nothing.
15
40
  end
16
41
 
42
+ # Returns the host clients should connect to.
43
+ #
44
+ # For {NoProxy}, this is the underlying runner configuration host.
45
+ #
46
+ # @return [String]
17
47
  def host
18
48
  service.host
19
49
  end
20
50
 
51
+ # Returns the port clients should connect to.
52
+ #
53
+ # For {NoProxy}, this is the underlying runner configuration port.
54
+ #
55
+ # @return [Integer]
21
56
  def port
22
57
  service.port
23
58
  end
@@ -1,6 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Raised when a configured runner cannot be found by name.
5
+ #
6
+ # This is typically raised by lookup helpers such as:
7
+ # - {Nonnative::Configuration#process_by_name}
8
+ # - {Nonnative::Pool#process_by_name}
9
+ # - {Nonnative::Pool#server_by_name}
10
+ # - {Nonnative::Pool#service_by_name}
4
11
  class NotFoundError < Nonnative::Error
5
12
  end
6
13
  end
@@ -1,25 +1,69 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # HTTP client for common observability endpoints exposed by the system under test.
5
+ #
6
+ # This client is returned by {Nonnative.observability} and builds endpoint paths from
7
+ # {Nonnative::Configuration#name}.
8
+ #
9
+ # Endpoints:
10
+ # - `/<name>/healthz`
11
+ # - `/<name>/livez`
12
+ # - `/<name>/readyz`
13
+ # - `/<name>/metrics`
14
+ #
15
+ # Requests are performed using {Nonnative::HTTPClient}, so callers may pass RestClient options
16
+ # such as `headers`, `open_timeout`, and `read_timeout`.
17
+ #
18
+ # @example
19
+ # Nonnative.configure do |config|
20
+ # config.name = 'my-service'
21
+ # config.url = 'http://127.0.0.1:8080'
22
+ # end
23
+ #
24
+ # response = Nonnative.observability.health(read_timeout: 2, open_timeout: 2)
25
+ # response.code # => 200
26
+ #
27
+ # @see Nonnative.observability
28
+ # @see Nonnative::HTTPClient
4
29
  class Observability < Nonnative::HTTPClient
30
+ # Calls `/<name>/healthz`.
31
+ #
32
+ # @param opts [Hash] RestClient options (e.g. `headers`, `read_timeout`, `open_timeout`)
33
+ # @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
5
34
  def health(opts = {})
6
35
  get("#{name}/healthz", opts)
7
36
  end
8
37
 
38
+ # Calls `/<name>/livez`.
39
+ #
40
+ # @param opts [Hash] RestClient options (e.g. `headers`, `read_timeout`, `open_timeout`)
41
+ # @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
9
42
  def liveness(opts = {})
10
43
  get("#{name}/livez", opts)
11
44
  end
12
45
 
46
+ # Calls `/<name>/readyz`.
47
+ #
48
+ # @param opts [Hash] RestClient options (e.g. `headers`, `read_timeout`, `open_timeout`)
49
+ # @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
13
50
  def readiness(opts = {})
14
51
  get("#{name}/readyz", opts)
15
52
  end
16
53
 
54
+ # Calls `/<name>/metrics`.
55
+ #
56
+ # @param opts [Hash] RestClient options (e.g. `headers`, `read_timeout`, `open_timeout`)
57
+ # @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
17
58
  def metrics(opts = {})
18
59
  get("#{name}/metrics", opts)
19
60
  end
20
61
 
21
62
  protected
22
63
 
64
+ # Returns the configured system name used as the endpoint prefix.
65
+ #
66
+ # @return [String, nil]
23
67
  def name
24
68
  Nonnative.configuration.name
25
69
  end
@@ -1,33 +1,83 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Orchestrates lifecycle for configured processes, servers and services.
5
+ #
6
+ # A pool is created when {Nonnative.start} is called and is accessible via {Nonnative.pool}.
7
+ #
8
+ # Lifecycle order is important:
9
+ # - On start: services first, then servers/processes (in parallel port-check threads)
10
+ # - On stop: processes/servers first, then services
11
+ #
12
+ # Readiness and shutdown are determined via TCP port checks ({Nonnative::Port#open?} / {Nonnative::Port#closed?}).
13
+ #
14
+ # @see Nonnative.start
15
+ # @see Nonnative.stop
16
+ # @see Nonnative::Port
4
17
  class Pool
18
+ # @param configuration [Nonnative::Configuration] the configuration to run
5
19
  def initialize(configuration)
6
20
  @configuration = configuration
7
21
  end
8
22
 
23
+ # Starts all configured runners and yields results for each process/server.
24
+ #
25
+ # Services are started first (proxy-only), then servers and processes are started and checked for readiness.
26
+ #
27
+ # @yieldparam name [String, nil] runner name
28
+ # @yieldparam values [Object] runner-specific return value from `start` (e.g. `[pid, running]` for processes)
29
+ # @yieldparam result [Boolean] result of the port readiness check (`true` if ready in time)
30
+ # @return [void]
9
31
  def start(&)
10
32
  services.each(&:start)
11
33
  [servers, processes].each { |t| process(t, :start, :open?, &) }
12
34
  end
13
35
 
36
+ # Stops all configured runners and yields results for each process/server.
37
+ #
38
+ # Processes and servers are stopped first and checked for shutdown, then services are stopped (proxy-only).
39
+ #
40
+ # @yieldparam name [String, nil] runner name
41
+ # @yieldparam id [Object] runner-specific identifier returned by `stop` (e.g. pid or object_id)
42
+ # @yieldparam result [Boolean] result of the port shutdown check (`true` if closed in time)
43
+ # @return [void]
14
44
  def stop(&)
15
45
  [processes, servers].each { |t| process(t, :stop, :closed?, &) }
16
46
  services.each(&:stop)
17
47
  end
18
48
 
49
+ # Finds a running process runner by configured name.
50
+ #
51
+ # @param name [String]
52
+ # @return [Nonnative::Process]
53
+ # @raise [Nonnative::NotFoundError] if no configured process matches the given name
19
54
  def process_by_name(name)
20
55
  processes[runner_index(configuration.processes, name)].first
21
56
  end
22
57
 
58
+ # Finds a running server runner by configured name.
59
+ #
60
+ # @param name [String]
61
+ # @return [Nonnative::Server]
62
+ # @raise [Nonnative::NotFoundError] if no configured server matches the given name
23
63
  def server_by_name(name)
24
64
  servers[runner_index(configuration.servers, name)].first
25
65
  end
26
66
 
67
+ # Finds a running service runner by configured name.
68
+ #
69
+ # @param name [String]
70
+ # @return [Nonnative::Service]
71
+ # @raise [Nonnative::NotFoundError] if no configured service matches the given name
27
72
  def service_by_name(name)
28
73
  services[runner_index(configuration.services, name)]
29
74
  end
30
75
 
76
+ # Resets proxies for all runners in this pool.
77
+ #
78
+ # This is used by the Cucumber `@reset` hook and is safe to call any time after the pool is created.
79
+ #
80
+ # @return [void]
31
81
  def reset
32
82
  services.each { |s| s.proxy.reset }
33
83
  servers.each { |s| s.first.proxy.reset }
@@ -1,12 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Performs TCP port readiness/shutdown checks for a configured runner.
5
+ #
6
+ # Nonnative uses this to decide whether a process/server is ready after start, and whether it has
7
+ # shut down after stop. The checks repeatedly attempt to open a TCP connection to `process.host`
8
+ # and `process.port` until either:
9
+ #
10
+ # - the expected condition is met, or
11
+ # - the configured timeout elapses (in which case the method returns `false`)
12
+ #
13
+ # The `process` argument is a runner configuration object (e.g. {Nonnative::ConfigurationProcess}
14
+ # or {Nonnative::ConfigurationServer}) that responds to `host`, `port`, and `timeout`.
15
+ #
16
+ # @see Nonnative::Pool for how these checks are orchestrated during start/stop
4
17
  class Port
18
+ # @param process [#host, #port, #timeout] runner configuration providing connection details
5
19
  def initialize(process)
6
20
  @process = process
7
21
  @timeout = Nonnative::Timeout.new(process.timeout)
8
22
  end
9
23
 
24
+ # Returns whether the configured host/port becomes connectable before the timeout elapses.
25
+ #
26
+ # This method retries on common connection errors until either a connection succeeds
27
+ # (returns `true`) or the timeout elapses (returns `false`).
28
+ #
29
+ # @return [Boolean] `true` if the port opened in time; otherwise `false`
10
30
  def open?
11
31
  Nonnative.logger.info "checking if port '#{process.port}' is open on host '#{process.host}'"
12
32
 
@@ -19,6 +39,12 @@ module Nonnative
19
39
  end
20
40
  end
21
41
 
42
+ # Returns whether the configured host/port becomes non-connectable before the timeout elapses.
43
+ #
44
+ # This method treats a successful connection as “not closed yet” and keeps retrying until it
45
+ # observes connection failure (returns `true`) or the timeout elapses (returns `false`).
46
+ #
47
+ # @return [Boolean] `true` if the port closed in time; otherwise `false`
22
48
  def closed?
23
49
  Nonnative.logger.info "checking if port '#{process.port}' is closed on host '#{process.host}'"
24
50