nonnative 3.29.0 → 3.30.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: d81874eb81ecd99758c560530adaf6dd701ac6136258ec36b04a074312e45e65
4
- data.tar.gz: 05e90f242c9cd70a7c544705d2db49c593e7615ff7edfc28e6864154036e758b
3
+ metadata.gz: 954eb6b4c3c122729406d275f3befa59e19b8db405e14cb2ee8fd759354f97ba
4
+ data.tar.gz: 39291c978f15b62b187aeb51b5d51f8b0479d314261f1eaab85e6d9f441b3a54
5
5
  SHA512:
6
- metadata.gz: 59abcf2ef7f4c1cf0785ab43dbca7af059441f8a65379601adc1204c8860955b462fc7fa8dac8b1bbb93cd7a449296cac36ea18bee692b67baad567b83cea3ad
7
- data.tar.gz: ac4407b3205ca54a8e2496e9e2ce2594ee8bf8d7fff8f9c0ae5bce929fdf4b707732c54c8b45ccb038e79bb34601cf52823a7ee99ae6c7ae05a8bbdfb60cc05a
6
+ metadata.gz: f2a6efd54e9af260f1e0261347e0f2d93825392833bbbc8818e073dfd13359eb3739afa8bd535fea01e502fbe3d0664abbae49b41eb17fed1531c1a19019e0f1
7
+ data.tar.gz: 3b6f5e46899a77d275500d7d5edca75a46f739db103d7b006ca8cbe8699a24edc362679226ba7e67221dd2fb1cc200e09c4c366b9b3faa8a42fa17be61b9b246
data/README.md CHANGED
@@ -57,7 +57,10 @@ YAML configuration is loaded as data only: ERB is not evaluated and arbitrary Ru
57
57
  deserialized.
58
58
 
59
59
  > [!CAUTION]
60
- > Treat YAML configuration as plain data. ERB is not evaluated and arbitrary Ruby object tags are rejected.
60
+ > Treat YAML configuration as plain data. ERB is not evaluated, `${VAR}` values are not expanded,
61
+ > and arbitrary Ruby object tags are rejected. Unknown structural keys may be ignored, although YAML
62
+ > syntax, object safety, and supported value shapes are still validated. Keep values that vary by
63
+ > environment in programmatic Ruby configuration, where Ruby's `ENV` is available.
61
64
 
62
65
  High-level configuration fields:
63
66
  - `version`: configuration version (example: `"1.0"`).
@@ -74,7 +77,8 @@ Common runner fields:
74
77
 
75
78
  Process/server fields:
76
79
  - `ports`: client-facing ports. These are also used for readiness/shutdown port checks.
77
- - `timeout`: max time (seconds) for readiness/shutdown port checks. Defaults to `1.0`.
80
+ - `timeout`: max time (seconds) for each readiness/shutdown check. For processes, the same value also
81
+ bounds optional HTTP/gRPC probes and graceful child exit after the stop signal. Defaults to `1.0`.
78
82
  - `wait`: small sleep (seconds) between lifecycle steps.
79
83
  - `log`: per-runner log file used by process output redirection or server implementations.
80
84
 
@@ -89,7 +93,7 @@ Service fields:
89
93
  - `readiness`: optional list of startup readiness checks. Supported kind is `tcp`, which requires
90
94
  explicit `host` and `port`.
91
95
 
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.
96
+ 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. HTTP readiness sends a plain HTTP `GET` without configurable request headers and is ready only when the final response is 2xx. gRPC readiness uses the standard health `Check` over an insecure channel and is ready only for `SERVING`. Non-ready responses are retried until the process timeout elapses. 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.
93
97
 
94
98
  > [!WARNING]
95
99
  > 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.
@@ -108,12 +112,19 @@ Nonnative.start
108
112
  Nonnative.stop
