nonnative 1.107.0 → 1.109.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +36 -6
  3. data/.rubocop.yml +8 -2
  4. data/AGENTS.md +248 -0
  5. data/CHANGELOG.md +141 -0
  6. data/Gemfile.lock +69 -58
  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/cucumber.rb +4 -12
  16. data/lib/nonnative/delay_socket_pair.rb +15 -0
  17. data/lib/nonnative/error.rb +7 -0
  18. data/lib/nonnative/fault_injection_proxy.rb +63 -0
  19. data/lib/nonnative/go_command.rb +34 -0
  20. data/lib/nonnative/grpc_server.rb +30 -0
  21. data/lib/nonnative/header.rb +44 -0
  22. data/lib/nonnative/http_client.rb +45 -0
  23. data/lib/nonnative/http_proxy_server.rb +62 -1
  24. data/lib/nonnative/http_server.rb +40 -0
  25. data/lib/nonnative/invalid_data_socket_pair.rb +15 -0
  26. data/lib/nonnative/no_proxy.rb +35 -0
  27. data/lib/nonnative/not_found_error.rb +7 -0
  28. data/lib/nonnative/observability.rb +44 -0
  29. data/lib/nonnative/pool.rb +50 -0
  30. data/lib/nonnative/port.rb +26 -0
  31. data/lib/nonnative/process.rb +29 -0
  32. data/lib/nonnative/proxy.rb +24 -0
  33. data/lib/nonnative/proxy_factory.rb +16 -0
  34. data/lib/nonnative/runner.rb +30 -0
  35. data/lib/nonnative/server.rb +28 -0
  36. data/lib/nonnative/service.rb +16 -0
  37. data/lib/nonnative/socket_pair.rb +46 -0
  38. data/lib/nonnative/socket_pair_factory.rb +21 -0
  39. data/lib/nonnative/start_error.rb +5 -0
  40. data/lib/nonnative/stop_error.rb +5 -0
  41. data/lib/nonnative/timeout.rb +20 -0
  42. data/lib/nonnative/version.rb +4 -1
  43. data/lib/nonnative.rb +128 -0
  44. metadata +3 -2
@@ -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
 
@@ -1,13 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Runtime runner that manages an OS-level child process.
5
+ #
6
+ # A process runner:
7
+ # - starts the configured proxy (if any),
8
+ # - spawns a child process using the configured command and environment,
9
+ # - waits briefly (via the runner `wait`), and
10
+ # - participates in readiness/shutdown via TCP port checks orchestrated by {Nonnative::Pool}.
11
+ #
12
+ # The underlying configuration is a {Nonnative::ConfigurationProcess}.
13
+ #
14
+ # @see Nonnative::ConfigurationProcess
15
+ # @see Nonnative::Pool
4
16
  class Process < Runner
17
+ # @param service [Nonnative::ConfigurationProcess] process configuration
5
18
  def initialize(service)
6
19
  super
7
20
 
8
21
  @timeout = Nonnative::Timeout.new(service.timeout)
9
22
  end
10
23
 
24
+ # Starts the proxy (if any) and spawns the configured process if it is not already running.
25
+ #
26
+ # @return [Array<(Integer, Boolean)>]
27
+ # a tuple of:
28
+ # - the spawned process id (pid)
29
+ # - whether the process appears to still be running (non-blocking wait result)
11
30
  def start
12
31
  unless process_exists?
13
32
  proxy.start
@@ -18,6 +37,11 @@ module Nonnative
18
37
  [pid, ::Process.waitpid2(pid, ::Process::WNOHANG).nil?]
19
38
  end
20
39
 
40
+ # Stops the process (if running) and stops the proxy (if any).
41
+ #
42
+ # The process is signalled using the configured signal (defaults to `INT` when not set).
43
+ #
44
+ # @return [Integer, nil] the pid that was stopped (or `nil` if the process was never started)
21
45
  def stop
22
46
  if process_exists?
23
47
  process_kill
@@ -28,6 +52,11 @@ module Nonnative
28
52
  pid
29
53
  end
30
54
 
55
+ # Returns a memoized memory reader for the spawned process.
56
+ #
57
+ # This is primarily used by acceptance tests to assert memory usage.
58
+ #
59
+ # @return [GetProcessMem, nil] a memory reader for the pid, or `nil` if not started
31
60
  def memory
32
61
  return if pid.nil?
33
62
 
