nonnative 3.30.0 → 3.32.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: 954eb6b4c3c122729406d275f3befa59e19b8db405e14cb2ee8fd759354f97ba
4
- data.tar.gz: 39291c978f15b62b187aeb51b5d51f8b0479d314261f1eaab85e6d9f441b3a54
3
+ metadata.gz: 108231da69ec8bdf5cd71d3b0a2bb1e724c7b7826a3c33b631044873d2de71cb
4
+ data.tar.gz: 43b4fb6617eebce09db0b460aa0ad8117290a5a8f952eccbe33f8fbe89ad6766
5
5
  SHA512:
6
- metadata.gz: f2a6efd54e9af260f1e0261347e0f2d93825392833bbbc8818e073dfd13359eb3739afa8bd535fea01e502fbe3d0664abbae49b41eb17fed1531c1a19019e0f1
7
- data.tar.gz: 3b6f5e46899a77d275500d7d5edca75a46f739db103d7b006ca8cbe8699a24edc362679226ba7e67221dd2fb1cc200e09c4c366b9b3faa8a42fa17be61b9b246
6
+ metadata.gz: 7c9446f51fe18cd021c0baa25d0de2ab3389c01e3d1ec571decc3855c711737012cc5268f42acac6b604f9b9aa7bb957ce5197caf74d8150bf0e395291de78e3
7
+ data.tar.gz: 1b4ce6b1bb74f9952f93f66d8064ae90f66108e7fb2038f35e4d52e4dcfe02b40c58469402883c2bd737246d35eb4a44a132b8033a2aa06980e2d7baff580a29
data/README.md CHANGED
@@ -276,8 +276,8 @@ The shipped steps are compatibility surface for downstream suites:
276
276
  text: `Then I should see a log entry of {string} for process {string}` and
277
277
  `Then I should see a log entry of {string} in the file {string}`.
278
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}`.
279
+ `timeout`, `invalid_data`, `bandwidth`, `limit_data`, `slicer`, `flaky`, or `reset`; the reset action
280
+ step is `Then I should reset the proxy for service {string}`.
281
281
 
282
282
  ```cucumber
283
283
  @manual
@@ -597,9 +597,11 @@ end
597
597
 
