nonnative 3.13.0 → 3.15.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: 304a9d9d01fafb9f0822e4510c647f87095f78987feecb5a9d2eddc43317a880
4
- data.tar.gz: 366157abd34f6ffa48cce159dffd49075e4a98653ea5d3f3b11b2494ad47b4a1
3
+ metadata.gz: 8c6665f1bde87f38b1ba0622168efd569d1cce78df5773597984ab3eb07e07e6
4
+ data.tar.gz: 5662743abeb5f331675ef634754c024fed13f876664d5e0beccea21199c47f9a
5
5
  SHA512:
6
- metadata.gz: 6fa7d92393e3a4df9f400a7f35c857998c13dc026ec85be407fa37d6e8ba2189c5992af70e001c1cdfaf9589d1b6d33a2daf11abf8361f84f07ab8bc3a72fed1
7
- data.tar.gz: bf37673b803ff66338009fbbd458d707c6eed9ef0ba618bb53b97e2f5770788708343f6bd34b67758fd6d1a13e464f63edffced76c26591f8ac4fe4ec2267a3e
6
+ metadata.gz: 1ec6d29ca79065600e123c9ec4e78d3d0b8ca76f06538b88c21b771f7be3ffce428f77949c80da574f7420826385e637de94f164711be82a5c7bc664a8ca9e41
7
+ data.tar.gz: 0aba7bdf975ef0f28ae9eb5f14ab4a78d8cf160ac09e63ebaff38d8edddbec8bb3fb94855dec864f18972771ad97076bcb0cfae2e5ad3713e6e0f87e65c38de6
data/.circleci/config.yml CHANGED
@@ -17,7 +17,7 @@ executors:
17
17
  - image: alexfalkowski/ruby:3.19
18
18
  release:
19
19
  docker:
20
- - image: alexfalkowski/release:8.19
20
+ - image: alexfalkowski/release:8.20
21
21
  alpine:
22
22
  docker:
23
23
  - image: alpine:latest
data/AGENTS.md CHANGED
@@ -79,6 +79,32 @@ of dependencies.
79
79
  thread. Do not flag the listener `join` as an unbounded reliability gap unless
80
80
  the task is explicitly about proxy shutdown behavior or there is a normal-use
81
81
  reproducer, failing CI evidence, or platform-specific hang evidence.
82
+ - Service readiness is intentionally TCP-only for externally managed
83
+ dependencies. Do not flag missing HTTP/gRPC service readiness as a feature gap
84
+ unless the task is explicitly about changing the `Nonnative::Service`
85
+ readiness model. HTTP/gRPC readiness belongs to managed processes, where the
86
+ user explicitly models the application health endpoints.
87
+ - Process runners intentionally inherit the parent working directory. Nonnative
88
+ is normally run from the test folder that owns `test/nonnative.yml`, relative
89
+ config paths, logs, and reports. Do not flag missing process cwd/chdir support
90
+ as a feature gap unless the task is explicitly about changing process working
91
+ directory behavior.
92
+ - Message-specific start/stop assertions and attempted-stop success assertions
93
+ in `features/step_definitions/lifecycle_steps.rb` are repository-local
94
+ Cucumber helpers. Do not flag their absence from `lib/nonnative/cucumber.rb`
95
+ as a feature gap unless downstream usage evidence exists or the task is
96
+ explicitly about expanding public lifecycle assertion steps.
97
+ - Liveness, readiness, and metrics endpoint assertion steps in
98
+ `features/step_definitions/servers_steps.rb` are repository-local Cucumber
99
+ helpers for testing nonnative's own framework assumptions. Do not flag their
100
+ absence from `lib/nonnative/cucumber.rb` as a feature gap unless downstream
101
+ usage evidence exists or the task is explicitly about expanding public
102
+ observability assertion steps.
103
+ - `Nonnative::Configuration` intentionally exposes `process_by_name` without
104
+ matching `server_by_name` or `service_by_name` helpers. Do not flag missing
105
+ server/service configuration lookup helpers as a feature gap unless downstream
106
+ usage evidence exists or the task is explicitly about changing the
107
+ pre-start configuration lookup API.
82
108
 