109
113
  ```
110
114
 
111
- `Nonnative.start` starts services first, then servers and processes. `Nonnative.stop` stops processes and servers first, then services. If startup fails, Nonnative rolls back runners that already started and raises `Nonnative::StartError`; shutdown failures raise `Nonnative::StopError`.
115
+ `Nonnative.start` runs ordered tiers: service lifecycle calls and optional readiness checks complete,
116
+ then server lifecycle and readiness checks complete, then process lifecycle and readiness checks run.
117
+ A failed service readiness check prevents later tiers; other collected startup errors trigger rollback
118
+ after the attempted tiers finish. `Nonnative.stop` reverses the tiers: processes, servers, then
119
+ services. Model dependencies in that direction; a managed server can satisfy a process dependency,
120
+ but a server cannot wait on a managed process. Startup failures raise `Nonnative::StartError`, and
121
+ shutdown failures raise `Nonnative::StopError`.
112
122
 
113
- > [!NOTE]
114
- > `Nonnative.start` / `Nonnative.stop` manage one lifecycle for the current pool.
115
- > Call `Nonnative.clear` before reconfiguring Nonnative or starting a new lifecycle in the same Ruby process.
116
- > `Nonnative.clear` clears memoized configuration, logger, observability client, and pool.
123
+ > [!WARNING]
124
+ > `Nonnative.clear` forgets the current pool; it does not stop live processes, server threads, or
125
+ > proxies. To reuse the same Ruby process, call `Nonnative.stop`, then `Nonnative.clear`, configure
126
+ > the next system, and call `Nonnative.start`. `clear` also clears the memoized configuration,
127
+ > logger, and observability client.
117
128
 
118
129
  ### 🧩 Test framework setup
119
130
 
@@ -167,6 +178,10 @@ response = Nonnative.observability.health(
167
178
  expect(response.code).to eq(200)
168
179
  ```
169
180
 
181
+ HTTP error statuses are returned as response objects so callers can inspect `.code` and `.body`.
182
+ Request timeouts and broken connections raise their RestClient exceptions; observability requests are
183
+ not retried automatically.
184
+
170
185
  `Nonnative.grpc_health` is a helper for the standard gRPC health checking protocol:
171
186
 
172
187
  ```ruby
@@ -180,6 +195,10 @@ health = Nonnative.grpc_health(
180
195
  expect(health.serving?).to eq(true)
181
196
  ```
182
197
 
198
+ The helper always uses an insecure plaintext gRPC channel. Pass `service: ''` or `nil` to check the
199
+ overall server. `check` returns the full `HealthCheckResponse` and propagates gRPC failures, while
200
+ `serving?` returns `false` for any non-`SERVING` status or gRPC failure.
201
+
183
202
  ### 🔑 Tokens
184
203
 
185
204
  `Nonnative.token` builds a signer for authenticating requests against a service under test. You pass the signing parameters directly (parsed from your own configuration); it is not coupled to any service's config format. The generated string is ready for `Nonnative::Header.auth_bearer`.
@@ -241,6 +260,32 @@ The repo’s own Cucumber suite also uses taxonomy tags to classify coverage:
241
260
 
242
261
  Requiring `nonnative` is enough; the Cucumber hooks and step definitions are installed lazily once Cucumber’s Ruby DSL is ready.
243
262
 
263
+ #### 🥒 Public Cucumber steps
264
+
265
+ The shipped steps are compatibility surface for downstream suites:
266
+
267
+ - `When I start the system` starts immediately. For expected failures, pair
268
+ `When I attempt to start the system` with `Then starting the system should raise an error`, or use
269
+ the equivalent stop steps.
270
+ - `Then I should see {string} as healthy` expects the configured health endpoint to return `200` and
271
+ a body containing neither the supplied service name nor `service unavailable`; `unhealthy` expects
272
+ `503` and a body identifying the service or `service unavailable`.
273
+ - `Then the process {string} should consume less than {string} of memory` accepts values such as
274
+ `25mb` for a started process.
275
+ - The two log steps search either a configured process log or an explicit file path for the requested
276
+ text: `Then I should see a log entry of {string} for process {string}` and
277
+ `Then I should see a log entry of {string} in the file {string}`.
278
+ - `Given I set the proxy for service {string} to {string}` accepts `close_all`, `reset_peer`, `delay`,
279
+ `timeout`, `invalid_data`, `bandwidth`, `limit_data`, or `reset`; the reset action step is
280
+ `Then I should reset the proxy for service {string}`.
281
+
282
+ ```cucumber
283
+ @manual
284
+ Scenario: startup is expected to fail
285
+ When I attempt to start the system
286
+ Then starting the system should raise an error
287
+ ```
288
+
244
289
  ### ⚙️ Processes
245
290
 
246
291
  A process is some sort of command that you would run locally.
@@ -249,6 +294,12 @@ Programmatic `p.command` values must be callables that return a shell string or
249
294
  > [!TIP]
250
295
  > Prefer argv arrays for new process commands. Use shell strings only when you intentionally need shell parsing, expansion, or redirection.
251
296
 
297
+ Managed processes inherit the Ruby parent's working directory and environment; loading YAML from a
298
+ different directory does not change the child working directory. Relative command, config, log, and
299
+ generated-output paths resolve from that inherited directory. Configured `environment` values are
300
+ stringified and override variables with the same names while preserving the rest of the parent
301
+ environment.
302
+
252
303
  Set it up programmatically:
253
304
 
