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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +36 -6
- data/.rubocop.yml +8 -2
- data/AGENTS.md +248 -0
- data/CHANGELOG.md +141 -0
- data/Gemfile.lock +69 -58
- data/README.md +51 -32
- data/lib/nonnative/close_all_socket_pair.rb +14 -0
- data/lib/nonnative/configuration.rb +67 -0
- data/lib/nonnative/configuration_process.rb +14 -0
- data/lib/nonnative/configuration_proxy.rb +28 -0
- data/lib/nonnative/configuration_runner.rb +44 -0
- data/lib/nonnative/configuration_server.rb +12 -0
- data/lib/nonnative/configuration_service.rb +9 -0
- data/lib/nonnative/cucumber.rb +4 -12
- data/lib/nonnative/delay_socket_pair.rb +15 -0
- data/lib/nonnative/error.rb +7 -0
- data/lib/nonnative/fault_injection_proxy.rb +63 -0
- data/lib/nonnative/go_command.rb +34 -0
- data/lib/nonnative/grpc_server.rb +30 -0
- data/lib/nonnative/header.rb +44 -0
- data/lib/nonnative/http_client.rb +45 -0
- data/lib/nonnative/http_proxy_server.rb +62 -1
- data/lib/nonnative/http_server.rb +40 -0
- data/lib/nonnative/invalid_data_socket_pair.rb +15 -0
- data/lib/nonnative/no_proxy.rb +35 -0
- data/lib/nonnative/not_found_error.rb +7 -0
- data/lib/nonnative/observability.rb +44 -0
- data/lib/nonnative/pool.rb +50 -0
- data/lib/nonnative/port.rb +26 -0
- data/lib/nonnative/process.rb +29 -0
- data/lib/nonnative/proxy.rb +24 -0
- data/lib/nonnative/proxy_factory.rb +16 -0
- data/lib/nonnative/runner.rb +30 -0
- data/lib/nonnative/server.rb +28 -0
- data/lib/nonnative/service.rb +16 -0
- data/lib/nonnative/socket_pair.rb +46 -0
- data/lib/nonnative/socket_pair_factory.rb +21 -0
- data/lib/nonnative/start_error.rb +5 -0
- data/lib/nonnative/stop_error.rb +5 -0
- data/lib/nonnative/timeout.rb +20 -0
- data/lib/nonnative/version.rb +4 -1
- data/lib/nonnative.rb +128 -0
- 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
|
data/lib/nonnative/pool.rb
CHANGED
|
@@ -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 }
|
data/lib/nonnative/port.rb
CHANGED
|
@@ -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
|
|
data/lib/nonnative/process.rb
CHANGED
|
@@ -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
|
|
data/lib/nonnative/proxy.rb
CHANGED
|
@@ -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
|
|
data/lib/nonnative/runner.rb
CHANGED
|
@@ -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
|
data/lib/nonnative/server.rb
CHANGED
|
@@ -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
|
data/lib/nonnative/service.rb
CHANGED
|
@@ -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
|
data/lib/nonnative/stop_error.rb
CHANGED
|
@@ -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
|
data/lib/nonnative/timeout.rb
CHANGED
|
@@ -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
|