598
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.
599
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.
600
+ The upstream scheme defaults to HTTPS on the scheme's default port; pass `scheme:`/`port:` to
601
+ `Nonnative::HTTPProxyServer.new` to target an `http://` upstream or a non-default port. The proxy
602
+ forwards the request path and query for `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, and
603
+ `OPTIONS`, while removing proxy credentials plus `Host` and `Accept-Encoding` before the upstream
604
+ request.
603
605
 
604
606
  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.
605
607
 
@@ -932,6 +934,8 @@ Clients connect to the service `host`/`port`, while the proxy forwards traffic t
932
934
  - `invalid_data` - Forwards client requests unchanged, then corrupts upstream responses before they reach the client.
933
935
  - `bandwidth` - Throttles forwarded throughput to `options.rate` kilobytes per second (1 KB = 1024 bytes) by sleeping in proportion to the bytes read, in both directions, so clients see a slow-but-alive dependency. When `rate` is absent or not positive, traffic forwards at full speed.
934
936
  - `limit_data` - Forwards client requests unchanged, then sends the first `options.bytes` bytes of the upstream byte stream on each connection and gracefully closes the connection. When `bytes` is absent or not positive, traffic forwards at full speed.
937
+ - `slicer` - Forwards client requests unchanged, then writes each response to the client in `options.slice_size`-byte pieces, optionally separated by `options.slice_delay` seconds, so a client must perform multiple reads to reassemble the message. When `slice_size` is absent or not positive, traffic forwards at full speed.
938
+ - `flaky` - Fails a fraction of new connections (closed immediately, like `close_all`) controlled by `options.probability` (0.0-1.0), forwarding the rest normally, so a client's retry/reconnect logic sees both failures and successes while this state stays active. When `probability` is absent or not positive, traffic forwards at full speed.
935
939
 
936
940
  ###### 🧩 Fault Injection Services
937
941
 
@@ -61,6 +61,8 @@ module Nonnative
61
61
  'invalid_data' => :invalid_data,
62
62
  'bandwidth' => :bandwidth,
63
63
  'limit_data' => :limit_data,
64
+ 'slicer' => :slicer,
65
+ 'flaky' => :flaky,
64
66
  'reset' => :reset
65
67
  }.freeze
66
68
 
@@ -16,6 +16,8 @@ module Nonnative
16
16
  # - {#invalid_data}: forward requests unchanged and mutate upstream responses before they reach clients
17
17
  # - {#bandwidth}: throttle forwarded throughput to a configured rate (KB/s)
18
18
  # - {#limit_data}: forward a configured number of response bytes, then gracefully close
19
+ # - {#slicer}: forward responses to the client in small writes to force multi-`recv` reassembly
20
+ # - {#flaky}: fail a configurable fraction of new connections, forwarding the rest normally
19
21
  # - {#reset}: return to healthy pass-through behavior
20
22
  #
21
23
  # State changes terminate any active connections so new connections observe the new behavior.
@@ -41,6 +43,11 @@ module Nonnative
41
43
  # values forward at full speed
42
44
  # - `bytes`: positive response byte limit used by {#limit_data}; absent or non-positive values
43
45
  # use pass-through behavior
46
+ # - `slice_size`: positive response slice size (bytes) used by {#slicer}; absent or non-positive
47
+ # values use pass-through behavior
48
+ # - `slice_delay`: optional delay (seconds) between slices used by {#slicer}
49
+ # - `probability`: connection failure fraction (0.0-1.0) used by {#flaky}; absent or non-positive
50
+ # values use pass-through behavior
44
51
  #
45
52
  # @see Nonnative::Proxy
46
53
  # @see Nonnative::SocketPairFactory
@@ -91,6 +98,10 @@ module Nonnative
91
98
  server&.close
92
99
 
93
100
  listener_thread = thread
101
+ # Closing the server is meant to wake the blocked `accept`, but a concurrently blocked
102
+ # `accept` is not reliably interrupted by `close` on every platform. Kill the listener so
103
+ # `stop` cannot hang joining an accept loop that never woke (a no-op once it has exited).
104
+ listener_thread&.kill
94
105
  listener_thread&.join
95
106
 
96
107
  @tcp_server = nil
@@ -166,6 +177,31 @@ module Nonnative
166
177
  apply_state :limit_data
167
178
  end
168
179
 
180
+ # Fragments upstream responses into small writes before forwarding to the client.
181
+ #
182
+ # Client requests are forwarded unchanged. Each response is split into
183
+ # `service.proxy.options[:slice_size]`-byte writes, optionally separated by
184
+ # `service.proxy.options[:slice_delay]` seconds, so a client's `recv` returns a strict prefix of
185
+ # the response rather than the whole message. When `slice_size` is absent or not positive, the
186
+ # connection forwards at full speed without slicing.
187
+ #
188
+ # @return [void]
189
+ def slicer
190
+ apply_state :slicer
191
+ end
192
+
193
+ # Fails a configurable fraction of new connections while forwarding the rest normally.
194
+ #
195
+ # The fraction is controlled by `service.proxy.options[:probability]` (0.0-1.0); absent or
196
+ # non-positive values behave like pass-through, and `1.0` fails every connection. Because each
197
+ # connection decides independently, clients that retry/reconnect can observe both failures and
198
+ # successes while this state stays active.
199
+ #
200
+ # @return [void]
201
+ def flaky
202
+ apply_state :flaky
203
+ end
204
+
169
205
  # Resets the proxy back to healthy pass-through behavior.
170
206
  #
171
207
  # @return [void]
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nonnative
4
+ # Socket-pair variant used by the fault-injection proxy to simulate a flapping dependency.
5
+ #
6
+ # When active, each new connection independently fails with probability
7
+ # `proxy.options[:probability]` (closed immediately, like {Nonnative::CloseAllSocketPair}) and
8
+ # otherwise forwards normally. Because the decision is made per connection rather than per state, a
9
+ # client that retries/reconnects can observe some attempts fail and others succeed while this fault
10
+ # state stays active, exercising retry/reconnect/circuit-breaker recovery rather than a fully down
11
+ # dependency. A missing or non-positive `probability` behaves like pass-through; `probability >= 1.0`
12
+ # fails every connection.
13
+ #
14
+ # This behavior is enabled by calling {Nonnative::FaultInjectionProxy#flaky}.
15
+ #
16
+ # @see Nonnative::FaultInjectionProxy
17
+ # @see Nonnative::SocketPairFactory
18
+ # @see Nonnative::SocketPair
19
+ # @see Nonnative::CloseAllSocketPair
20
+ class FlakySocketPair < SocketPair
21
+ # Fails the connection with the configured probability, otherwise forwards it normally.
22
+ #
23
+ # @param local_socket [TCPSocket] the accepted client socket
24
+ # @return [void]
25
+ def connect(local_socket)
26
+ return super unless fail?
27
+
28
+ Nonnative.logger.info "closing socket '#{local_socket.inspect}' for 'flaky' pair"
29
+
30
+ local_socket.close
31
+ end
32
+
33
+ private
34
+
35
+ # Decides whether this connection should fail, based on `proxy.options[:probability]`.
36
+ #
37
+ # @return [Boolean]
38
+ def fail?
39
+ probability = proxy.options[:probability]
40
+ return false if probability.nil? || probability <= 0
41
+
42
+ rand < probability
43
+ end
44
+ end
45
+ end
@@ -2,8 +2,9 @@
2
2
 
3
3
  # Sinatra-based HTTP forward proxy server used as an in-process Nonnative server.
4
4
  #
5
- # The proxy receives inbound HTTP requests and forwards them to an upstream host over HTTPS, returning
6
- # the upstream response status, body, and safe end-to-end response headers. Hop-by-hop,
5
+ # The proxy receives inbound HTTP requests and forwards them to an upstream host, defaulting to HTTPS
6
+ # on the scheme's default port but configurable to HTTP and/or a non-default port. It returns the
7
+ # upstream response status, body, and safe end-to-end response headers. Hop-by-hop,
7
8
  # connection-nominated, proxy-authentication, framing, and deferred response headers are not forwarded.
8
9
  #
9
10
  # This file defines two classes:
@@ -69,10 +70,12 @@ module Nonnative
69
70
  # Builds the upstream URL for the given request.
70
71
  #
71
72
  # @param request [Sinatra::Request] the incoming request
72
- # @param settings [Sinatra::Base] Sinatra settings, expected to include `host`
73
- # @return [String] HTTPS URL for the upstream request
73
+ # @param settings [Sinatra::Base] Sinatra settings, expected to include `host`, `scheme`, and `port`
74
+ # @return [String] upstream URL
74
75
  def build_url(request, settings)
75
- URI::HTTPS.build(host: settings.host, path: request.path_info, query: request.query_string).to_s
76
+ uri_class = settings.scheme == 'http' ? URI::HTTP : URI::HTTPS
77
+
78
+ uri_class.build(host: settings.host, port: settings.port, path: request.path_info, query: request.query_string).to_s
76
79
  end
77
80
 
78
81
  # Executes the upstream request and returns the response.
@@ -193,11 +196,15 @@ module Nonnative
193
196
  #
194
197
  # @see Nonnative::HTTPServer
195
198
  class HTTPProxyServer < Nonnative::HTTPServer
196
- # @param host [String] upstream host to proxy to (HTTPS)
199
+ # @param host [String] upstream host to proxy to
197
200
  # @param service [Nonnative::ConfigurationServer] server configuration
198
- def initialize(host, service)
201
+ # @param scheme [String] upstream scheme, `"http"` or `"https"`
202
+ # @param port [Integer, nil] upstream port; `nil` uses the scheme's default port
203
+ def initialize(host, service, scheme: 'https', port: nil)
199
204
  http_service = Class.new(Nonnative::HTTPProxy) do
200
205
  set :host, host
206
+ set :scheme, scheme
207
+ set :port, port
201
208
  end
202
209
 
203
210
  super(http_service.new, service)
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nonnative
4
+ # Socket-pair variant used by the fault-injection proxy to fragment upstream responses.
5
+ #
6
+ # When active, client requests pass through unchanged, while each response written back to the
7
+ # client is split into `proxy.options[:slice_size]`-byte writes, optionally separated by
8
+ # `proxy.options[:slice_delay]` seconds, so a client that assumes one `recv` yields a whole protocol
9
+ # frame is forced to perform multiple reads and reassemble the message. A missing or non-positive
10
+ # `slice_size` leaves the connection in pass-through mode.
11
+ #
12
+ # This behavior is enabled by calling {Nonnative::FaultInjectionProxy#slicer}.
13
+ #
14
+ # @see Nonnative::FaultInjectionProxy
15
+ # @see Nonnative::SocketPairFactory
16
+ # @see Nonnative::SocketPair
17
+ class SlicerSocketPair < SocketPair
18
+ # Tracks the client socket so we can fragment only the response path.
19
+ #
20
+ # @param local_socket [TCPSocket] the accepted client socket
21
+ # @return [void]
22
+ def connect(local_socket)
23
+ @local_socket = local_socket
24
+
25
+ super
26
+ ensure
27
+ @local_socket = nil
28
+ end
29
+
30
+ # Writes response data to the client in configured slices, leaving requests unchanged.
31
+ #
32
+ # @param socket [IO] the socket to write to
33
+ # @param data [String] the original payload
34
+ # @return [Integer] number of bytes written
35
+ def write(socket, data)
36
+ return super unless socket.equal?(@local_socket)
37
+
38
+ size = proxy.options[:slice_size]
39
+ return super if size.nil? || size <= 0
40
+
41
+ sliced_write(socket, data, size)
42
+ end
43
+
44
+ private
45
+
46
+ # Writes `data` to `socket` in `size`-byte slices, sleeping `proxy.options[:slice_delay]` seconds
47
+ # between slices (when positive) to help defeat TCP write coalescing.
48
+ #
49
+ # @param socket [IO] the socket to write to
50
+ # @param data [String] the original payload
51
+ # @param size [Integer] the slice size in bytes
52
+ # @return [Integer] number of bytes written
53
+ def sliced_write(socket, data, size)
54
+ delay = proxy.options[:slice_delay]
55
+ offset = 0
56
+ written = 0
57
+
58
+ while offset < data.bytesize
59
+ chunk = data.byteslice(offset, size)
60
+ written += socket.write(chunk)
61
+ offset += chunk.bytesize
62
+
63
+ sleep(delay) if delay&.positive? && offset < data.bytesize
64
+ end
65
+
66
+ written
67
+ end
68
+ end
69
+ end
@@ -15,6 +15,8 @@ module Nonnative
15
15
  # - `:invalid_data` -> {Nonnative::InvalidDataSocketPair}
16
16
  # - `:bandwidth` -> {Nonnative::BandwidthSocketPair}
17
17
  # - `:limit_data` -> {Nonnative::LimitDataSocketPair}
18
+ # - `:slicer` -> {Nonnative::SlicerSocketPair}
19
+ # - `:flaky` -> {Nonnative::FlakySocketPair}
18
20
  #
19
21
  # @see Nonnative::FaultInjectionProxy
20
22
  # @see Nonnative::SocketPair
@@ -25,6 +27,8 @@ module Nonnative
25
27
  # @see Nonnative::InvalidDataSocketPair
26
28
  # @see Nonnative::BandwidthSocketPair
27
29
  # @see Nonnative::LimitDataSocketPair
30
+ # @see Nonnative::SlicerSocketPair
31
+ # @see Nonnative::FlakySocketPair
28
32
  class SocketPairFactory
29
33
  PAIR_BY_STATE = {
30
34
  close_all: CloseAllSocketPair,
@@ -33,7 +37,9 @@ module Nonnative
33
37
  timeout: TimeoutSocketPair,
34
38
  invalid_data: InvalidDataSocketPair,
35
39
  bandwidth: BandwidthSocketPair,
36
- limit_data: LimitDataSocketPair
40
+ limit_data: LimitDataSocketPair,
41
+ slicer: SlicerSocketPair,
42
+ flaky: FlakySocketPair
37
43
  }.freeze
38
44
  private_constant :PAIR_BY_STATE
39
45
 
@@ -41,7 +47,7 @@ module Nonnative
41
47
  # Creates a socket-pair instance for the given proxy state.
42
48
  #
43
49
  # @param kind [Symbol] proxy state (e.g. `:none`, `:close_all`, `:reset_peer`, `:delay`, `:timeout`,
44
- # `:invalid_data`, `:bandwidth`, `:limit_data`)
50
+ # `:invalid_data`, `:bandwidth`, `:limit_data`, `:slicer`, `:flaky`)
45
51
  # @param proxy [Nonnative::ConfigurationProxy] proxy configuration (host/port/options)
46
52
  # @return [Nonnative::SocketPair] a socket-pair implementation instance
47
53
  def create(kind, proxy)
@@ -4,5 +4,5 @@ module Nonnative
4
4
  # The current gem version.
5
5
  #
6
6
  # @return [String]
7
- VERSION = '3.30.0'
7
+ VERSION = '3.32.0'
8
8
  end
data/lib/nonnative.rb CHANGED
@@ -117,6 +117,8 @@ require 'nonnative/timeout_socket_pair'
117
117
  require 'nonnative/invalid_data_socket_pair'
118
118
  require 'nonnative/bandwidth_socket_pair'
119
119
  require 'nonnative/limit_data_socket_pair'
120
+ require 'nonnative/slicer_socket_pair'
121
+ require 'nonnative/flaky_socket_pair'
120
122
  require 'nonnative/socket_pair_factory'
121
123
  require 'nonnative/go_executable'
122
124
  require 'nonnative/cucumber'
@@ -349,10 +351,11 @@ module Nonnative
349
351
 
350
352
  # Resets proxies for all currently started runners.
351
353
  #
354
+ # No-op when called before {Nonnative.start}, since no pool exists yet.
355
+ #
352
356
  # @return [void]
353
- # @raise [NoMethodError] if called before {Nonnative.start} (because {Nonnative.pool} is nil)
354
357
  def reset
355
- Nonnative.pool.reset
358
+ Nonnative.pool&.reset
356
359
  end
357
360
 
358
361
  private
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.30.0
4
+ version: 3.32.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alejandro Falkowski
@@ -410,6 +410,7 @@ files:
410
410
  - lib/nonnative/ed25519_key.rb
411
411
  - lib/nonnative/error.rb
412
412
  - lib/nonnative/fault_injection_proxy.rb
413
+ - lib/nonnative/flaky_socket_pair.rb
413
414
  - lib/nonnative/go_executable.rb
414
415
  - lib/nonnative/grpc_health.rb
415
416
  - lib/nonnative/grpc_probe.rb
@@ -437,6 +438,7 @@ files:
437
438
  - lib/nonnative/runner.rb
438
439
  - lib/nonnative/server.rb
439
440
  - lib/nonnative/service.rb
441
+ - lib/nonnative/slicer_socket_pair.rb
440
442
  - lib/nonnative/socket_pair.rb
441
443
  - lib/nonnative/socket_pair_factory.rb
442
444
  - lib/nonnative/ssh_token.rb
@@ -470,7 +472,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
470
472
  - !ruby/object:Gem::Version
471
473
  version: '0'
472
474
  requirements: []
473
- rubygems_version: 4.0.11
475
+ rubygems_version: 4.0.16
474
476
  specification_version: 4
475
477
  summary: Ruby-first end-to-end harness for testing systems implemented in other languages
476
478
  test_files: []