254
305
  ```ruby
@@ -338,6 +389,10 @@ Nonnative.configure do |config|
338
389
  end
339
390
  ```
340
391
 
392
+ On stop, Nonnative sends the configured signal (`INT` by default) and waits up to `timeout` for the
393
+ child to exit. If it remains alive, Nonnative sends `KILL` and reports the stop as unsuccessful, so
394
+ `Nonnative.stop` raises `Nonnative::StopError` even if the configured shutdown ports close.
395
+
341
396
  With cucumber you can also verify how much memory is used by the process:
342
397
 
343
398
  ```cucumber
@@ -465,8 +520,9 @@ module Nonnative
465
520
  end
466
521
  ```
467
522
 
468
- To run multiple Rack services on one managed port, pass a non-empty mount map. Each key must start
469
- with `/`, and the mounted application receives the remaining `PATH_INFO`:
523
+ To run multiple Rack services on one managed port, pass a non-empty `Rack::URLMap` mount map. Keys
524
+ can be path prefixes beginning with `/` or host-qualified URLs; the mounted application receives the
525
+ remaining `PATH_INFO`:
470
526
 
471
527
  ```ruby
472
528
  module Nonnative
@@ -541,6 +597,10 @@ end
541
597
 
542
598
  The system allows you to define an in-process HTTP forward proxy server for external systems, e.g. `api.github.com`. This is a server implementation, not a fault-injection service proxy.
543
599
 
600
+ The upstream scheme is always HTTPS. The proxy forwards the request path and query for `GET`, `HEAD`,
601
+ `POST`, `PUT`, `PATCH`, `DELETE`, and `OPTIONS`, while removing proxy credentials plus `Host` and
602
+ `Accept-Encoding` before the upstream request.
603
+
544
604
  The proxy preserves the upstream status, body, and safe end-to-end response headers such as `Content-Type`, `ETag`, and application-specific metadata. It removes hop-by-hop, connection-nominated, proxy-authentication, and framing headers; `Set-Cookie`, `Location`, and `Content-Encoding` are not forwarded.
545
605
 
546
606
  Define your server:
@@ -783,9 +843,28 @@ These proxies can simulate different situations. Available proxy kinds are:
783
843
  Custom proxy kinds can be registered through `Nonnative.proxies`:
784
844
 
785
845
  ```ruby
846
+ class CustomProxy < Nonnative::Proxy
847
+ # Inherit #initialize(service), or call super from a custom initializer.
848
+ def start; end
849
+ def stop; end
850
+ def reset; end
851
+ end
852
+
786
853
  Nonnative.proxies['custom'] = CustomProxy
854
+
855
+ Nonnative.configure do |config|
856
+ config.service do |s|
857
+ s.name = 'dependency'
858
+ s.host = '127.0.0.1'
859
+ s.port = 12_345
860
+ s.proxy.kind = 'custom'
861
+ end
862
+ end
787
863
  ```
788
864
 
865
+ Custom proxies must accept the service configuration and implement `start`, `stop`, and `reset`;
866
+ Nonnative invokes those methods during service lifecycle and pool reset.
867
+
789
868
  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.
790
869
  When service readiness is configured for a proxied dependency, set the readiness `host`/`port` to the upstream dependency, not the client-facing proxy listener.
791
870
 
@@ -856,6 +935,10 @@ Clients connect to the service `host`/`port`, while the proxy forwards traffic t
856
935
 
857
936
  ###### 🧩 Fault Injection Services
858
937
 
938
+ > [!WARNING]
939
+ > Every proxy state change closes active client connections so that new connections observe the new
940
+ > state. Apply the state before connecting, and reconnect after changing or resetting it.
941
+
859
942
  Set the proxy state programmatically:
860
943
 
861
944
  ```ruby
@@ -902,6 +985,11 @@ YAML `go:` configuration is for Go test binaries compiled with `go test -c`. It
902
985
  > [!IMPORTANT]
903
986
  > If `tools` is omitted or empty, Nonnative enables all Go tools: `prof`, `trace`, and `cover`. Provide a subset, such as `tools: [cover]`, to limit the generated `-test.*` flags.
904
987
 
988
+ Create the configured `output` directory before starting Nonnative; the helper does not create it and
989
+ the Go test binary must be able to write there. Artifact names include the executable basename
990
+ without its extension, the command, and a random four-character suffix, for example
991
+ `reports/your_binary-sub_command-Ab12-cpu.prof`.
992
+
905
993
  To get this to work you will need to create a `main_test.go` file with these contents:
906
994
 