83
109
  ## Runtime Model
84
110
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- nonnative (3.13.0)
4
+ nonnative (3.15.0)
5
5
  concurrent-ruby (>= 1, < 2)
6
6
  config (>= 5, < 6)
7
7
  cucumber (>= 7, < 12)
data/README.md CHANGED
@@ -410,7 +410,7 @@ Define your server:
410
410
  ```ruby
411
411
  module Nonnative
412
412
  module Features
413
- class Hello < Sinatra::Application
413
+ class HelloService < Nonnative::HTTPService
414
414
  get '/hello' do
415
415
  'Hello World!'
416
416
  end
@@ -418,7 +418,7 @@ module Nonnative
418
418
 
419
419
  class HTTPServer < Nonnative::HTTPServer
420
420
  def initialize(service)
421
- super(Sinatra.new(Hello), service)
421
+ super(HelloService.new, service)
422
422
  end
423
423
  end
424
424
  end
@@ -543,7 +543,7 @@ end
543
543
 
544
544
  Define your server:
545
545
 
546
- Assume the gRPC service base class and response types below come from your generated gRPC stubs.
546
+ Assume the gRPC service type and response types below come from your generated gRPC stubs.
547
547
 
548
548
  ```ruby
549
549
  module Nonnative
@@ -758,6 +758,7 @@ Clients connect to the service `host`/`port`, while the proxy forwards traffic t
758
758
 
759
759
  - `close_all` - Closes the socket as soon as it connects.
760
760
  - `delay` - Delays traffic on the connection. Defaults to 2 seconds and can be configured through options.
761
+ - `timeout` - Accepts the connection and stalls traffic until reset or stop closes the connection, so clients exercise their own read timeout behavior.
761
762
  - `invalid_data` - Forwards client requests unchanged, then corrupts upstream responses before they reach the client.
762
763
 
763
764
  ###### 🧩 Fault Injection Services
@@ -769,6 +770,7 @@ name = 'name of service in configuration'
769
770
  service = Nonnative.pool.service_by_name(name)
770
771
 
771
772
  service.proxy.close_all # To use close_all.
773
+ service.proxy.timeout # To stall traffic until reset or stop.
772
774
  service.proxy.reset # To reset it back to a good state.
773
775
  ```
774
776
 
@@ -776,6 +778,7 @@ Use the Cucumber proxy steps:
776
778
 
777
779
  ```cucumber
778
780
  Given I set the proxy for service 'service_1' to 'close_all'
781
+ Given I set the proxy for service 'service_1' to 'timeout'
779
782
  Then I should reset the proxy for service 'service_1'