@@ -1,15 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Base class for proxy implementations.
5
+ #
6
+ # A proxy is responsible for interposing behavior between a client and a target service.
7
+ # Runners ({Nonnative::Process}, {Nonnative::Server}, and {Nonnative::Service}) create a proxy
8
+ # instance via {Nonnative::ProxyFactory} based on `service.proxy.kind`.
9
+ #
10
+ # Concrete proxies typically implement these public methods:
11
+ # - `start`: begin proxying (bind/listen, start threads, etc)
12
+ # - `stop`: stop proxying and release resources
13
+ # - `reset`: return proxy behavior to a healthy/default state
14
+ # - `host` / `port`: endpoint clients should connect to when the proxy is enabled
15
+ #
16
+ # @see Nonnative::ProxyFactory
17
+ # @see Nonnative::NoProxy
18
+ # @see Nonnative::FaultInjectionProxy
4
19
  class Proxy
20
+ # @param service [Nonnative::ConfigurationRunner] runner configuration with an attached proxy configuration
5
21
  def initialize(service)
6
22
  @service = service
7
23
  end
8
24
 
9
25
  protected
10
26
 
27
+ # Returns the underlying runner configuration.
28
+ #
29
+ # @return [Nonnative::ConfigurationRunner]
11
30
  attr_reader :service
12
31
 
32
+ # Sleeps for the proxy wait interval configured on `service.proxy.wait`.
33
+ #
34
+ # Proxies can use this to allow state transitions to take effect.
35
+ #
36
+ # @return [void]
13
37
  def wait
14
38
  sleep service.proxy.wait
15
39
  end
@@ -1,8 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Factory for creating proxy instances for runners.
5
+ #
6
+ # Each runtime runner ({Nonnative::Process}, {Nonnative::Server}, {Nonnative::Service}) constructs
7
+ # a proxy via this factory. The proxy implementation is selected by `service.proxy.kind` and resolved
8
+ # using {Nonnative.proxy}.
9
+ #
10
+ # If the kind is unknown (or `"none"`), {Nonnative.proxy} returns {Nonnative::NoProxy}.
11
+ #
12
+ # @see Nonnative.proxy
13
+ # @see Nonnative.proxies
14
+ # @see Nonnative::Proxy
15
+ # @see Nonnative::NoProxy
4
16
  class ProxyFactory
5
17
  class << self
18
+ # Creates a proxy instance for the given runner configuration.
19
+ #
20
+ # @param service [Nonnative::ConfigurationRunner] runner configuration with an attached proxy configuration
21
+ # @return [Nonnative::Proxy] proxy instance (may be a {Nonnative::NoProxy})
6
22
  def create(service)
7
23
  proxy = Nonnative.proxy(service.proxy.kind)
8
24
 
@@ -1,26 +1,56 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Base runtime wrapper for a configured runnable unit.
5
+ #
6
+ # A runner wraps a configuration object (a subclass of {Nonnative::ConfigurationRunner}) and
7
+ # exposes lifecycle behavior via specialized subclasses:
8
+ #
9
+ # - {Nonnative::Process} for OS-level child processes
10
+ # - {Nonnative::Server} for in-process Ruby servers (threads)
11
+ # - {Nonnative::Service} for proxy-only external dependencies
12
+ #
13
+ # Each runner has an associated proxy instance created via {Nonnative::ProxyFactory}.
14
+ #
15
+ # @see Nonnative::Process
16
+ # @see Nonnative::Server
17
+ # @see Nonnative::Service
4
18
  class Runner
19
+ # Returns the proxy instance for this runner.
20
+ #
21
+ # @return [Nonnative::Proxy]
5
22
  attr_reader :proxy
6
23
 
24
+ # @param service [Nonnative::ConfigurationRunner] runner configuration
7
25
  def initialize(service)
8
26
  @service = service
9
27
  @proxy = Nonnative::ProxyFactory.create(service)
10
28
  end
11
29
 
30
+ # Returns the configured runner name.
31
+ #
32
+ # @return [String, nil]
12
33
  def name
13
34
  service.name
14
35
  end
15
36
 
16
37
  protected
17
38
 
39
+ # Returns the underlying configuration object.
40
+ #
41
+ # @return [Nonnative::ConfigurationRunner]
18
42
  attr_reader :service
19
43
 
44
+ # Sleeps for the configured `wait` interval after start-related work.
45
+ #
46
+ # @return [void]
20
47
  def wait_start
21
48
  sleep service.wait
22
49
  end
23
50
 
51
+ # Sleeps for the configured `wait` interval after stop-related work.
52
+ #
53
+ # @return [void]
24
54
  def wait_stop
25
55
  sleep service.wait
26
56
  end