907
995
  ```go
@@ -60,14 +60,15 @@ module Nonnative
60
60
  # @return [Array<Nonnative::ConfigurationServer>] configured in-process servers
61
61
  attr_accessor :servers
62
62
 
63
- # @return [Array<Nonnative::ConfigurationService>] configured services (proxy-only)
63
+ # @return [Array<Nonnative::ConfigurationService>] configured external dependencies
64
64
  attr_accessor :services
65
65
 
66
66
  # Loads a configuration file and appends its runners to this instance.
67
67
  #
68
- # The file is loaded using safe YAML parsing. ERB is not evaluated,
69
- # arbitrary object deserialization is not allowed, top-level attributes are copied onto this object,
70
- # and runner sections are transformed into configuration runner objects.
68
+ # The file is loaded using safe YAML parsing. ERB and environment placeholders are not evaluated,
69
+ # arbitrary object deserialization is not allowed, and unknown structural keys may be ignored.
70
+ # Top-level supported attributes are copied onto this object, and runner sections are transformed
71
+ # into configuration runner objects.
71
72
  #
72
73
  # @param path [String] path to a configuration file (typically YAML)
73
74
  # @return [void]
@@ -108,8 +109,8 @@ module Nonnative
108
109
 
109
110
  # Adds a service configuration entry.
110
111
  #
111
- # A "service" does not manage a Ruby thread or OS process; it exists so that a proxy can be started
112
- # and controlled for an external dependency.
112
+ # A "service" represents an externally managed dependency. It does not manage a Ruby thread or OS
113
+ # process, but it can wait for TCP readiness and optionally control a proxy for the dependency.
113
114
  #
114
115
  # @yieldparam service [Nonnative::ConfigurationService]
115
116
  # @return [void]
@@ -11,6 +11,8 @@ module Nonnative
11
11
  # @see Nonnative::Configuration
12
12
  # @see Nonnative::Process
13
13
  class ConfigurationProcess < ConfigurationRunner
14
+ # The child inherits the parent Ruby process's working directory.
15
+ #
14
16
  # @return [Proc] a callable that returns the command to execute
15
17
  # as a shell string or argv array
16
18
  # (e.g. `-> { "./bin/api" }` or `-> { ["./bin/api", "--port", "8080"] }`)
@@ -19,12 +21,15 @@ module Nonnative
19
21
  # @return [String, nil] signal name to use for stopping (defaults to `"INT"` when not set)
20
22
  attr_accessor :signal
21
23
 
22
- # @return [Numeric] readiness timeout (seconds) used when waiting for ports to open/close (defaults to `1.0`)
24
+ # @return [Numeric] timeout (seconds) independently applied to child exit, port checks, and each
25
+ # optional HTTP/gRPC readiness probe (defaults to `1.0`)
23
26
  attr_accessor :timeout
24
27
 
25
28
  # @return [String] log file path to append process stdout/stderr to
26
29
  attr_accessor :log
27
30
 
31
+ # Values are stringified and override matching variables in the inherited parent environment.
32
+ #
28
33
  # @return [Hash, nil] environment variables to pass to the spawned process
29
34
  attr_accessor :environment
30
35
 
@@ -4,7 +4,9 @@ module Nonnative
4
4
  # Readiness check configuration for a managed process.
5
5
  #
6
6
  # Readiness is optional. When present, each check declares a `kind` and explicit endpoint details
7
- # to poll after TCP readiness succeeds.
7
+ # to poll after TCP readiness succeeds. HTTP checks issue a plain unauthenticated GET and accept a
8
+ # final 2xx response. gRPC checks use the standard health Check over an insecure channel and accept
9
+ # only SERVING. Non-ready results are retried until the process timeout elapses.
8
10
  class ConfigurationReadiness
9
11
  KINDS = %w[http grpc].freeze
10
12
 
@@ -12,7 +12,7 @@ module Nonnative
12
12
  # @see Nonnative::ConfigurationServer
13
13
  # @see Nonnative::ConfigurationService
14
14
  class ConfigurationRunner
15
- # Default bounded readiness/shutdown timeout for process and server runners.
15
+ # Default bounded lifecycle/readiness timeout for configured runners.
16
16
  DEFAULT_TIMEOUT = 1.0
17
17
 
18
18
  # @return [String, nil] runner name used for lookup (for example via `pool.process_by_name`)
@@ -3,15 +3,15 @@
3
3
  module Nonnative
4
4
  # Service-specific configuration.
5
5
  #
6
- # A "service" is proxy-only: it does not start a Ruby thread or OS process. It exists so Nonnative can
7
- # start and control a proxy in front of an external dependency.
6
+ # A "service" represents an externally managed dependency. It does not start a Ruby thread or OS
7
+ # process. It can wait for TCP readiness and can optionally run a proxy in front of the dependency.
8
8
  #
9
9
  # Instances are usually created through {Nonnative::Configuration#service}.
10
10
  #
11
11
  # @see Nonnative::Configuration
12
12
  # @see Nonnative::Service
13
13
  class ConfigurationService < ConfigurationRunner
14
- # @return [Integer] client-facing port used by the service proxy
14
+ # @return [Integer] client-facing dependency port, used as the listener when a proxy is enabled
15
15
  attr_accessor :port
16
16
 
17
17
  # @return [Numeric] readiness timeout (seconds) used when waiting for service readiness
@@ -3,7 +3,8 @@
3
3
  module Nonnative
4
4
  # Parent error for all Nonnative errors.
5
5
  #
6
- # Catch this error type if you want to handle any exception raised by this gem.
6
+ # Rescue this type to handle Nonnative-specific errors. Public APIs may also raise standard Ruby,
7
+ # dependency, filesystem, or transport exceptions that are not subclasses of this class.
7
8
  #
8
9
  # @see Nonnative::StartError
9
10
  # @see Nonnative::StopError
@@ -37,6 +37,8 @@ module Nonnative
37
37
  # - `delay`: delay duration in seconds used by {#delay}
38
38
  # - `jitter`: optional random offset (seconds) added in `-jitter..jitter` to each `delay` (a
39
39
  # negative value uses its magnitude), so clients see variable latency instead of a flat value
40
+ # - `rate`: positive throughput limit in KB/s used by {#bandwidth}; absent or non-positive
41
+ # values forward at full speed
40
42
  # - `bytes`: positive response byte limit used by {#limit_data}; absent or non-positive values
41
43
  # use pass-through behavior
42
44
  #
@@ -20,6 +20,7 @@ module Nonnative
20
20
  # If `tools` is `nil` or empty, all tools (`prof`, `trace`, `cover`) are enabled.
21
21
  #
22
22
  # Parameter strings are parsed into argv words using shell-style quoting.
23
+ # The output directory must already exist and be writable; this helper does not create it.
23
24
  #
24
25
  # @example
25
26
  # executable = Nonnative::GoExecutable.new(%w[prof cover], './svc.test', 'reports')
@@ -31,7 +32,7 @@ module Nonnative
31
32
  class GoExecutable
32
33
  # @param tools [Array<String>, nil] tool names to enable (see class docs)
33
34
  # @param exec [String] path to the compiled Go test binary
34
- # @param output [String] output directory for generated files
35
+ # @param output [String] existing writable output directory for generated files
35
36
  def initialize(tools, exec, output)
36
37
  @tools = tools.nil? || tools.empty? ? %w[prof trace cover] : tools
37
38
  @exec = exec
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
- # Client helper for the standard gRPC health checking protocol.
4
+ # Client helper for the standard gRPC health checking protocol over an insecure plaintext channel.
5
+ # An empty or nil service name checks overall server health.
5
6
  class GRPCHealth
6
7
  NETWORK_ERRORS = [
7
8
  GRPC::BadStatus,
@@ -10,7 +11,7 @@ module Nonnative
10
11
 
11
12
  # @param host [String] gRPC server host
12
13
  # @param port [Integer] gRPC server port
13
- # @param service [String] gRPC health service name
14
+ # @param service [String, nil] gRPC health service name, or empty/nil for overall server health
14
15
  # @param timeout [Numeric] default call timeout in seconds
15
16
  def initialize(host:, port:, service:, timeout: 1)
16
17
  @host = host
@@ -21,6 +22,8 @@ module Nonnative
21
22
 
22
23
  # Calls the gRPC health check endpoint.
23
24
  #
25
+ # gRPC status and network failures are propagated to the caller.
26
+ #
24
27
  # @param deadline [Time] gRPC deadline
25
28
  # @return [Grpc::Health::V1::HealthCheckResponse]
26
29
  def check(deadline: Time.now + timeout)
@@ -29,6 +32,8 @@ module Nonnative
29
32
 
30
33
  # Returns true when the gRPC health endpoint reports SERVING.
31
34
  #
35
+ # Returns `false` for non-SERVING responses and gRPC status/network failures.
36
+ #
32
37
  # @param deadline [Time] gRPC deadline
33
38
  # @return [Boolean]
34
39
  def serving?(deadline: Time.now + timeout)
@@ -36,9 +36,10 @@ module Nonnative
36
36
  class HTTPServer < Nonnative::Server
37
37
  # Creates a Puma server for the given HTTP service and runner configuration.
38
38
  #
39
- # @param http_service [#call, Hash<String, #call>] an HTTP service instance or mount map
39
+ # @param http_service [#call, Hash<String, #call>] an HTTP service instance or `Rack::URLMap`
40
+ # mount map using path prefixes or host-qualified URLs
40
41
  # @param service [Nonnative::ConfigurationServer] server configuration
41
- # @raise [ArgumentError] if `http_service` is an empty mount map
42
+ # @raise [ArgumentError] if `http_service` is an empty mount map or contains an invalid mount path
42
43
  def initialize(http_service, service)
43
44
  if http_service.is_a?(Hash)
44
45
  raise ArgumentError, 'HTTP server requires at least one service mount' if http_service.empty?
@@ -13,7 +13,9 @@ module Nonnative
13
13
  # - `/<name>/metrics`
14
14
  #
15
15
  # Requests are performed using {Nonnative::HTTPClient}, so callers may pass RestClient options
16
- # such as `headers`, `open_timeout`, and `read_timeout`.
16
+ # such as `headers`, `open_timeout`, and `read_timeout`. HTTP error statuses are returned as response
17
+ # objects; timeouts and broken connections raise their RestClient exceptions. Requests are not
18
+ # retried automatically.
17
19
  #
18
20
  # @example
19
21
  # Nonnative.configure do |config|
@@ -6,10 +6,11 @@ module Nonnative
6
6
  # A pool is created when {Nonnative.start} is called and is accessible via {Nonnative.pool}.
7
7
  #
8
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
9
+ # - On start: services, then servers, then processes; each tier completes readiness before the next
10
+ # - On stop: processes, then servers, then services
11
11
  #
12
- # Readiness and shutdown are determined via TCP port checks ({Nonnative::Ports#open?} / {Nonnative::Ports#closed?}).
12
+ # Readiness uses TCP port checks plus optional process HTTP/gRPC probes and optional service TCP
13
+ # checks. Shutdown uses configured TCP port checks.
13
14
  #
14
15
  # @see Nonnative.start
15
16
  # @see Nonnative.stop
@@ -25,9 +26,9 @@ module Nonnative
25
26
 
26
27
  # Starts all configured runners and collects lifecycle and readiness errors.
27
28
  #
28
- # Services are started first (proxy-only) and checked for opt-in readiness, then servers and processes are
29
- # started and checked for readiness. Each readiness failure is described with the runner and, for a process
30
- # that exited early, its termination detail.
29
+ # Externally managed services are handled first and checked for opt-in readiness. Servers are then
30
+ # started and checked for readiness, followed by processes. Each readiness failure is described
31
+ # with the runner and, for a process that exited early, its termination detail.
31
32
  #
32
33
  # @return [Array<String>] lifecycle and readiness-check errors collected while starting
33
34
  def start
@@ -45,7 +46,8 @@ module Nonnative
45
46
 
46
47
  # Stops all configured runners and collects lifecycle and shutdown errors.
47
48
  #
48
- # Processes and servers are stopped first and checked for shutdown, then services are stopped (proxy-only).
49
+ # Processes and servers are stopped first and checked for shutdown, then service proxies are
50
+ # stopped when configured.
49
51
  #
50
52
  # @return [Array<String>] lifecycle and shutdown-check errors collected while stopping
51
53
  def stop
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
- # Performs aggregate TCP readiness/shutdown checks for all configured runner ports.
4
+ # Performs aggregate readiness and shutdown checks for a configured runner.
5
5
  #
6
- # A runner is considered ready only when every configured port is open, and stopped only when every
7
- # configured port is closed.
6
+ # A runner is considered ready only when every configured port is open and every configured
7
+ # HTTP/gRPC readiness probe reports ready, and stopped only when every configured port is closed.
8
8
  #
9
9
  # @see Nonnative::Port
10
10
  class Ports
@@ -15,7 +15,8 @@ module Nonnative
15
15
  @readiness = readiness_probes
16
16
  end
17
17
 
18
- # Returns whether all configured ports become connectable before their timeouts elapse.
18
+ # Returns whether all configured ports become connectable and all configured HTTP/gRPC readiness
19
+ # probes report ready before their timeouts elapse.
19
20
  #
20
21
  # @return [Boolean]
21
22
  def open?
@@ -6,7 +6,8 @@ module Nonnative
6
6
  # A process runner:
7
7
  # - spawns a child process using the configured command and environment,
8
8
  # - waits briefly (via the runner `wait`), and
9
- # - participates in readiness/shutdown via TCP port checks orchestrated by {Nonnative::Pool}.
9
+ # - participates in TCP readiness/shutdown checks plus optional HTTP/gRPC readiness probes
10
+ # orchestrated by {Nonnative::Pool}.
10
11
  #
11
12
  # The underlying configuration is a {Nonnative::ConfigurationProcess}.
12
13
  #
@@ -46,6 +47,8 @@ module Nonnative
46
47
  # Stops the process if it is running.
47
48
  #
48
49
  # The process is signalled using the configured signal (defaults to `INT` when not set).
50
+ # If it does not exit before the configured timeout, it is killed and the returned success value
51
+ # is `false`; {Nonnative.stop} reports that outcome as a {Nonnative::StopError}.
49
52
  #
50
53
  # @return [Array<(Integer, Boolean)>]
51
54
  # a tuple of:
@@ -10,7 +10,8 @@ module Nonnative
10
10
  # implementation-specific; for example {Nonnative::FaultInjectionProxy#host} and
11
11
  # {Nonnative::FaultInjectionProxy#port} return the upstream target behind the proxy.
12
12
  #
13
- # Concrete proxies typically implement these public methods:
13
+ # Custom proxies must accept the service configuration in `initialize` and implement these public
14
+ # methods, which the service and pool invoke unconditionally:
14
15
  # - `start`: begin proxying (bind/listen, start threads, etc)
15
16
  # - `stop`: stop proxying and release resources
16
17
  # - `reset`: return proxy behavior to a healthy/default state
@@ -8,7 +8,7 @@ module Nonnative
8
8
  #
9
9
  # - {Nonnative::Process} for OS-level child processes
10
10
  # - {Nonnative::Server} for in-process Ruby servers (threads)
11
- # - {Nonnative::Service} for proxy-only external dependencies
11
+ # - {Nonnative::Service} for externally managed dependencies with optional readiness/proxying
12
12
  #
13
13
  # @see Nonnative::Process
14
14
  # @see Nonnative::Server
@@ -3,9 +3,9 @@
3
3
  module Nonnative
4
4
  # Runtime runner for an external dependency.
5
5
  #
6
- # A service runner does not manage an OS process or Ruby thread. It exists so Nonnative can manage
7
- # a proxy lifecycle (start/stop/reset) for an external service that is managed elsewhere (for example
8
- # a database running in Docker).
6
+ # A service runner does not manage an OS process or Ruby thread. It represents a dependency managed
7
+ # elsewhere (for example a database running in Docker), supports optional TCP readiness, and manages
8
+ # an optional proxy lifecycle (start/stop/reset).
9
9
  #
10
10
  # The underlying configuration is a {Nonnative::ConfigurationService}.
11
11
  #
@@ -4,7 +4,9 @@ module Nonnative
4
4
  # Base socket-pair implementation used by TCP proxies.
5
5
  #
6
6
  # A socket-pair connects an accepted local socket to a remote upstream socket and forwards bytes
7
- # in both directions until one side closes.
7
+ # in both directions. When one direction reaches EOF (for example a client that half-closes its
8
+ # write side after sending a request), the paired write side is closed and the other direction
9
+ # keeps forwarding until it too ends, so an in-flight response is delivered rather than dropped.
8
10
  #
9
11
  # This is used by {Nonnative::FaultInjectionProxy} to implement pass-through forwarding, and is
10
12
  # subclassed to inject failures (close immediately, delay reads, corrupt writes, etc).
@@ -31,12 +33,14 @@ module Nonnative
31
33
  def connect(local_socket)
32
34
  @local_socket = local_socket
33
35
  @remote_socket = create_remote_socket
36
+ @open = [@local_socket, @remote_socket]
34
37
 
35
38
  loop do
36
- ready = select([@local_socket, @remote_socket], nil, nil)
39
+ ready = select(@open, nil, nil)
37
40
 
38
41
  break if pipe?(ready, @local_socket, @remote_socket)
39
42
  break if pipe?(ready, @remote_socket, @local_socket)
43
+ break if @open.empty?
40
44
  end
41
45
  ensure
42
46
  Nonnative.logger.info "finished connect for local socket '#{@local_socket.inspect}' and '#{@remote_socket&.inspect}' for 'socket_pair'"
@@ -68,6 +72,10 @@ module Nonnative
68
72
 
69
73
  # Pipes data from `source_socket` to `destination_socket` when the source is readable.
70
74
  #
75
+ # On EOF the source is half-closed rather than terminating the whole connection: the destination
76
+ # write side is closed and the source is removed from the forwarding set, so the opposite
77
+ # direction keeps forwarding until it also ends.
78
+ #
71
79
  # @param ready [Array<Array<IO>>] the result from `select`
72
80
  # @param source_socket [IO] readable side
73
81
  # @param destination_socket [IO] writable side
@@ -75,7 +83,7 @@ module Nonnative
75
83
  def pipe?(ready, source_socket, destination_socket)
76
84
  if ready[0].include?(source_socket)
77
85
  data = read(source_socket)
78
- return true if data.empty?
86
+ return half_close(source_socket, destination_socket) if data.empty?
79
87
 
80
88
  write destination_socket, data
81
89
  end
@@ -104,6 +112,21 @@ module Nonnative
104
112
  socket.write(data)
105
113
  end
106
114
 
115
+ # Half-closes one direction after its source reaches EOF: closes the destination write side and
116
+ # removes the source from the forwarding set, so the opposite direction keeps forwarding.
117
+ #
118
+ # @param source_socket [IO] the side that reached EOF
119
+ # @param destination_socket [IO] the paired side whose write half is closed
120
+ # @return [false] so the forwarding loop continues until both directions have ended
121
+ def half_close(source_socket, destination_socket)
122
+ @open.delete(source_socket)
123
+ destination_socket.close_write
124
+
125
+ false
126
+ rescue IOError, SystemCallError
127
+ false
128
+ end
129
+
107
130
  def close_socket(socket)
108
131
  return if socket.nil? || socket.closed?
109
132
 
@@ -4,5 +4,5 @@ module Nonnative
4
4
  # The current gem version.
5
5
  #
6
6
  # @return [String]
7
- VERSION = '3.29.0'
7
+ VERSION = '3.30.0'
8
8
  end
data/lib/nonnative.rb CHANGED
@@ -188,7 +188,7 @@ module Nonnative
188
188
  # Use this when passing a command string directly to `spawn`.
189
189
  #
190
190
  # @param tools [Array<String>] enabled tool names (e.g. `["prof", "trace", "cover"]`)
191
- # @param output [String] directory where outputs should be written
191
+ # @param output [String] existing writable directory where outputs should be written
192
192
  # @param exec [String] the test binary (or wrapper) to execute
193
193
  # @param cmd [String] the command argument passed to the test binary
194
194
  # @param params [Array<String>] extra parameter strings for the command
@@ -202,7 +202,7 @@ module Nonnative
202
202
  # Use this when passing argv entries directly to `spawn`.
203
203
  #
204
204
  # @param tools [Array<String>] enabled tool names (e.g. `["prof", "trace", "cover"]`)
205
- # @param output [String] directory where outputs should be written
205
+ # @param output [String] existing writable directory where outputs should be written
206
206
  # @param exec [String] the test binary (or wrapper) to execute
207
207
  # @param cmd [String] the command argument passed to the test binary
208
208
  # @param params [Array<String>] extra parameter strings for the command
@@ -237,7 +237,7 @@ module Nonnative
237
237
  #
238
238
  # @param host [String] gRPC server host
239
239
  # @param port [Integer] gRPC server port
240
- # @param service [String] gRPC health service name
240
+ # @param service [String, nil] gRPC health service name, or empty/nil for overall server health
241
241
  # @param timeout [Numeric] default call timeout in seconds
242
242
  # @return [Nonnative::GRPCHealth]
243
243
  def grpc_health(host:, port:, service:, timeout: 1) = Nonnative::GRPCHealth.new(host: host, port: port, service: service, timeout: timeout)
@@ -265,7 +265,8 @@ module Nonnative
265
265
 
266
266
  # Starts all configured services, servers, and processes, and waits for readiness.
267
267
  #
268
- # Readiness is determined by attempting to connect to each runner's configured host/ports.
268
+ # Readiness is determined by port checks, plus optional process HTTP/gRPC readiness and optional
269
+ # service TCP readiness.
269
270
  #
270
271
  # @return [void]
271
272
  # @raise [Nonnative::StartError] if one or more runners fail to start or become ready in time
@@ -325,6 +326,8 @@ module Nonnative
325
326
 
326
327
  # Clears the memoized pool instance.
327
328
  #
329
+ # This does not stop runners owned by the pool. Call {Nonnative.stop} before clearing a live pool.
330
+ #
328
331
  # @return [void]
329
332
  def clear_pool
330
333
  @pool = nil
@@ -332,8 +335,9 @@ module Nonnative
332
335
 
333
336
  # Clears memoized configuration, logger, observability client, and pool.
334
337
  #
335
- # Call this before reconfiguring Nonnative or starting a new lifecycle in the same Ruby process.
336
- # `start`/`stop` are intended to manage one lifecycle for the current pool.
338
+ # This does not stop processes, server threads, or proxies. To reconfigure in the same Ruby
339
+ # process, call {Nonnative.stop}, then `clear`, configure the next system, and call
340
+ # {Nonnative.start}.
337
341
  #
338
342
  # @return [void]
339
343
  def clear
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.29.0
4
+ version: 3.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alejandro Falkowski