780
783
  ```
781
784
 
@@ -4,8 +4,8 @@ module Nonnative
4
4
  # Proxy configuration attached to a service configuration.
5
5
  #
6
6
  # A proxy allows you to interpose behavior between a client and a real service. For example,
7
- # the built-in `"fault_injection"` proxy can close connections, introduce delays, or corrupt data
8
- # for resilience testing.
7
+ # the built-in `"fault_injection"` proxy can close connections, introduce delays, stall traffic,
8
+ # or corrupt data for resilience testing.
9
9
  #
10
10
  # This object is created automatically for each service via {Nonnative::ConfigurationService}.
11
11
  # When `kind` is set to `"none"`, no proxy is started and the service will use its configured
@@ -56,6 +56,7 @@ module Nonnative
56
56
  PROXY_OPERATIONS = {
57
57
  'close_all' => :close_all,
58
58
  'delay' => :delay,
59
+ 'timeout' => :timeout,
59
60
  'invalid_data' => :invalid_data,
60
61
  'reset' => :reset
61
62
  }.freeze
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
- # Base class for all Nonnative errors.
4
+ # Parent error for all Nonnative errors.
5
5
  #
6
6
  # Catch this error type if you want to handle any exception raised by this gem.
7
7
  #
@@ -11,6 +11,7 @@ module Nonnative
11
11
  #
12
12
  # - {#close_all}: close connections immediately on accept
13
13
  # - {#delay}: delay reads by a configured duration (default: 2 seconds)
14
+ # - {#timeout}: accept connections and keep them silent until clients time out
14
15
  # - {#invalid_data}: forward requests unchanged and mutate upstream responses before they reach clients
15
16
  # - {#reset}: return to healthy pass-through behavior
16
17
  #
@@ -107,6 +108,17 @@ module Nonnative
107
108
  apply_state :delay
108
109
  end
109
110
 
111
+ # Accepts connections and stalls without forwarding bytes.
112
+ #
113
+ # This simulates a dependency that accepts a TCP connection but leaves clients waiting until
114
+ # their own read timeout fires. The proxy keeps the connection silent until reset or stop closes
115
+ # active connections.
116
+ #
117
+ # @return [void]
118
+ def timeout
119
+ apply_state :timeout
120
+ end
121
+
110
122
  # Mutates upstream responses while forwarding client requests unchanged.
111
123
  #
112
124
  # @return [void]
@@ -7,7 +7,7 @@
7
7
  #
8
8
  # This file defines two classes:
9
9
  #
10
- # - {Nonnative::HTTPProxy}: a Sinatra application that implements the proxying behavior.
10
+ # - {Nonnative::HTTPProxy}: an HTTP service that implements the proxying behavior.
11
11
  # - {Nonnative::HTTPProxyServer}: a {Nonnative::HTTPServer} wrapper that runs the proxy app under Puma.
12
12
  #
13
13
  # Notes:
@@ -17,12 +17,12 @@
17
17
  # @see Nonnative::HTTPServer
18
18
  # @see Nonnative::Server
19
19
  module Nonnative
20
- # Sinatra application implementing a simple forward proxy.
20
+ # HTTP service implementing a simple forward proxy.
21
21
  #
22
- # The upstream host is configured via Sinatra settings (see {Nonnative::HTTPProxyServer}).
22
+ # The upstream host is configured via service settings (see {Nonnative::HTTPProxyServer}).
23
23
  #
24
24
  # Supported HTTP verbs: GET, POST, PUT, PATCH, DELETE.
25
- class HTTPProxy < Sinatra::Application
25
+ class HTTPProxy < Nonnative::HTTPService
26
26
  NON_FORWARDABLE_HEADERS = %w[
27
27
  Host
28
28
  Accept-Encoding
@@ -135,13 +135,11 @@ module Nonnative
135
135
  # @param host [String] upstream host to proxy to (HTTPS)
136
136
  # @param service [Nonnative::ConfigurationServer] server configuration
137
137
  def initialize(host, service)
138
- app = Sinatra.new(Nonnative::HTTPProxy) do
139
- configure do
140
- set :host, host
141
- end
138
+ http_service = Class.new(Nonnative::HTTPProxy) do
139
+ set :host, host
142
140
  end
143
141
 
144
- super(app, service)
142
+ super(http_service.new, service)
145
143
  end
146
144
  end
147
145
  end
@@ -3,19 +3,19 @@
3
3
  module Nonnative
4
4
  # Puma-based HTTP server runner.
5
5
  #
6
- # This is a convenience server implementation for running a Rack/Sinatra application in-process
7
- # under Nonnative's server lifecycle. It binds to the configured server `host` and first `ports` entry.
6
+ # This is a convenience server implementation for running an HTTP service in-process under
7
+ # Nonnative's server lifecycle. It binds to the configured server `host` and first `ports` entry.
8
8
  #
9
9
  # The server is started and stopped by {Nonnative::Server} via {#perform_start} / {#perform_stop}.
10
10
  #
11
- # @example Running a Sinatra app
11
+ # @example Running an HTTP service
12
+ # class HelloHTTPService < Nonnative::HTTPService
13
+ # get('/hello') { 'Hello World!' }
14
+ # end
15
+ #
12
16
  # class HelloHTTPServer < Nonnative::HTTPServer
13
17
  # def initialize(service)
14
- # app = Sinatra.new do
15
- # get('/hello') { 'Hello World!' }
16
- # end
17
- #
18
- # super(app, service)
18
+ # super(HelloHTTPService.new, service)
19
19
  # end
20
20
  # end
21
21
  #
@@ -34,18 +34,18 @@ module Nonnative
34
34
  #
35
35
  # @see Nonnative::Server
36
36
  class HTTPServer < Nonnative::Server
37
- # Creates a Puma server for the given Rack app and runner configuration.
37
+ # Creates a Puma server for the given HTTP service and runner configuration.
38
38
  #
39
- # @param app [#call] a Rack-compatible application (e.g. Sinatra/Rack app)
39
+ # @param http_service [#call] an HTTP service instance
40
40
  # @param service [Nonnative::ConfigurationServer] server configuration
41
- def initialize(app, service)
41
+ def initialize(http_service, service)
42
42
  # Keep the log IO so the server lifecycle can release Puma's file handle on stop.
43
43
  @log = File.open(service.log, 'a')
44
44
  options = {
45
45
  log_writer: Puma::LogWriter.new(log, log),
46
46
  force_shutdown_after: service.timeout
47
47
  }
48
- @server = Puma::Server.new(app, Puma::Events.new, options)
48
+ @server = Puma::Server.new(http_service, Puma::Events.new, options)
49
49
 
50
50
  super(service)
51
51
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nonnative
4
+ # HTTP service run by {Nonnative::HTTPServer}.
5
+ #
6
+ # Subclass this instead of `Sinatra::Application` when defining an in-process HTTP server for
7
+ # Nonnative. {Nonnative::HTTPServer} accepts a service instance and runs it under Puma.
8
+ class HTTPService < Sinatra::Application
9
+ end
10
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
- # Base class for proxy implementations.
4
+ # Parent type for proxy implementations.
5
5
  #
6
6
  # A proxy is responsible for interposing behavior between a client and a target service.
7
7
  # Runtime services create a proxy instance via {Nonnative::ProxyFactory} based on `service.proxy.kind`.
@@ -16,6 +16,7 @@ module Nonnative
16
16
  # @see Nonnative::SocketPairFactory
17
17
  # @see Nonnative::CloseAllSocketPair
18
18
  # @see Nonnative::DelaySocketPair
19
+ # @see Nonnative::TimeoutSocketPair
19
20
  # @see Nonnative::InvalidDataSocketPair
20
21
  class SocketPair
21
22
  # @param proxy [#host, #port, #options] proxy configuration used to connect upstream
@@ -10,18 +10,20 @@ module Nonnative
10
10
  # - `:none` (or any unknown value) -> {Nonnative::SocketPair} (pass-through)
11
11
  # - `:close_all` -> {Nonnative::CloseAllSocketPair}
12
12
  # - `:delay` -> {Nonnative::DelaySocketPair}
13
+ # - `:timeout` -> {Nonnative::TimeoutSocketPair}
13
14
  # - `:invalid_data` -> {Nonnative::InvalidDataSocketPair}
14
15
  #
15
16
  # @see Nonnative::FaultInjectionProxy
16
17
  # @see Nonnative::SocketPair
17
18
  # @see Nonnative::CloseAllSocketPair
18
19
  # @see Nonnative::DelaySocketPair
20
+ # @see Nonnative::TimeoutSocketPair
19
21
  # @see Nonnative::InvalidDataSocketPair
20
22
  class SocketPairFactory
21
23
  class << self
22
24
  # Creates a socket-pair instance for the given proxy state.
23
25
  #
24
- # @param kind [Symbol] proxy state (e.g. `:none`, `:close_all`, `:delay`, `:invalid_data`)
26
+ # @param kind [Symbol] proxy state (e.g. `:none`, `:close_all`, `:delay`, `:timeout`, `:invalid_data`)
25
27
  # @param proxy [Nonnative::ConfigurationProxy] proxy configuration (host/port/options)
26
28
  # @return [Nonnative::SocketPair] a socket-pair implementation instance
27
29
  def create(kind, proxy)
@@ -30,6 +32,8 @@ module Nonnative
30
32
  CloseAllSocketPair
31
33
  when :delay
32
34
  DelaySocketPair
35
+ when :timeout
36
+ TimeoutSocketPair
33
37
  when :invalid_data
34
38
  InvalidDataSocketPair
35
39
  else
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nonnative
4
+ # Socket-pair variant used by the fault-injection proxy to simulate read timeouts.
5
+ #
6
+ # When active, the proxy accepts the client connection and keeps it open without forwarding bytes.
7
+ # Clients with read deadlines should observe their own timeout behavior.
8
+ #
9
+ # This behavior is enabled by calling {Nonnative::FaultInjectionProxy#timeout}.
10
+ #
11
+ # @see Nonnative::FaultInjectionProxy
12
+ # @see Nonnative::SocketPairFactory
13
+ # @see Nonnative::SocketPair
14
+ class TimeoutSocketPair < SocketPair
15
+ # Keeps the accepted socket silent until reset or stop closes active proxy connections.
16
+ #
17
+ # @param local_socket [TCPSocket] the accepted client socket
18
+ # @return [void]
19
+ def connect(local_socket)
20
+ @local_socket = local_socket
21
+
22
+ Nonnative.logger.info "stalling socket '#{local_socket.inspect}' for 'timeout' pair"
23
+
24
+ # Do not call super: the base implementation opens the upstream socket and forwards traffic.
25
+ sleep 0.1 until local_socket.closed?
26
+ ensure
27
+ close
28
+ end
29
+ end
30
+ end
@@ -4,5 +4,5 @@ module Nonnative
4
4
  # The current gem version.
5
5
  #
6
6
  # @return [String]
7
- VERSION = '3.13.0'
7
+ VERSION = '3.15.0'
8
8
  end
data/lib/nonnative.rb CHANGED
@@ -91,6 +91,7 @@ require 'nonnative/tcp_probe'
91
91
  require 'nonnative/http_probe'
92
92
  require 'nonnative/grpc_health'
93
93
  require 'nonnative/grpc_probe'
94
+ require 'nonnative/http_service'
94
95
  require 'nonnative/http_server'
95
96
  require 'nonnative/http_proxy_server'
96
97
  require 'nonnative/grpc_server'
@@ -102,6 +103,7 @@ require 'nonnative/fault_injection_proxy'
102
103
  require 'nonnative/socket_pair'
103
104
  require 'nonnative/close_all_socket_pair'
104
105
  require 'nonnative/delay_socket_pair'
106
+ require 'nonnative/timeout_socket_pair'
105
107
  require 'nonnative/invalid_data_socket_pair'
106
108
  require 'nonnative/socket_pair_factory'
107
109
  require 'nonnative/go_executable'
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.13.0
4
+ version: 3.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alejandro Falkowski
@@ -310,6 +310,7 @@ files:
310
310
  - lib/nonnative/http_probe.rb
311
311
  - lib/nonnative/http_proxy_server.rb
312
312
  - lib/nonnative/http_server.rb
313
+ - lib/nonnative/http_service.rb
313
314
  - lib/nonnative/invalid_data_socket_pair.rb
314
315
  - lib/nonnative/no_proxy.rb
315
316
  - lib/nonnative/not_found_error.rb
@@ -330,6 +331,7 @@ files:
330
331
  - lib/nonnative/stop_error.rb
331
332
  - lib/nonnative/tcp_probe.rb
332
333
  - lib/nonnative/timeout.rb
334
+ - lib/nonnative/timeout_socket_pair.rb
333
335
  - lib/nonnative/version.rb
334
336
  - nonnative.gemspec
335
337
  homepage: https://github.com/alexfalkowski/nonnative