@@ -1,13 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Runtime runner that manages an in-process Ruby server.
5
+ #
6
+ # A server runner:
7
+ # - starts the configured proxy (if any),
8
+ # - starts a Ruby thread that runs {#perform_start},
9
+ # - waits briefly (via the runner `wait`), and
10
+ # - participates in readiness/shutdown via TCP port checks orchestrated by {Nonnative::Pool}.
11
+ #
12
+ # Concrete server implementations are expected to subclass {Nonnative::Server} and implement:
13
+ # - {#perform_start} (to bind/listen and begin serving), and
14
+ # - {#perform_stop} (to gracefully shut down).
15
+ #
16
+ # The underlying configuration is a {Nonnative::ConfigurationServer}.
17
+ #
18
+ # @see Nonnative::ConfigurationServer
19
+ # @see Nonnative::Pool
4
20
  class Server < Runner
21
+ # @param service [Nonnative::ConfigurationServer] server configuration
5
22
  def initialize(service)
6
23
  super
7
24
 
8
25
  @timeout = Nonnative::Timeout.new(service.timeout)
9
26
  end
10
27
 
28
+ # Starts the proxy (if any) and starts the server thread if not already started.
29
+ #
30
+ # @return [Array<(Integer, TrueClass)>]
31
+ # a tuple of:
32
+ # - a stable identifier for this server instance (`object_id`)
33
+ # - `true` (thread creation itself is considered started; readiness is checked separately)
11
34
  def start
12
35
  unless thread
13
36
  proxy.start
@@ -21,6 +44,11 @@ module Nonnative
21
44
  [object_id, true]
22
45
  end
23
46
 
47
+ # Stops the server if it is running.
48
+ #
49
+ # Calls {#perform_stop}, terminates the server thread, stops the proxy (if any), and waits briefly.
50
+ #
51
+ # @return [Integer] the server identifier (`object_id`)
24
52
  def stop
25
53
  if thread
26
54
  perform_stop
@@ -1,13 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Runtime runner for an external dependency.
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).
9
+ #
10
+ # The underlying configuration is a {Nonnative::ConfigurationService}.
11
+ #
12
+ # @see Nonnative::ConfigurationService
13
+ # @see Nonnative::Proxy
4
14
  class Service < Runner
15
+ # Starts the configured proxy (if any).
16
+ #
17
+ # @return [void]
5
18
  def start
6
19
  proxy.start
7
20
 
8
21
  Nonnative.logger.info "started service '#{service.name}'"
9
22
  end
10
23
 
24
+ # Stops the configured proxy (if any).
25
+ #
26
+ # @return [void]
11
27
  def stop
12
28
  proxy.stop
13
29
 
@@ -1,11 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Base socket-pair implementation used by TCP proxies.
5
+ #
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.
8
+ #
9
+ # This is used by {Nonnative::FaultInjectionProxy} to implement pass-through forwarding, and is
10
+ # subclassed to inject failures (close immediately, delay reads, corrupt writes, etc).
11
+ #
12
+ # The `proxy` argument is expected to provide `host` and `port` for the upstream connection
13
+ # (typically a {Nonnative::ConfigurationProxy}).
14
+ #
15
+ # @see Nonnative::FaultInjectionProxy
16
+ # @see Nonnative::SocketPairFactory
17
+ # @see Nonnative::CloseAllSocketPair
18
+ # @see Nonnative::DelaySocketPair
19
+ # @see Nonnative::InvalidDataSocketPair
4
20
  class SocketPair
21
+ # @param proxy [#host, #port, #options] proxy configuration used to connect upstream
5
22
  def initialize(proxy)
6
23
  @proxy = proxy
7
24
  end
8
25
 
26
+ # Connects the given local socket to an upstream socket and pipes data until the connection ends.
27
+ #
28
+ # @param local_socket [TCPSocket] the accepted client socket
29
+ # @return [void]
9
30
  def connect(local_socket)
10
31
  remote_socket = create_remote_socket
11
32
 
@@ -24,12 +45,24 @@ module Nonnative
24
45
 
25
46
  protected
26
47
 
48
+ # Returns the proxy configuration.
49
+ #
50
+ # @return [Object]
27
51
  attr_reader :proxy
28
52
 
53
+ # Creates the upstream socket connection.
54
+ #
55
+ # @return [TCPSocket]
29
56
  def create_remote_socket
30
57
  ::TCPSocket.new(proxy.host, proxy.port)
31
58
  end
32
59
 
