nonnative 3.26.0 → 3.29.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/README.md +3 -0
- data/lib/nonnative/cucumber.rb +1 -0
- data/lib/nonnative/fault_injection_proxy.rb +14 -0
- data/lib/nonnative/http_client.rb +27 -1
- data/lib/nonnative/http_proxy_server.rb +11 -2
- data/lib/nonnative/limit_data_socket_pair.rb +63 -0
- data/lib/nonnative/server.rb +23 -2
- data/lib/nonnative/socket_pair_factory.rb +16 -17
- data/lib/nonnative/version.rb +1 -1
- data/lib/nonnative.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d81874eb81ecd99758c560530adaf6dd701ac6136258ec36b04a074312e45e65
|
|
4
|
+
data.tar.gz: 05e90f242c9cd70a7c544705d2db49c593e7615ff7edfc28e6864154036e758b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 59abcf2ef7f4c1cf0785ab43dbca7af059441f8a65379601adc1204c8860955b462fc7fa8dac8b1bbb93cd7a449296cac36ea18bee692b67baad567b83cea3ad
|
|
7
|
+
data.tar.gz: ac4407b3205ca54a8e2496e9e2ce2594ee8bf8d7fff8f9c0ae5bce929fdf4b707732c54c8b45ccb038e79bb34601cf52823a7ee99ae6c7ae05a8bbdfb60cc05a
|
data/README.md
CHANGED
|
@@ -852,6 +852,7 @@ Clients connect to the service `host`/`port`, while the proxy forwards traffic t
|
|
|
852
852
|
- `timeout` - Accepts the connection and stalls traffic until reset or stop closes the connection, so clients exercise their own read timeout behavior.
|
|
853
853
|
- `invalid_data` - Forwards client requests unchanged, then corrupts upstream responses before they reach the client.
|
|
854
854
|
- `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.
|
|
855
|
+
- `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.
|
|
855
856
|
|
|
856
857
|
###### 🧩 Fault Injection Services
|
|
857
858
|
|
|
@@ -864,6 +865,7 @@ service = Nonnative.pool.service_by_name(name)
|
|
|
864
865
|
service.proxy.close_all # To use close_all.
|
|
865
866
|
service.proxy.reset_peer # To reset (RST) client connections.
|
|
866
867
|
service.proxy.timeout # To stall traffic until reset or stop.
|
|
868
|
+
service.proxy.limit_data # To truncate the upstream byte stream at options.bytes.
|
|
867
869
|
service.proxy.reset # To reset it back to a good state.
|
|
868
870
|
```
|
|
869
871
|
|
|
@@ -873,6 +875,7 @@ Use the Cucumber proxy steps:
|
|
|
873
875
|
Given I set the proxy for service 'service_1' to 'close_all'
|
|
874
876
|
Given I set the proxy for service 'service_1' to 'reset_peer'
|
|
875
877
|
Given I set the proxy for service 'service_1' to 'timeout'
|
|
878
|
+
Given I set the proxy for service 'service_1' to 'limit_data'
|
|
876
879
|
Then I should reset the proxy for service 'service_1'
|
|
877
880
|
```
|
|
878
881
|
|
data/lib/nonnative/cucumber.rb
CHANGED
|
@@ -15,6 +15,7 @@ module Nonnative
|
|
|
15
15
|
# - {#timeout}: accept connections and keep them silent until clients time out
|
|
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
|
+
# - {#limit_data}: forward a configured number of response bytes, then gracefully close
|
|
18
19
|
# - {#reset}: return to healthy pass-through behavior
|
|
19
20
|
#
|
|
20
21
|
# State changes terminate any active connections so new connections observe the new behavior.
|
|
@@ -36,6 +37,8 @@ module Nonnative
|
|
|
36
37
|
# - `delay`: delay duration in seconds used by {#delay}
|
|
37
38
|
# - `jitter`: optional random offset (seconds) added in `-jitter..jitter` to each `delay` (a
|
|
38
39
|
# negative value uses its magnitude), so clients see variable latency instead of a flat value
|
|
40
|
+
# - `bytes`: positive response byte limit used by {#limit_data}; absent or non-positive values
|
|
41
|
+
# use pass-through behavior
|
|
39
42
|
#
|
|
40
43
|
# @see Nonnative::Proxy
|
|
41
44
|
# @see Nonnative::SocketPairFactory
|
|
@@ -150,6 +153,17 @@ module Nonnative
|
|
|
150
153
|
apply_state :bandwidth
|
|
151
154
|
end
|
|
152
155
|
|
|
156
|
+
# Truncates the upstream byte stream after a configured number of bytes.
|
|
157
|
+
#
|
|
158
|
+
# Client requests are forwarded unchanged. The response byte limit is read from
|
|
159
|
+
# `service.proxy.options[:bytes]`; when it is absent or not positive, the connection forwards at
|
|
160
|
+
# full speed without truncation.
|
|
161
|
+
#
|
|
162
|
+
# @return [void]
|
|
163
|
+
def limit_data
|
|
164
|
+
apply_state :limit_data
|
|
165
|
+
end
|
|
166
|
+
|
|
153
167
|
# Resets the proxy back to healthy pass-through behavior.
|
|
154
168
|
#
|
|
155
169
|
# @return [void]
|
|
@@ -92,19 +92,45 @@ module Nonnative
|
|
|
92
92
|
end
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
# Performs a HEAD request.
|
|
96
|
+
#
|
|
97
|
+
# @param pathname [String] path relative to `host`
|
|
98
|
+
# @param opts [Hash] RestClient request options (e.g. `headers`, `read_timeout`, `open_timeout`)
|
|
99
|
+
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
100
|
+
def head(pathname, opts = {})
|
|
101
|
+
with_exception do
|
|
102
|
+
resource(pathname, opts).head
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Performs an OPTIONS request.
|
|
107
|
+
#
|
|
108
|
+
# @param pathname [String] path relative to `host`
|
|
109
|
+
# @param opts [Hash] RestClient request options (e.g. `headers`, `read_timeout`, `open_timeout`)
|
|
110
|
+
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
111
|
+
def options(pathname, opts = {})
|
|
112
|
+
with_exception do
|
|
113
|
+
RestClient::Request.execute(opts.merge(method: :options, url: request_url(pathname)))
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
95
117
|
# Creates a RestClient resource for a relative path.
|
|
96
118
|
#
|
|
97
119
|
# @param pathname [String] path relative to `host`
|
|
98
120
|
# @param opts [Hash] RestClient request options
|
|
99
121
|
# @return [RestClient::Resource]
|
|
100
122
|
def resource(pathname, opts)
|
|
101
|
-
RestClient::Resource.new(
|
|
123
|
+
RestClient::Resource.new(request_url(pathname), opts)
|
|
102
124
|
end
|
|
103
125
|
|
|
104
126
|
private
|
|
105
127
|
|
|
106
128
|
attr_reader :host, :exceptions
|
|
107
129
|
|
|
130
|
+
def request_url(pathname)
|
|
131
|
+
URI.join(host, pathname).to_s
|
|
132
|
+
end
|
|
133
|
+
|
|
108
134
|
def with_exception
|
|
109
135
|
yield
|
|
110
136
|
rescue *exceptions => e
|
|
@@ -22,7 +22,7 @@ module Nonnative
|
|
|
22
22
|
#
|
|
23
23
|
# The upstream host is configured via service settings (see {Nonnative::HTTPProxyServer}).
|
|
24
24
|
#
|
|
25
|
-
# Supported HTTP verbs: GET, POST, PUT, PATCH, DELETE.
|
|
25
|
+
# Supported HTTP verbs: GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS.
|
|
26
26
|
class HTTPProxy < Nonnative::HTTPService
|
|
27
27
|
NON_FORWARDABLE_RESPONSE_HEADERS = %w[
|
|
28
28
|
connection
|
|
@@ -145,7 +145,16 @@ module Nonnative
|
|
|
145
145
|
header.delete_prefix('HTTP_').split('_').map(&:capitalize).join('-')
|
|
146
146
|
end
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
# Registered before `get` so it takes precedence over Sinatra's GET-generated HEAD route,
|
|
149
|
+
# which would otherwise forward HEAD requests upstream as GET.
|
|
150
|
+
head(/.*/) do
|
|
151
|
+
res = api_response(method: :head, url: build_url(request, settings), headers: forward_request_headers(request))
|
|
152
|
+
|
|
153
|
+
headers(forward_response_headers(res))
|
|
154
|
+
status res.code
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
%w[get post put patch delete options].each do |verb|
|
|
149
158
|
send(verb, /.*/) do
|
|
150
159
|
res = api_response(
|
|
151
160
|
method: verb.to_sym,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nonnative
|
|
4
|
+
# Socket-pair variant used by the fault-injection proxy to truncate upstream responses.
|
|
5
|
+
#
|
|
6
|
+
# When active, client requests pass through unchanged, while the first `proxy.options[:bytes]`
|
|
7
|
+
# bytes of the upstream stream are forwarded to the client on each connection. Once the limit is
|
|
8
|
+
# reached, the pair's normal connection lifecycle closes both sockets gracefully. A missing or
|
|
9
|
+
# non-positive limit leaves the connection in pass-through mode.
|
|
10
|
+
#
|
|
11
|
+
# This behavior is enabled by calling {Nonnative::FaultInjectionProxy#limit_data}.
|
|
12
|
+
#
|
|
13
|
+
# @see Nonnative::FaultInjectionProxy
|
|
14
|
+
# @see Nonnative::SocketPairFactory
|
|
15
|
+
# @see Nonnative::SocketPair
|
|
16
|
+
class LimitDataSocketPair < SocketPair
|
|
17
|
+
# Tracks the client socket and response byte budget for this connection.
|
|
18
|
+
#
|
|
19
|
+
# @param local_socket [TCPSocket] the accepted client socket
|
|
20
|
+
# @return [void]
|
|
21
|
+
def connect(local_socket)
|
|
22
|
+
@local_socket = local_socket
|
|
23
|
+
@remaining = proxy.options[:bytes]
|
|
24
|
+
@limit_reached = false
|
|
25
|
+
|
|
26
|
+
super
|
|
27
|
+
ensure
|
|
28
|
+
@local_socket = nil
|
|
29
|
+
@remaining = nil
|
|
30
|
+
@limit_reached = nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
protected
|
|
34
|
+
|
|
35
|
+
# Stops the base forwarding loop after the response budget has been written.
|
|
36
|
+
#
|
|
37
|
+
# @param ready [Array<Array<IO>>] the result from `select`
|
|
38
|
+
# @param source_socket [IO] readable side
|
|
39
|
+
# @param destination_socket [IO] writable side
|
|
40
|
+
# @return [Boolean] whether the forwarding loop should terminate
|
|
41
|
+
def pipe?(ready, source_socket, destination_socket)
|
|
42
|
+
return true if @limit_reached
|
|
43
|
+
|
|
44
|
+
super || @limit_reached
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Limits writes to the client while leaving writes to the upstream unchanged.
|
|
48
|
+
#
|
|
49
|
+
# @param socket [IO] the socket to write to
|
|
50
|
+
# @param data [String] the original payload
|
|
51
|
+
# @return [Integer] number of bytes written
|
|
52
|
+
def write(socket, data)
|
|
53
|
+
return super unless socket.equal?(@local_socket)
|
|
54
|
+
return super if @remaining.nil? || @remaining <= 0
|
|
55
|
+
|
|
56
|
+
data = data.byteslice(0, @remaining)
|
|
57
|
+
@remaining -= data.bytesize
|
|
58
|
+
@limit_reached = @remaining.zero?
|
|
59
|
+
|
|
60
|
+
super
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/nonnative/server.rb
CHANGED
|
@@ -32,7 +32,14 @@ module Nonnative
|
|
|
32
32
|
# - `true` (thread creation itself is considered started; readiness is checked separately)
|
|
33
33
|
def start
|
|
34
34
|
unless thread
|
|
35
|
-
@
|
|
35
|
+
@error = nil
|
|
36
|
+
@thread = Thread.new do
|
|
37
|
+
perform_start
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
@error = e
|
|
40
|
+
raise
|
|
41
|
+
end
|
|
42
|
+
@thread.report_on_exception = true
|
|
36
43
|
|
|
37
44
|
wait_start
|
|
38
45
|
|
|
@@ -61,8 +68,22 @@ module Nonnative
|
|
|
61
68
|
object_id
|
|
62
69
|
end
|
|
63
70
|
|
|
71
|
+
# Describes how the server thread terminated before becoming ready, for lifecycle diagnostics.
|
|
72
|
+
#
|
|
73
|
+
# Returns `nil` while the thread is still alive, so callers can distinguish a dead thread from a
|
|
74
|
+
# live server that merely missed its readiness window.
|
|
75
|
+
#
|
|
76
|
+
# @return [String, nil] termination detail (clean early return or uncaught exception), or `nil`
|
|
77
|
+
def termination
|
|
78
|
+
return if thread.nil? || thread.alive?
|
|
79
|
+
|
|
80
|
+
return "server thread raised #{error.class}: #{error.message}" if error
|
|
81
|
+
|
|
82
|
+
'server thread exited before readiness'
|
|
83
|
+
end
|
|
84
|
+
|
|
64
85
|
private
|
|
65
86
|
|
|
66
|
-
attr_reader :thread, :timeout
|
|
87
|
+
attr_reader :thread, :timeout, :error
|
|
67
88
|
end
|
|
68
89
|
end
|
|
@@ -14,6 +14,7 @@ module Nonnative
|
|
|
14
14
|
# - `:timeout` -> {Nonnative::TimeoutSocketPair}
|
|
15
15
|
# - `:invalid_data` -> {Nonnative::InvalidDataSocketPair}
|
|
16
16
|
# - `:bandwidth` -> {Nonnative::BandwidthSocketPair}
|
|
17
|
+
# - `:limit_data` -> {Nonnative::LimitDataSocketPair}
|
|
17
18
|
#
|
|
18
19
|
# @see Nonnative::FaultInjectionProxy
|
|
19
20
|
# @see Nonnative::SocketPair
|
|
@@ -23,30 +24,28 @@ module Nonnative
|
|
|
23
24
|
# @see Nonnative::TimeoutSocketPair
|
|
24
25
|
# @see Nonnative::InvalidDataSocketPair
|
|
25
26
|
# @see Nonnative::BandwidthSocketPair
|
|
27
|
+
# @see Nonnative::LimitDataSocketPair
|
|
26
28
|
class SocketPairFactory
|
|
29
|
+
PAIR_BY_STATE = {
|
|
30
|
+
close_all: CloseAllSocketPair,
|
|
31
|
+
reset_peer: ResetPeerSocketPair,
|
|
32
|
+
delay: DelaySocketPair,
|
|
33
|
+
timeout: TimeoutSocketPair,
|
|
34
|
+
invalid_data: InvalidDataSocketPair,
|
|
35
|
+
bandwidth: BandwidthSocketPair,
|
|
36
|
+
limit_data: LimitDataSocketPair
|
|
37
|
+
}.freeze
|
|
38
|
+
private_constant :PAIR_BY_STATE
|
|
39
|
+
|
|
27
40
|
class << self
|
|
28
41
|
# Creates a socket-pair instance for the given proxy state.
|
|
29
42
|
#
|
|
30
|
-
# @param kind [Symbol] proxy state (e.g. `:none`, `:close_all`, `:reset_peer`, `:delay`, `:timeout`,
|
|
43
|
+
# @param kind [Symbol] proxy state (e.g. `:none`, `:close_all`, `:reset_peer`, `:delay`, `:timeout`,
|
|
44
|
+
# `:invalid_data`, `:bandwidth`, `:limit_data`)
|
|
31
45
|
# @param proxy [Nonnative::ConfigurationProxy] proxy configuration (host/port/options)
|
|
32
46
|
# @return [Nonnative::SocketPair] a socket-pair implementation instance
|
|
33
47
|
def create(kind, proxy)
|
|
34
|
-
pair =
|
|
35
|
-
when :close_all
|
|
36
|
-
CloseAllSocketPair
|
|
37
|
-
when :reset_peer
|
|
38
|
-
ResetPeerSocketPair
|
|
39
|
-
when :delay
|
|
40
|
-
DelaySocketPair
|
|
41
|
-
when :timeout
|
|
42
|
-
TimeoutSocketPair
|
|
43
|
-
when :invalid_data
|
|
44
|
-
InvalidDataSocketPair
|
|
45
|
-
when :bandwidth
|
|
46
|
-
BandwidthSocketPair
|
|
47
|
-
else
|
|
48
|
-
SocketPair
|
|
49
|
-
end
|
|
48
|
+
pair = PAIR_BY_STATE.fetch(kind, SocketPair)
|
|
50
49
|
|
|
51
50
|
pair.new(proxy)
|
|
52
51
|
end
|
data/lib/nonnative/version.rb
CHANGED
data/lib/nonnative.rb
CHANGED
|
@@ -116,6 +116,7 @@ require 'nonnative/delay_socket_pair'
|
|
|
116
116
|
require 'nonnative/timeout_socket_pair'
|
|
117
117
|
require 'nonnative/invalid_data_socket_pair'
|
|
118
118
|
require 'nonnative/bandwidth_socket_pair'
|
|
119
|
+
require 'nonnative/limit_data_socket_pair'
|
|
119
120
|
require 'nonnative/socket_pair_factory'
|
|
120
121
|
require 'nonnative/go_executable'
|
|
121
122
|
require 'nonnative/cucumber'
|
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.
|
|
4
|
+
version: 3.29.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alejandro Falkowski
|
|
@@ -422,6 +422,7 @@ files:
|
|
|
422
422
|
- lib/nonnative/http_service.rb
|
|
423
423
|
- lib/nonnative/invalid_data_socket_pair.rb
|
|
424
424
|
- lib/nonnative/jwt_token.rb
|
|
425
|
+
- lib/nonnative/limit_data_socket_pair.rb
|
|
425
426
|
- lib/nonnative/no_proxy.rb
|
|
426
427
|
- lib/nonnative/not_found_error.rb
|
|
427
428
|
- lib/nonnative/observability.rb
|