nonnative 3.22.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 +53 -0
- data/lib/nonnative/cucumber.rb +1 -0
- data/lib/nonnative/fault_injection_proxy.rb +14 -0
- data/lib/nonnative/grpc_server.rb +8 -3
- data/lib/nonnative/http_client.rb +39 -1
- data/lib/nonnative/http_proxy_server.rb +72 -11
- data/lib/nonnative/http_server.rb +8 -1
- data/lib/nonnative/limit_data_socket_pair.rb +63 -0
- data/lib/nonnative/pool.rb +51 -28
- data/lib/nonnative/process.rb +40 -1
- data/lib/nonnative/runner.rb +9 -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 +4 -14
- 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
|
@@ -465,6 +465,30 @@ module Nonnative
|
|
|
465
465
|
end
|
|
466
466
|
```
|
|
467
467
|
|
|
468
|
+
To run multiple Rack services on one managed port, pass a non-empty mount map. Each key must start
|
|
469
|
+
with `/`, and the mounted application receives the remaining `PATH_INFO`:
|
|
470
|
+
|
|
471
|
+
```ruby
|
|
472
|
+
module Nonnative
|
|
473
|
+
module Features
|
|
474
|
+
class HealthService < Nonnative::HTTPService
|
|
475
|
+
get '/' do
|
|
476
|
+
'ok'
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
class HTTPServer < Nonnative::HTTPServer
|
|
481
|
+
def initialize(service)
|
|
482
|
+
super({ '/api' => HelloService.new, '/health' => HealthService.new }, service)
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
The existing single-service form remains supported. Nonnative converts a mount map to a
|
|
490
|
+
`Rack::URLMap` and keeps one server lifecycle and port. An empty mount map raises `ArgumentError`.
|
|
491
|
+
|
|
468
492
|
Set it up programmatically:
|
|
469
493
|
|
|
470
494
|
```ruby
|
|
@@ -517,6 +541,8 @@ end
|
|
|
517
541
|
|
|
518
542
|
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.
|
|
519
543
|
|
|
544
|
+
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.
|
|
545
|
+
|
|
520
546
|
Define your server:
|
|
521
547
|
|
|
522
548
|
```ruby
|
|
@@ -603,6 +629,30 @@ module Nonnative
|
|
|
603
629
|
end
|
|
604
630
|
```
|
|
605
631
|
|
|
632
|
+
To serve multiple gRPC services on one managed port, pass a non-empty array of handler classes or
|
|
633
|
+
instances:
|
|
634
|
+
|
|
635
|
+
```ruby
|
|
636
|
+
module Nonnative
|
|
637
|
+
module Features
|
|
638
|
+
class HealthService < Grpc::Health::V1::Health::Service
|
|
639
|
+
def check(_request, _call)
|
|
640
|
+
Grpc::Health::V1::HealthCheckResponse.new(status: :SERVING)
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
class GRPCServer < Nonnative::GRPCServer
|
|
645
|
+
def initialize(service)
|
|
646
|
+
super([Greeter.new, HealthService.new], service)
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
end
|
|
650
|
+
end
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
The existing single-handler form remains supported. Nonnative registers each handler before the
|
|
654
|
+
server starts, so application and standard health handlers can share one lifecycle and endpoint.
|
|
655
|
+
|
|
606
656
|
Set it up programmatically:
|
|
607
657
|
|
|
608
658
|
```ruby
|
|
@@ -802,6 +852,7 @@ Clients connect to the service `host`/`port`, while the proxy forwards traffic t
|
|
|
802
852
|
- `timeout` - Accepts the connection and stalls traffic until reset or stop closes the connection, so clients exercise their own read timeout behavior.
|
|
803
853
|
- `invalid_data` - Forwards client requests unchanged, then corrupts upstream responses before they reach the client.
|
|
804
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.
|
|
805
856
|
|
|
806
857
|
###### 🧩 Fault Injection Services
|
|
807
858
|
|
|
@@ -814,6 +865,7 @@ service = Nonnative.pool.service_by_name(name)
|
|
|
814
865
|
service.proxy.close_all # To use close_all.
|
|
815
866
|
service.proxy.reset_peer # To reset (RST) client connections.
|
|
816
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.
|
|
817
869
|
service.proxy.reset # To reset it back to a good state.
|
|
818
870
|
```
|
|
819
871
|
|
|
@@ -823,6 +875,7 @@ Use the Cucumber proxy steps:
|
|
|
823
875
|
Given I set the proxy for service 'service_1' to 'close_all'
|
|
824
876
|
Given I set the proxy for service 'service_1' to 'reset_peer'
|
|
825
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'
|
|
826
879
|
Then I should reset the proxy for service 'service_1'
|
|
827
880
|
```
|
|
828
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]
|
|
@@ -12,13 +12,18 @@ module Nonnative
|
|
|
12
12
|
#
|
|
13
13
|
# @see Nonnative::Server
|
|
14
14
|
class GRPCServer < Nonnative::Server
|
|
15
|
-
# Creates a gRPC server and registers the provided service handler.
|
|
15
|
+
# Creates a gRPC server and registers the provided service handler(s).
|
|
16
16
|
#
|
|
17
|
-
# @param svc [Object] a gRPC service implementation
|
|
17
|
+
# @param svc [Object, Array<Object>] a gRPC service implementation or a non-empty array of
|
|
18
|
+
# implementations (typically `...::Service` subclasses or instances)
|
|
18
19
|
# @param service [Nonnative::ConfigurationServer] server configuration
|
|
20
|
+
# @raise [ArgumentError] if `svc` is an empty array
|
|
19
21
|
def initialize(svc, service)
|
|
20
22
|
@server = GRPC::RpcServer.new
|
|
21
|
-
|
|
23
|
+
handlers = svc.is_a?(Array) ? svc : [svc]
|
|
24
|
+
raise ArgumentError, 'gRPC server requires at least one service handler' if handlers.empty?
|
|
25
|
+
|
|
26
|
+
handlers.each { |handler| server.handle(handler) }
|
|
22
27
|
|
|
23
28
|
# Unfortunately gRPC has only one logger so the first server wins.
|
|
24
29
|
GRPC.define_singleton_method(:logger) do
|
|
@@ -80,19 +80,57 @@ module Nonnative
|
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
# Performs a PATCH request.
|
|
84
|
+
#
|
|
85
|
+
# @param pathname [String] path relative to `host`
|
|
86
|
+
# @param payload [Object] request payload
|
|
87
|
+
# @param opts [Hash] RestClient request options
|
|
88
|
+
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
89
|
+
def patch(pathname, payload, opts = {})
|
|
90
|
+
with_exception do
|
|
91
|
+
resource(pathname, opts).patch(payload)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
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
|
+
|
|
83
117
|
# Creates a RestClient resource for a relative path.
|
|
84
118
|
#
|
|
85
119
|
# @param pathname [String] path relative to `host`
|
|
86
120
|
# @param opts [Hash] RestClient request options
|
|
87
121
|
# @return [RestClient::Resource]
|
|
88
122
|
def resource(pathname, opts)
|
|
89
|
-
RestClient::Resource.new(
|
|
123
|
+
RestClient::Resource.new(request_url(pathname), opts)
|
|
90
124
|
end
|
|
91
125
|
|
|
92
126
|
private
|
|
93
127
|
|
|
94
128
|
attr_reader :host, :exceptions
|
|
95
129
|
|
|
130
|
+
def request_url(pathname)
|
|
131
|
+
URI.join(host, pathname).to_s
|
|
132
|
+
end
|
|
133
|
+
|
|
96
134
|
def with_exception
|
|
97
135
|
yield
|
|
98
136
|
rescue *exceptions => e
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
# Sinatra-based HTTP forward proxy server used as an in-process Nonnative server.
|
|
4
4
|
#
|
|
5
5
|
# The proxy receives inbound HTTP requests and forwards them to an upstream host over HTTPS, returning
|
|
6
|
-
# the upstream response status and
|
|
6
|
+
# the upstream response status, body, and safe end-to-end response headers. Hop-by-hop,
|
|
7
|
+
# connection-nominated, proxy-authentication, framing, and deferred response headers are not forwarded.
|
|
7
8
|
#
|
|
8
9
|
# This file defines two classes:
|
|
9
10
|
#
|
|
@@ -21,9 +22,27 @@ module Nonnative
|
|
|
21
22
|
#
|
|
22
23
|
# The upstream host is configured via service settings (see {Nonnative::HTTPProxyServer}).
|
|
23
24
|
#
|
|
24
|
-
# Supported HTTP verbs: GET, POST, PUT, PATCH, DELETE.
|
|
25
|
+
# Supported HTTP verbs: GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS.
|
|
25
26
|
class HTTPProxy < Nonnative::HTTPService
|
|
26
|
-
|
|
27
|
+
NON_FORWARDABLE_RESPONSE_HEADERS = %w[
|
|
28
|
+
connection
|
|
29
|
+
content-encoding
|
|
30
|
+
content-length
|
|
31
|
+
keep-alive
|
|
32
|
+
location
|
|
33
|
+
proxy-authenticate
|
|
34
|
+
proxy-authorization
|
|
35
|
+
proxy-authentication-info
|
|
36
|
+
proxy-connection
|
|
37
|
+
set-cookie
|
|
38
|
+
status
|
|
39
|
+
te
|
|
40
|
+
trailer
|
|
41
|
+
transfer-encoding
|
|
42
|
+
upgrade
|
|
43
|
+
].freeze
|
|
44
|
+
|
|
45
|
+
NON_FORWARDABLE_REQUEST_HEADERS = %w[
|
|
27
46
|
Host
|
|
28
47
|
Accept-Encoding
|
|
29
48
|
Version
|
|
@@ -37,14 +56,14 @@ module Nonnative
|
|
|
37
56
|
#
|
|
38
57
|
# @param request [Sinatra::Request] the incoming request
|
|
39
58
|
# @return [Hash{String=>String}] headers to forward to the upstream
|
|
40
|
-
def
|
|
59
|
+
def forward_request_headers(request)
|
|
41
60
|
headers = request.env.each_with_object({}) do |(header, value), result|
|
|
42
|
-
next unless
|
|
61
|
+
next unless forward_request_header?(header)
|
|
43
62
|
|
|
44
|
-
result[
|
|
63
|
+
result[normalized_request_header_name(header)] = value
|
|
45
64
|
end
|
|
46
65
|
|
|
47
|
-
headers.except(*
|
|
66
|
+
headers.except(*NON_FORWARDABLE_REQUEST_HEADERS)
|
|
48
67
|
end
|
|
49
68
|
|
|
50
69
|
# Builds the upstream URL for the given request.
|
|
@@ -86,23 +105,65 @@ module Nonnative
|
|
|
86
105
|
|
|
87
106
|
private
|
|
88
107
|
|
|
89
|
-
def
|
|
108
|
+
def forward_response_headers(response)
|
|
109
|
+
raw_headers = response.raw_headers
|
|
110
|
+
connection_headers = response_connection_headers(raw_headers)
|
|
111
|
+
|
|
112
|
+
raw_headers.each_with_object({}) do |(header, value), result|
|
|
113
|
+
normalized_header = normalized_response_header_name(header)
|
|
114
|
+
next unless forward_response_header?(normalized_header, connection_headers)
|
|
115
|
+
|
|
116
|
+
result[normalized_header] = Array(value).join(', ')
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def response_connection_headers(raw_headers)
|
|
121
|
+
connection_headers = raw_headers.each_with_object([]) do |(header, values), result|
|
|
122
|
+
next unless normalized_response_header_name(header) == 'connection'
|
|
123
|
+
|
|
124
|
+
Array(values).each { |value| result.concat(value.to_s.split(',')) }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
connection_headers.map { |header| normalized_response_header_name(header) }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def forward_response_header?(header, connection_headers)
|
|
131
|
+
!NON_FORWARDABLE_RESPONSE_HEADERS.include?(header) &&
|
|
132
|
+
!connection_headers.include?(header) &&
|
|
133
|
+
!header.start_with?('rack.')
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def normalized_response_header_name(header)
|
|
137
|
+
header.to_s.strip.downcase
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def forward_request_header?(header)
|
|
90
141
|
header.start_with?('HTTP_') || %w[CONTENT_TYPE CONTENT_LENGTH].include?(header)
|
|
91
142
|
end
|
|
92
143
|
|
|
93
|
-
def
|
|
144
|
+
def normalized_request_header_name(header)
|
|
94
145
|
header.delete_prefix('HTTP_').split('_').map(&:capitalize).join('-')
|
|
95
146
|
end
|
|
96
147
|
|
|
97
|
-
|
|
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|
|
|
98
158
|
send(verb, /.*/) do
|
|
99
159
|
res = api_response(
|
|
100
160
|
method: verb.to_sym,
|
|
101
161
|
url: build_url(request, settings),
|
|
102
|
-
headers:
|
|
162
|
+
headers: forward_request_headers(request),
|
|
103
163
|
payload: retrieve_payload(request, verb)
|
|
104
164
|
)
|
|
105
165
|
|
|
166
|
+
headers(forward_response_headers(res))
|
|
106
167
|
status res.code
|
|
107
168
|
res.body
|
|
108
169
|
end
|
|
@@ -36,9 +36,16 @@ module Nonnative
|
|
|
36
36
|
class HTTPServer < Nonnative::Server
|
|
37
37
|
# Creates a Puma server for the given HTTP service and runner configuration.
|
|
38
38
|
#
|
|
39
|
-
# @param http_service [#call] an HTTP service instance
|
|
39
|
+
# @param http_service [#call, Hash<String, #call>] an HTTP service instance or mount map
|
|
40
40
|
# @param service [Nonnative::ConfigurationServer] server configuration
|
|
41
|
+
# @raise [ArgumentError] if `http_service` is an empty mount map
|
|
41
42
|
def initialize(http_service, service)
|
|
43
|
+
if http_service.is_a?(Hash)
|
|
44
|
+
raise ArgumentError, 'HTTP server requires at least one service mount' if http_service.empty?
|
|
45
|
+
|
|
46
|
+
http_service = Rack::URLMap.new(http_service)
|
|
47
|
+
end
|
|
48
|
+
|
|
42
49
|
# Keep the log IO so the server lifecycle can release Puma's file handle on stop.
|
|
43
50
|
@log = File.open(service.log, 'a')
|
|
44
51
|
options = {
|
|
@@ -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/pool.rb
CHANGED
|
@@ -23,17 +23,14 @@ module Nonnative
|
|
|
23
23
|
@processes = nil
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
# Starts all configured runners and
|
|
26
|
+
# Starts all configured runners and collects lifecycle and readiness errors.
|
|
27
27
|
#
|
|
28
28
|
# Services are started first (proxy-only) and checked for opt-in readiness, then servers and processes are
|
|
29
|
-
# started and checked for readiness.
|
|
29
|
+
# started and checked for readiness. Each readiness failure is described with the runner and, for a process
|
|
30
|
+
# that exited early, its termination detail.
|
|
30
31
|
#
|
|
31
|
-
# @yieldparam name [String, nil] runner name
|
|
32
|
-
# @yieldparam values [Object] runner-specific return value from `start` (e.g. `[pid, running]` for processes)
|
|
33
|
-
# @yieldparam result [Boolean] result of the port readiness check (`true` if ready in time)
|
|
34
|
-
# @yieldparam port [Nonnative::Ports] checked port group
|
|
35
32
|
# @return [Array<String>] lifecycle and readiness-check errors collected while starting
|
|
36
|
-
def start
|
|
33
|
+
def start
|
|
37
34
|
errors = []
|
|
38
35
|
|
|
39
36
|
errors.concat(service_lifecycle(services, :start, :start))
|
|
@@ -41,24 +38,20 @@ module Nonnative
|
|
|
41
38
|
errors.concat(service_readiness_errors)
|
|
42
39
|
return errors if service_readiness_errors.any?
|
|
43
40
|
|
|
44
|
-
[servers, processes].each { |runners| errors.concat(run_lifecycle_checks(runners, :start, :open?, :start
|
|
41
|
+
[servers, processes].each { |runners| errors.concat(run_lifecycle_checks(runners, :start, :open?, :start)) }
|
|
45
42
|
|
|
46
43
|
errors
|
|
47
44
|
end
|
|
48
45
|
|
|
49
|
-
# Stops all configured runners and
|
|
46
|
+
# Stops all configured runners and collects lifecycle and shutdown errors.
|
|
50
47
|
#
|
|
51
48
|
# Processes and servers are stopped first and checked for shutdown, then services are stopped (proxy-only).
|
|
52
49
|
#
|
|
53
|
-
# @yieldparam name [String, nil] runner name
|
|
54
|
-
# @yieldparam id [Object] runner-specific identifier returned by `stop` (e.g. pid or object_id)
|
|
55
|
-
# @yieldparam result [Boolean] result of the port shutdown check (`true` if closed in time)
|
|
56
|
-
# @yieldparam port [Nonnative::Ports] checked port group
|
|
57
50
|
# @return [Array<String>] lifecycle and shutdown-check errors collected while stopping
|
|
58
|
-
def stop
|
|
51
|
+
def stop
|
|
59
52
|
errors = []
|
|
60
53
|
|
|
61
|
-
[processes, servers].each { |runners| errors.concat(run_lifecycle_checks(runners, :stop, :closed?, :stop
|
|
54
|
+
[processes, servers].each { |runners| errors.concat(run_lifecycle_checks(runners, :stop, :closed?, :stop)) }
|
|
62
55
|
errors.concat(service_lifecycle(services, :stop, :stop))
|
|
63
56
|
|
|
64
57
|
errors
|
|
@@ -69,16 +62,12 @@ module Nonnative
|
|
|
69
62
|
# This is used to rollback partial startup after a failed {#start} without constructing new runner
|
|
70
63
|
# wrappers as a side effect.
|
|
71
64
|
#
|
|
72
|
-
# @yieldparam name [String, nil] runner name
|
|
73
|
-
# @yieldparam id [Object] runner-specific identifier returned by `stop`
|
|
74
|
-
# @yieldparam result [Boolean] result of the port shutdown check (`true` if closed in time)
|
|
75
|
-
# @yieldparam port [Nonnative::Ports] checked port group
|
|
76
65
|
# @return [Array<String>] lifecycle and shutdown-check errors collected while rolling back
|
|
77
|
-
def rollback
|
|
66
|
+
def rollback
|
|
78
67
|
errors = []
|
|
79
68
|
|
|
80
69
|
[existing_processes, existing_servers].each do |runners|
|
|
81
|
-
errors.concat(run_lifecycle_checks(runners, :stop, :closed?, :
|
|
70
|
+
errors.concat(run_lifecycle_checks(runners, :stop, :closed?, :rollback))
|
|
82
71
|
end
|
|
83
72
|
errors.concat(service_lifecycle(existing_services, :stop, :stop))
|
|
84
73
|
|
|
@@ -196,7 +185,8 @@ module Nonnative
|
|
|
196
185
|
end
|
|
197
186
|
end
|
|
198
187
|
|
|
199
|
-
def run_lifecycle_checks(runners, lifecycle_method, port_method,
|
|
188
|
+
def run_lifecycle_checks(runners, lifecycle_method, port_method, phase)
|
|
189
|
+
action = phase == :start ? :start : :stop
|
|
200
190
|
checks = []
|
|
201
191
|
errors = []
|
|
202
192
|
|
|
@@ -207,7 +197,7 @@ module Nonnative
|
|
|
207
197
|
errors << lifecycle_error(action, runner, e)
|
|
208
198
|
end
|
|
209
199
|
|
|
210
|
-
errors.concat(
|
|
200
|
+
errors.concat(lifecycle_results(checks, phase, action))
|
|
211
201
|
end
|
|
212
202
|
|
|
213
203
|
def check_port(port, port_method)
|
|
@@ -216,17 +206,50 @@ module Nonnative
|
|
|
216
206
|
{ error: e }
|
|
217
207
|
end
|
|
218
208
|
|
|
219
|
-
def
|
|
220
|
-
checks.each_with_object([]) do |(
|
|
209
|
+
def lifecycle_results(checks, phase, action)
|
|
210
|
+
checks.each_with_object([]) do |(runner, values, port, thread), errors|
|
|
221
211
|
result = thread.value
|
|
222
212
|
if result[:error]
|
|
223
|
-
errors << port_error(action,
|
|
224
|
-
|
|
225
|
-
|
|
213
|
+
errors << port_error(action, runner, result[:error])
|
|
214
|
+
else
|
|
215
|
+
errors.concat(readiness_errors(phase, runner, values, result[:result], port))
|
|
226
216
|
end
|
|
227
217
|
end
|
|
228
218
|
end
|
|
229
219
|
|
|
220
|
+
def readiness_errors(phase, runner, values, ready, port)
|
|
221
|
+
case phase
|
|
222
|
+
when :start then start_errors(runner, values, ready, port)
|
|
223
|
+
when :stop then stop_errors(runner, values, ready, port)
|
|
224
|
+
else rollback_errors(runner, values, ready, port)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def start_errors(runner, values, ready, port)
|
|
229
|
+
id, started = values
|
|
230
|
+
return [] if started && ready
|
|
231
|
+
|
|
232
|
+
message = "Started #{runner.name} with id #{id}, though did not respond in time for #{port.description}"
|
|
233
|
+
detail = runner.termination
|
|
234
|
+
[detail ? "#{message}; #{detail}" : message]
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def stop_errors(runner, values, ready, port)
|
|
238
|
+
id, stopped = Array(values).then { |v| [v.first, v.fetch(1, true)] }
|
|
239
|
+
errors = []
|
|
240
|
+
errors << "Stopped #{runner.name} with id #{id}, though did not respond in time for #{port.description}" unless ready
|
|
241
|
+
errors << "Stopped #{runner.name} with id #{id}, though the process did not exit in time" unless stopped
|
|
242
|
+
errors
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def rollback_errors(runner, values, ready, port)
|
|
246
|
+
id, stopped = Array(values).then { |v| [v.first, v.fetch(1, true)] }
|
|
247
|
+
errors = []
|
|
248
|
+
errors << "Rollback failed for #{runner.name} with id #{id}, because it did not stop in time for #{port.description}" unless ready
|
|
249
|
+
errors << "Rollback failed for #{runner.name} with id #{id}, because the process did not exit in time" unless stopped
|
|
250
|
+
errors
|
|
251
|
+
end
|
|
252
|
+
|
|
230
253
|
def lifecycle_error(action, type, error)
|
|
231
254
|
"#{action.to_s.capitalize} failed for #{runner_name(type)}: #{error.class} - #{error.message}"
|
|
232
255
|
end
|
data/lib/nonnative/process.rb
CHANGED
|
@@ -37,7 +37,10 @@ module Nonnative
|
|
|
37
37
|
wait_start
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
# Retain the reaped status for this lifecycle so startup diagnostics can report it.
|
|
41
|
+
status = ::Process.waitpid2(pid, ::Process::WNOHANG)
|
|
42
|
+
@status = status&.last
|
|
43
|
+
[pid, status.nil?]
|
|
41
44
|
end
|
|
42
45
|
|
|
43
46
|
# Stops the process if it is running.
|
|
@@ -62,6 +65,20 @@ module Nonnative
|
|
|
62
65
|
@memory = nil
|
|
63
66
|
end
|
|
64
67
|
|
|
68
|
+
# Describes how the process terminated when it exited before becoming ready.
|
|
69
|
+
#
|
|
70
|
+
# Returns `nil` while the process is still running, so callers can distinguish an early exit
|
|
71
|
+
# from a live process that merely missed its readiness window. The check is non-blocking and
|
|
72
|
+
# reuses the status captured during {#start} when available.
|
|
73
|
+
#
|
|
74
|
+
# @return [String, nil] termination detail (exit status or terminating signal), or `nil`
|
|
75
|
+
def termination
|
|
76
|
+
status = captured_status
|
|
77
|
+
return if status.nil?
|
|
78
|
+
|
|
79
|
+
terminated_description(status)
|
|
80
|
+
end
|
|
81
|
+
|
|
65
82
|
protected
|
|
66
83
|
|
|
67
84
|
def wait_stop
|
|
@@ -74,6 +91,28 @@ module Nonnative
|
|
|
74
91
|
|
|
75
92
|
attr_reader :pid, :timeout
|
|
76
93
|
|
|
94
|
+
def captured_status
|
|
95
|
+
return @status unless @status.nil?
|
|
96
|
+
return if pid.nil?
|
|
97
|
+
|
|
98
|
+
# A process that exited during the readiness window has not been reaped yet.
|
|
99
|
+
@status = ::Process.waitpid2(pid, ::Process::WNOHANG)&.last
|
|
100
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def terminated_description(status)
|
|
105
|
+
return "process exited before readiness with exit status #{status.exitstatus}" if status.exited?
|
|
106
|
+
return "process exited before readiness after being killed by signal #{signal_description(status.termsig)}" if status.signaled?
|
|
107
|
+
|
|
108
|
+
"process exited before readiness (#{status})"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def signal_description(signal)
|
|
112
|
+
name = Signal.signame(signal)
|
|
113
|
+
name ? "SIG#{name} (#{signal})" : signal.to_s
|
|
114
|
+
end
|
|
115
|
+
|
|
77
116
|
def process_kill
|
|
78
117
|
signal = Signal.list[service.signal || 'INT'] || Signal.list['INT']
|
|
79
118
|
::Process.kill(signal, pid)
|
data/lib/nonnative/runner.rb
CHANGED
|
@@ -26,6 +26,15 @@ module Nonnative
|
|
|
26
26
|
service.name
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# Describes how the runner terminated before becoming ready, for lifecycle diagnostics.
|
|
30
|
+
#
|
|
31
|
+
# Base runners report nothing; {Nonnative::Process} overrides this to describe an early exit.
|
|
32
|
+
#
|
|
33
|
+
# @return [String, nil]
|
|
34
|
+
def termination
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
29
38
|
protected
|
|
30
39
|
|
|
31
40
|
# Returns the underlying configuration object.
|
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'
|
|
@@ -271,10 +272,7 @@ module Nonnative
|
|
|
271
272
|
def start
|
|
272
273
|
@pool ||= Nonnative::Pool.new(configuration)
|
|
273
274
|
errors = []
|
|
274
|
-
errors.concat(@pool.start
|
|
275
|
-
id, started = values
|
|
276
|
-
errors << "Started #{name} with id #{id}, though did not respond in time for #{ports.description}" if !started || !result
|
|
277
|
-
end)
|
|
275
|
+
errors.concat(@pool.start)
|
|
278
276
|
nil
|
|
279
277
|
rescue StandardError => e
|
|
280
278
|
errors << unexpected_lifecycle_error(:start, e)
|
|
@@ -294,11 +292,7 @@ module Nonnative
|
|
|
294
292
|
errors = []
|
|
295
293
|
return if @pool.nil?
|
|
296
294
|
|
|
297
|
-
errors.concat(@pool.stop
|
|
298
|
-
id, stopped = Array(values).then { |v| [v.first, v.fetch(1, true)] }
|
|
299
|
-
errors << "Stopped #{name} with id #{id}, though did not respond in time for #{ports.description}" unless result
|
|
300
|
-
errors << "Stopped #{name} with id #{id}, though the process did not exit in time" unless stopped
|
|
301
|
-
end)
|
|
295
|
+
errors.concat(@pool.stop)
|
|
302
296
|
nil
|
|
303
297
|
rescue StandardError => e
|
|
304
298
|
errors << unexpected_lifecycle_error(:stop, e)
|
|
@@ -363,11 +357,7 @@ module Nonnative
|
|
|
363
357
|
errors = []
|
|
364
358
|
return errors if @pool.nil?
|
|
365
359
|
|
|
366
|
-
errors.concat(@pool.rollback
|
|
367
|
-
id, stopped = Array(values).then { |v| [v.first, v.fetch(1, true)] }
|
|
368
|
-
errors << "Rollback failed for #{name} with id #{id}, because it did not stop in time for #{ports.description}" unless result
|
|
369
|
-
errors << "Rollback failed for #{name} with id #{id}, because the process did not exit in time" unless stopped
|
|
370
|
-
end)
|
|
360
|
+
errors.concat(@pool.rollback)
|
|
371
361
|
rescue StandardError => e
|
|
372
362
|
errors << unexpected_lifecycle_error(:rollback, e)
|
|
373
363
|
ensure
|
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
|