60
+ # Pipes data from `socket1` to `socket2` if `socket1` is readable.
61
+ #
62
+ # @param ready [Array<Array<IO>>] the result from `select`
63
+ # @param socket1 [IO] readable side
64
+ # @param socket2 [IO] writable side
65
+ # @return [Boolean] whether the piping loop should terminate
33
66
  def pipe?(ready, socket1, socket2)
34
67
  if ready[0].include?(socket1)
35
68
  data = read(socket1)
@@ -41,10 +74,23 @@ module Nonnative
41
74
  false
42
75
  end
43
76
 
77
+ # Reads bytes from the given socket.
78
+ #
79
+ # Subclasses can override this to inject behavior (e.g. delay).
80
+ #
81
+ # @param socket [IO]
82
+ # @return [String]
44
83
  def read(socket)
45
84
  socket.recv(1024) || ''
46
85
  end
47
86
 
87
+ # Writes bytes to the given socket.
88
+ #
89
+ # Subclasses can override this to inject behavior (e.g. corrupt data).
90
+ #
91
+ # @param socket [IO]
92
+ # @param data [String]
93
+ # @return [Integer] number of bytes written
48
94
  def write(socket, data)
49
95
  socket.write(data)
50
96
  end
@@ -1,8 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Factory for creating socket-pair implementations used by {Nonnative::FaultInjectionProxy}.
5
+ #
6
+ # A socket-pair is responsible for wiring a local accepted socket to a remote upstream socket,
7
+ # optionally injecting failures (close connections, add delays, corrupt data, etc).
8
+ #
9
+ # Proxy states are mapped as follows:
10
+ # - `:none` (or any unknown value) -> {Nonnative::SocketPair} (pass-through)
11
+ # - `:close_all` -> {Nonnative::CloseAllSocketPair}
12
+ # - `:delay` -> {Nonnative::DelaySocketPair}
13
+ # - `:invalid_data` -> {Nonnative::InvalidDataSocketPair}
14
+ #
15
+ # @see Nonnative::FaultInjectionProxy
16
+ # @see Nonnative::SocketPair
17
+ # @see Nonnative::CloseAllSocketPair
18
+ # @see Nonnative::DelaySocketPair
19
+ # @see Nonnative::InvalidDataSocketPair
4
20
  class SocketPairFactory
5
21
  class << self
22
+ # Creates a socket-pair instance for the given proxy state.
23
+ #
24
+ # @param kind [Symbol] proxy state (e.g. `:none`, `:close_all`, `:delay`, `:invalid_data`)
25
+ # @param proxy [Nonnative::ConfigurationProxy] proxy configuration (host/port/options)
26
+ # @return [Nonnative::SocketPair] a socket-pair implementation instance
6
27
  def create(kind, proxy)
7
28
  pair = case kind
8
29
  when :close_all
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Raised when {Nonnative.start} fails to start one or more configured runners.
5
+ #
6
+ # The error message typically contains one line per failing runner.
7
+ #
8
+ # @see Nonnative.start
4
9
  class StartError < Nonnative::Error
5
10
  end
6
11
  end
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Raised when {Nonnative.stop} fails to stop one or more configured runners within the configured timeouts.
5
+ #
6
+ # The error message typically contains one line per runner that did not stop cleanly in time.
7
+ #
8
+ # @see Nonnative.stop
4
9
  class StopError < Nonnative::Error
5
10
  end
6
11
  end
@@ -1,11 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Small helper to run a block with a timeout and convert timeout errors into `false`.
5
+ #
6
+ # This is used internally for readiness/shutdown loops (for example port checks) where the common
7
+ # control-flow is “keep retrying until the timeout elapses”.
8
+ #
9
+ # @example
10
+ # timeout = Nonnative::Timeout.new(1) # seconds
11
+ # ok = timeout.perform do
12
+ # # do work that may take time
13
+ # true
14
+ # end
15
+ # # ok is either the block result or false if the timeout elapsed
16
+ #
4
17
  class Timeout
18
+ # @param time [Numeric] timeout duration in seconds
5
19
  def initialize(time)
6
20
  @time = time
7
21
  end
8
22
 
23
+ # Executes the given block with the configured timeout.
24
+ #
25
+ # If the timeout elapses, returns `false` instead of raising `Timeout::Error`.
26
+ #
27
+ # @yield the work to execute under a timeout
28
+ # @return [Object, false] the block's return value, or `false` if the timeout elapsed
9
29
  def perform(&)
10
30
  ::Timeout.timeout(time, &)
11
31
  rescue ::Timeout::Error
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
- VERSION = '1.107.0'
4
+ # The current gem version.
5
+ #
6
+ # @return [String]
7
+ VERSION = '1.109.0'
5
8
  end