nonnative 3.36.0 → 3.40.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 +17 -6
- data/lib/nonnative/configuration.rb +3 -1
- data/lib/nonnative/configuration_server.rb +2 -1
- data/lib/nonnative/fault_injection_proxy.rb +51 -22
- data/lib/nonnative/grpc_server.rb +5 -2
- data/lib/nonnative/http_client.rb +15 -10
- data/lib/nonnative/http_proxy_server.rb +9 -4
- data/lib/nonnative/http_server.rb +6 -1
- data/lib/nonnative/observability.rb +3 -2
- data/lib/nonnative/pool.rb +8 -4
- data/lib/nonnative/server.rb +40 -13
- data/lib/nonnative/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f3513aa619cd9dab8134d9c4e8d22d333dcd2300f92e271cb8dee10ed7c0de2c
|
|
4
|
+
data.tar.gz: 4aeae3fd49ad6bd1da6b929bfa666c17c3c346a73a56b430ca7214a82ee19698
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d5c02d41c403e663ba1ffe2e8f36e67fe4a2a36910b60a921d0e02c3ac84b1c68be01158ff223c40ee3e6990e429d79faf79eada9e538195a26652523d89813a
|
|
7
|
+
data.tar.gz: 6be1f9067df0fd1c121d0604813d3f91b4e9bbe9cbecfcd10aacd8a96a06a76f27a0a42bb3a08398cb7f0c3e68678a42c0f55b62390f304461814332a1e7e1bf
|
data/README.md
CHANGED
|
@@ -78,8 +78,9 @@ Common runner fields:
|
|
|
78
78
|
Process/server fields:
|
|
79
79
|
- `ports`: client-facing ports. These are also used for readiness/shutdown port checks.
|
|
80
80
|
- `timeout`: max time (seconds) for each readiness/shutdown check. For processes, the same value also
|
|
81
|
-
bounds optional HTTP/gRPC probes and graceful child exit after the stop signal.
|
|
82
|
-
|
|
81
|
+
bounds optional HTTP/gRPC probes and graceful child exit after the stop signal. For servers, it
|
|
82
|
+
also bounds worker-thread cleanup after the stop hook returns. Defaults to `1.0`. A value of `0`
|
|
83
|
+
fails immediately; setting it to `nil` programmatically does the same.
|
|
83
84
|
- `wait`: small sleep (seconds) between lifecycle steps.
|
|
84
85
|
- `log`: per-runner log file used by process output redirection or server implementations.
|
|
85
86
|
|
|
@@ -181,8 +182,9 @@ expect(response.code).to eq(200)
|
|
|
181
182
|
```
|
|
182
183
|
|
|
183
184
|
HTTP error statuses are returned as response objects so callers can inspect `.code` and `.body`.
|
|
184
|
-
|
|
185
|
-
|
|
185
|
+
Broken connections raise their RestClient exceptions, and a request timeout raises either a RestClient
|
|
186
|
+
timeout exception or `Timeout::Error` (the latter when `read_timeout` bounds a Net::HTTP retry of an
|
|
187
|
+
idempotent verb); observability requests are not retried automatically.
|
|
186
188
|
|
|
187
189
|
`Nonnative.grpc_health` is a helper for the standard gRPC health checking protocol:
|
|
188
190
|
|
|
@@ -437,6 +439,13 @@ module Nonnative
|
|
|
437
439
|
end
|
|
438
440
|
```
|
|
439
441
|
|
|
442
|
+
Rollback calls `perform_stop` for every successfully constructed server, even when a later
|
|
443
|
+
constructor fails before any server thread starts. Keep `perform_stop` safe in that state and use it
|
|
444
|
+
to release resources acquired during initialization. After `perform_stop` requests shutdown,
|
|
445
|
+
Nonnative waits up to the server `timeout` for the owned thread to finish. A thread that exceeds the
|
|
446
|
+
timeout is terminated and causes `Nonnative.stop` to raise `Nonnative::StopError`; the server can
|
|
447
|
+
still be started again afterward.
|
|
448
|
+
|
|
440
449
|
Set it up programmatically:
|
|
441
450
|
|
|
442
451
|
```ruby
|
|
@@ -600,8 +609,10 @@ end
|
|
|
600
609
|
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.
|
|
601
610
|
|
|
602
611
|
The upstream scheme defaults to HTTPS on the scheme's default port; pass `scheme:`/`port:` to
|
|
603
|
-
`Nonnative::HTTPProxyServer.new` to target an `http://` upstream or a non-default port. The
|
|
604
|
-
|
|
612
|
+
`Nonnative::HTTPProxyServer.new` to target an `http://` upstream or a non-default port. The whole
|
|
613
|
+
upstream call defaults to a one-second bound, including Net::HTTP's built-in retry of an
|
|
614
|
+
unresponsive idempotent request; pass `upstream_timeout:` to tune it for a slow dependency. The
|
|
615
|
+
proxy forwards the request path and query for `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, and
|
|
605
616
|
`OPTIONS`, while removing proxy credentials, `Host`, `Accept-Encoding`, hop-by-hop headers, and
|
|
606
617
|
headers nominated by `Connection` before the upstream request.
|
|
607
618
|
|
|
@@ -93,6 +93,8 @@ module Nonnative
|
|
|
93
93
|
process = Nonnative::ConfigurationProcess.new
|
|
94
94
|
yield process
|
|
95
95
|
|
|
96
|
+
raise ArgumentError, "Process '#{process.name}' requires 'command' or 'go'" if process.command.nil?
|
|
97
|
+
|
|
96
98
|
processes << process
|
|
97
99
|
end
|
|
98
100
|
|
|
@@ -157,7 +159,7 @@ module Nonnative
|
|
|
157
159
|
tools = go.tools || []
|
|
158
160
|
|
|
159
161
|
-> { Nonnative.go_argv(tools, go.output, go.executable, go.command, *params) }
|
|
160
|
-
|
|
162
|
+
elsif process.command
|
|
161
163
|
-> { process.command }
|
|
162
164
|
end
|
|
163
165
|
end
|
|
@@ -14,7 +14,8 @@ module Nonnative
|
|
|
14
14
|
# @return [Class] a class that implements `#initialize(service)` and the hooks expected by {Nonnative::Server}
|
|
15
15
|
attr_accessor :klass
|
|
16
16
|
|
|
17
|
-
# @return [Numeric]
|
|
17
|
+
# @return [Numeric] lifecycle timeout (seconds) used for port checks and worker-thread cleanup
|
|
18
|
+
# (defaults to `1.0`)
|
|
18
19
|
attr_accessor :timeout
|
|
19
20
|
|
|
20
21
|
# @return [String] log file path used by server implementations (for example Puma/gRPC log files)
|
|
@@ -52,6 +52,8 @@ module Nonnative
|
|
|
52
52
|
# @see Nonnative::Proxy
|
|
53
53
|
# @see Nonnative::SocketPairFactory
|
|
54
54
|
class FaultInjectionProxy < Nonnative::Proxy
|
|
55
|
+
# Used both to flush the accept-queue barrier and as the aggregate deadline for draining active
|
|
56
|
+
# worker threads during stop.
|
|
55
57
|
STOP_DRAIN_TIMEOUT = 1
|
|
56
58
|
|
|
57
59
|
class Connection
|
|
@@ -62,10 +64,9 @@ module Nonnative
|
|
|
62
64
|
@socket = socket
|
|
63
65
|
end
|
|
64
66
|
|
|
65
|
-
def
|
|
67
|
+
def close_sockets
|
|
66
68
|
pair&.close
|
|
67
69
|
socket.close unless socket.closed?
|
|
68
|
-
thread&.terminate
|
|
69
70
|
end
|
|
70
71
|
end
|
|
71
72
|
|
|
@@ -88,6 +89,7 @@ module Nonnative
|
|
|
88
89
|
# @return [void]
|
|
89
90
|
def start
|
|
90
91
|
mutex.synchronize { @stopping = false }
|
|
92
|
+
logger
|
|
91
93
|
@tcp_server = ::TCPServer.new(service.host, service.port)
|
|
92
94
|
@thread = Thread.new { perform_start }
|
|
93
95
|
|
|
@@ -100,7 +102,7 @@ module Nonnative
|
|
|
100
102
|
def stop
|
|
101
103
|
server = tcp_server
|
|
102
104
|
mark_stopping
|
|
103
|
-
close_connections
|
|
105
|
+
drain_workers(close_connections)
|
|
104
106
|
close_queued_connections(server)
|
|
105
107
|
server&.close
|
|
106
108
|
|
|
@@ -113,6 +115,7 @@ module Nonnative
|
|
|
113
115
|
|
|
114
116
|
@tcp_server = nil
|
|
115
117
|
@thread = nil
|
|
118
|
+
close_logger
|
|
116
119
|
|
|
117
120
|
Nonnative.logger.info "stopped with host '#{service.host}' and port '#{service.port}' for proxy 'fault_injection'"
|
|
118
121
|
end
|
|
@@ -230,7 +233,7 @@ module Nonnative
|
|
|
230
233
|
|
|
231
234
|
private
|
|
232
235
|
|
|
233
|
-
attr_reader :tcp_server, :thread, :connections, :mutex, :state, :
|
|
236
|
+
attr_reader :tcp_server, :thread, :connections, :mutex, :state, :stopping
|
|
234
237
|
|
|
235
238
|
def perform_start
|
|
236
239
|
loop do
|
|
@@ -240,10 +243,9 @@ module Nonnative
|
|
|
240
243
|
local_socket.close
|
|
241
244
|
next
|
|
242
245
|
end
|
|
243
|
-
|
|
244
|
-
accept_connection
|
|
246
|
+
Thread.start(local_socket) do |accepted_socket|
|
|
247
|
+
accept_connection(id, accepted_socket) if register_connection_thread(id)
|
|
245
248
|
end
|
|
246
|
-
attach_connection_thread(id, connection_thread)
|
|
247
249
|
end
|
|
248
250
|
rescue IOError, Errno::EBADF
|
|
249
251
|
nil
|
|
@@ -281,8 +283,29 @@ module Nonnative
|
|
|
281
283
|
active_connections.each do |id, connection|
|
|
282
284
|
close_connection(id, connection)
|
|
283
285
|
end
|
|
286
|
+
|
|
287
|
+
active_connections.filter_map { |_id, connection| connection.thread }
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def drain_workers(workers)
|
|
291
|
+
deadline = monotonic_now + STOP_DRAIN_TIMEOUT
|
|
292
|
+
workers.each do |worker|
|
|
293
|
+
worker.join(remaining_time(deadline))
|
|
294
|
+
terminate_worker(worker)
|
|
295
|
+
end
|
|
284
296
|
end
|
|
285
297
|
|
|
298
|
+
def terminate_worker(worker)
|
|
299
|
+
return unless worker.alive?
|
|
300
|
+
|
|
301
|
+
worker.kill
|
|
302
|
+
worker.join
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def remaining_time(deadline) = [deadline - monotonic_now, 0].max
|
|
306
|
+
|
|
307
|
+
def monotonic_now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
308
|
+
|
|
286
309
|
def close_queued_connections(server)
|
|
287
310
|
return unless server
|
|
288
311
|
|
|
@@ -309,9 +332,7 @@ module Nonnative
|
|
|
309
332
|
wait
|
|
310
333
|
end
|
|
311
334
|
|
|
312
|
-
def read_state
|
|
313
|
-
mutex.synchronize { state }
|
|
314
|
-
end
|
|
335
|
+
def read_state = mutex.synchronize { state }
|
|
315
336
|
|
|
316
337
|
def register_connection(id, socket)
|
|
317
338
|
mutex.synchronize do
|
|
@@ -322,26 +343,34 @@ module Nonnative
|
|
|
322
343
|
end
|
|
323
344
|
end
|
|
324
345
|
|
|
325
|
-
def mark_stopping
|
|
326
|
-
mutex.synchronize { @stopping = true }
|
|
327
|
-
end
|
|
346
|
+
def mark_stopping = mutex.synchronize { @stopping = true }
|
|
328
347
|
|
|
329
|
-
def
|
|
330
|
-
mutex.synchronize
|
|
331
|
-
|
|
348
|
+
def register_connection_thread(id)
|
|
349
|
+
mutex.synchronize do
|
|
350
|
+
connection = connections[id]
|
|
351
|
+
return false unless connection && !stopping
|
|
332
352
|
|
|
333
|
-
|
|
334
|
-
|
|
353
|
+
connection.thread = Thread.current
|
|
354
|
+
true
|
|
355
|
+
end
|
|
335
356
|
end
|
|
336
357
|
|
|
337
|
-
def
|
|
338
|
-
|
|
339
|
-
|
|
358
|
+
def attach_connection_pair(id, pair) = mutex.synchronize { connections[id]&.pair = pair }
|
|
359
|
+
|
|
360
|
+
def delete_connection(id) = mutex.synchronize { connections.delete(id) }
|
|
340
361
|
|
|
341
362
|
def close_connection(id, connection)
|
|
342
363
|
Nonnative.logger.info "closing connection for '#{id}' for proxy 'fault_injection'"
|
|
343
364
|
|
|
344
|
-
connection.
|
|
365
|
+
connection.close_sockets
|
|
345
366
|
end
|
|
367
|
+
|
|
368
|
+
def close_logger
|
|
369
|
+
@logger&.close
|
|
370
|
+
ensure
|
|
371
|
+
@logger = nil
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def logger = (@logger ||= Logger.new(service.proxy.log))
|
|
346
375
|
end
|
|
347
376
|
end
|
|
@@ -45,11 +45,14 @@ module Nonnative
|
|
|
45
45
|
server.run
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
# Stops the gRPC server.
|
|
48
|
+
# Stops the gRPC server after it has started.
|
|
49
|
+
#
|
|
50
|
+
# Construction rollback can call this hook before `GRPC::RpcServer#run`; gRPC rejects `stop` in
|
|
51
|
+
# that state and has not acquired its listener yet, so there is nothing to release.
|
|
49
52
|
#
|
|
50
53
|
# @return [void]
|
|
51
54
|
def perform_stop
|
|
52
|
-
server.stop
|
|
55
|
+
server.stop unless server.running_state == :not_started
|
|
53
56
|
end
|
|
54
57
|
|
|
55
58
|
# Waits until the gRPC server reports it is running, or the configured timeout elapses.
|
|
@@ -18,7 +18,8 @@ module Nonnative
|
|
|
18
18
|
@host = host
|
|
19
19
|
@exceptions = [
|
|
20
20
|
RestClient::Exceptions::Timeout,
|
|
21
|
-
RestClient::ServerBrokeConnection
|
|
21
|
+
RestClient::ServerBrokeConnection,
|
|
22
|
+
::Timeout::Error
|
|
22
23
|
]
|
|
23
24
|
end
|
|
24
25
|
|
|
@@ -40,7 +41,7 @@ module Nonnative
|
|
|
40
41
|
# @param opts [Hash] RestClient request options (e.g. `headers`, `read_timeout`, `open_timeout`)
|
|
41
42
|
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
42
43
|
def get(pathname, opts = {})
|
|
43
|
-
with_exception do
|
|
44
|
+
with_exception(opts) do
|
|
44
45
|
resource(pathname, opts).get
|
|
45
46
|
end
|
|
46
47
|
end
|
|
@@ -52,7 +53,7 @@ module Nonnative
|
|
|
52
53
|
# @param opts [Hash] RestClient request options
|
|
53
54
|
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
54
55
|
def post(pathname, payload, opts = {})
|
|
55
|
-
with_exception do
|
|
56
|
+
with_exception(opts) do
|
|
56
57
|
resource(pathname, opts).post(payload)
|
|
57
58
|
end
|
|
58
59
|
end
|
|
@@ -63,7 +64,7 @@ module Nonnative
|
|
|
63
64
|
# @param opts [Hash] RestClient request options
|
|
64
65
|
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
65
66
|
def delete(pathname, opts = {})
|
|
66
|
-
with_exception do
|
|
67
|
+
with_exception(opts) do
|
|
67
68
|
resource(pathname, opts).delete
|
|
68
69
|
end
|
|
69
70
|
end
|
|
@@ -75,7 +76,7 @@ module Nonnative
|
|
|
75
76
|
# @param opts [Hash] RestClient request options
|
|
76
77
|
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
77
78
|
def put(pathname, payload, opts = {})
|
|
78
|
-
with_exception do
|
|
79
|
+
with_exception(opts) do
|
|
79
80
|
resource(pathname, opts).put(payload)
|
|
80
81
|
end
|
|
81
82
|
end
|
|
@@ -87,7 +88,7 @@ module Nonnative
|
|
|
87
88
|
# @param opts [Hash] RestClient request options
|
|
88
89
|
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
89
90
|
def patch(pathname, payload, opts = {})
|
|
90
|
-
with_exception do
|
|
91
|
+
with_exception(opts) do
|
|
91
92
|
resource(pathname, opts).patch(payload)
|
|
92
93
|
end
|
|
93
94
|
end
|
|
@@ -98,7 +99,7 @@ module Nonnative
|
|
|
98
99
|
# @param opts [Hash] RestClient request options (e.g. `headers`, `read_timeout`, `open_timeout`)
|
|
99
100
|
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
100
101
|
def head(pathname, opts = {})
|
|
101
|
-
with_exception do
|
|
102
|
+
with_exception(opts) do
|
|
102
103
|
resource(pathname, opts).head
|
|
103
104
|
end
|
|
104
105
|
end
|
|
@@ -109,7 +110,7 @@ module Nonnative
|
|
|
109
110
|
# @param opts [Hash] RestClient request options (e.g. `headers`, `read_timeout`, `open_timeout`)
|
|
110
111
|
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
111
112
|
def options(pathname, opts = {})
|
|
112
|
-
with_exception do
|
|
113
|
+
with_exception(opts) do
|
|
113
114
|
RestClient::Request.execute(opts.merge(method: :options, url: request_url(pathname)))
|
|
114
115
|
end
|
|
115
116
|
end
|
|
@@ -131,8 +132,12 @@ module Nonnative
|
|
|
131
132
|
URI.join(host, pathname).to_s
|
|
132
133
|
end
|
|
133
134
|
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
# Net::HTTP silently retries idempotent verbs once on a read timeout, so bound the whole call
|
|
136
|
+
# (including any retry) by the wall clock rather than trusting `:read_timeout` alone.
|
|
137
|
+
def with_exception(opts = {}, &)
|
|
138
|
+
timeout = opts[:read_timeout]
|
|
139
|
+
|
|
140
|
+
timeout ? ::Timeout.timeout(timeout, &) : yield
|
|
136
141
|
rescue *exceptions => e
|
|
137
142
|
raise e
|
|
138
143
|
rescue RestClient::Exception => e
|
|
@@ -59,7 +59,8 @@ module Nonnative
|
|
|
59
59
|
].freeze
|
|
60
60
|
|
|
61
61
|
GATEWAY_TIMEOUT_ERRORS = [
|
|
62
|
-
RestClient::Exceptions::Timeout
|
|
62
|
+
RestClient::Exceptions::Timeout,
|
|
63
|
+
::Timeout::Error
|
|
63
64
|
].freeze
|
|
64
65
|
|
|
65
66
|
GATEWAY_CONNECTION_ERRORS = [
|
|
@@ -112,10 +113,12 @@ module Nonnative
|
|
|
112
113
|
# @return [RestClient::Response] response for error statuses, otherwise RestClient return value
|
|
113
114
|
# @raise [Sinatra::Halt] with a gateway status when the upstream is unavailable or times out
|
|
114
115
|
def api_response(method:, url:, headers:, payload: nil)
|
|
115
|
-
options = { method:, url:, headers: }
|
|
116
|
+
options = { method:, url:, headers:, open_timeout: settings.upstream_timeout, read_timeout: settings.upstream_timeout }
|
|
116
117
|
options[:payload] = payload unless payload.nil?
|
|
117
118
|
|
|
118
|
-
|
|
119
|
+
# Net::HTTP retries idempotent verbs once on a read timeout, so bound the whole
|
|
120
|
+
# call (including any retry) by the wall clock rather than trusting read_timeout alone.
|
|
121
|
+
::Timeout.timeout(settings.upstream_timeout) { RestClient::Request.execute(options) }
|
|
119
122
|
rescue *GATEWAY_TIMEOUT_ERRORS
|
|
120
123
|
halt 504
|
|
121
124
|
rescue *GATEWAY_CONNECTION_ERRORS
|
|
@@ -246,11 +249,13 @@ module Nonnative
|
|
|
246
249
|
# @param service [Nonnative::ConfigurationServer] server configuration
|
|
247
250
|
# @param scheme [String] upstream scheme, `"http"` or `"https"`
|
|
248
251
|
# @param port [Integer, nil] upstream port; `nil` uses the scheme's default port
|
|
249
|
-
|
|
252
|
+
# @param upstream_timeout [Numeric] maximum seconds for each upstream operation (defaults to `1.0`)
|
|
253
|
+
def initialize(host, service, scheme: 'https', port: nil, upstream_timeout: ConfigurationRunner::DEFAULT_TIMEOUT)
|
|
250
254
|
http_service = Class.new(Nonnative::HTTPProxy) do
|
|
251
255
|
set :host, host
|
|
252
256
|
set :scheme, scheme
|
|
253
257
|
set :port, port
|
|
258
|
+
set :upstream_timeout, upstream_timeout
|
|
254
259
|
end
|
|
255
260
|
|
|
256
261
|
super(http_service.new, service)
|
|
@@ -72,9 +72,14 @@ module Nonnative
|
|
|
72
72
|
|
|
73
73
|
# Gracefully shuts down the Puma server.
|
|
74
74
|
#
|
|
75
|
+
# Signals shutdown through Puma's own control pipe (`Puma::Server#stop`) rather than calling
|
|
76
|
+
# `graceful_shutdown` directly, so the accept loop wakes up reliably; calling `graceful_shutdown`
|
|
77
|
+
# without that signal leaves the accept loop dependent on noticing a closed socket, which is not
|
|
78
|
+
# guaranteed to happen promptly on every platform.
|
|
79
|
+
#
|
|
75
80
|
# @return [void]
|
|
76
81
|
def perform_stop
|
|
77
|
-
server.
|
|
82
|
+
server.stop(true)
|
|
78
83
|
ensure
|
|
79
84
|
close_log
|
|
80
85
|
end
|
|
@@ -14,8 +14,9 @@ module Nonnative
|
|
|
14
14
|
#
|
|
15
15
|
# Requests are performed using {Nonnative::HTTPClient}, so callers may pass RestClient options
|
|
16
16
|
# such as `headers`, `open_timeout`, and `read_timeout`. HTTP error statuses are returned as response
|
|
17
|
-
# objects;
|
|
18
|
-
#
|
|
17
|
+
# objects; broken connections raise their RestClient exceptions, and a timeout raises either a
|
|
18
|
+
# RestClient timeout exception or `Timeout::Error` (the latter when `read_timeout` bounds a Net::HTTP
|
|
19
|
+
# retry of an idempotent verb). Requests are not retried automatically.
|
|
19
20
|
#
|
|
20
21
|
# @example
|
|
21
22
|
# Nonnative.configure do |config|
|
data/lib/nonnative/pool.rb
CHANGED
|
@@ -46,8 +46,8 @@ module Nonnative
|
|
|
46
46
|
|
|
47
47
|
# Stops all configured runners and collects lifecycle and shutdown errors.
|
|
48
48
|
#
|
|
49
|
-
# Processes and servers are stopped first and checked for shutdown,
|
|
50
|
-
# stopped when configured.
|
|
49
|
+
# Processes and servers are stopped first and checked for shutdown, including bounded server
|
|
50
|
+
# thread cleanup, then service proxies are stopped when configured.
|
|
51
51
|
#
|
|
52
52
|
# @return [Array<String>] lifecycle and shutdown-check errors collected while stopping
|
|
53
53
|
def stop
|
|
@@ -240,7 +240,7 @@ module Nonnative
|
|
|
240
240
|
id, stopped = Array(values).then { |v| [v.first, v.fetch(1, true)] }
|
|
241
241
|
errors = []
|
|
242
242
|
errors << "Stopped #{runner.name} with id #{id}, though did not respond in time for #{port.description}" unless ready
|
|
243
|
-
errors << "Stopped #{runner.name} with id #{id}, though the
|
|
243
|
+
errors << "Stopped #{runner.name} with id #{id}, though the #{exit_subject(runner)} did not exit in time" unless stopped
|
|
244
244
|
errors
|
|
245
245
|
end
|
|
246
246
|
|
|
@@ -248,7 +248,7 @@ module Nonnative
|
|
|
248
248
|
id, stopped = Array(values).then { |v| [v.first, v.fetch(1, true)] }
|
|
249
249
|
errors = []
|
|
250
250
|
errors << "Rollback failed for #{runner.name} with id #{id}, because it did not stop in time for #{port.description}" unless ready
|
|
251
|
-
errors << "Rollback failed for #{runner.name} with id #{id}, because the
|
|
251
|
+
errors << "Rollback failed for #{runner.name} with id #{id}, because the #{exit_subject(runner)} did not exit in time" unless stopped
|
|
252
252
|
errors
|
|
253
253
|
end
|
|
254
254
|
|
|
@@ -265,6 +265,10 @@ module Nonnative
|
|
|
265
265
|
"Started #{runner_name(service)}, though did not respond in time for readiness: #{checks.map(&:endpoint).join(', ')}"
|
|
266
266
|
end
|
|
267
267
|
|
|
268
|
+
def exit_subject(runner)
|
|
269
|
+
runner.is_a?(Nonnative::Server) ? 'server thread' : 'process'
|
|
270
|
+
end
|
|
271
|
+
|
|
268
272
|
def runner_name(type)
|
|
269
273
|
name = type.name
|
|
270
274
|
return "runner '#{name}'" if name
|
data/lib/nonnative/server.rb
CHANGED
|
@@ -12,6 +12,9 @@ module Nonnative
|
|
|
12
12
|
# - {#perform_start} (to bind/listen and begin serving), and
|
|
13
13
|
# - {#perform_stop} (to gracefully shut down).
|
|
14
14
|
#
|
|
15
|
+
# {#stop} calls the stop hook once for every successfully constructed lifecycle, even if startup
|
|
16
|
+
# never created a worker thread. This lets rollback release resources acquired by a constructor.
|
|
17
|
+
#
|
|
15
18
|
# The underlying configuration is a {Nonnative::ConfigurationServer}.
|
|
16
19
|
#
|
|
17
20
|
# @see Nonnative::ConfigurationServer
|
|
@@ -22,17 +25,22 @@ module Nonnative
|
|
|
22
25
|
super
|
|
23
26
|
|
|
24
27
|
@timeout = Nonnative::Timeout.new(service.timeout)
|
|
28
|
+
@cleanup_required = true
|
|
25
29
|
end
|
|
26
30
|
|
|
27
|
-
# Starts the server thread if it is not already
|
|
31
|
+
# Starts the server thread if it is not already running.
|
|
32
|
+
#
|
|
33
|
+
# A thread retained from a stop that exceeded its timeout is treated as not running, so a server
|
|
34
|
+
# can be restarted even if a prior {#stop} never observed that thread's actual termination.
|
|
28
35
|
#
|
|
29
36
|
# @return [Array<(Integer, TrueClass)>]
|
|
30
37
|
# a tuple of:
|
|
31
38
|
# - a stable identifier for this server instance (`object_id`)
|
|
32
39
|
# - `true` (thread creation itself is considered started; readiness is checked separately)
|
|
33
40
|
def start
|
|
34
|
-
unless thread
|
|
41
|
+
unless thread&.alive?
|
|
35
42
|
@error = nil
|
|
43
|
+
@cleanup_required = true
|
|
36
44
|
@thread = Thread.new do
|
|
37
45
|
perform_start
|
|
38
46
|
rescue StandardError => e
|
|
@@ -49,23 +57,27 @@ module Nonnative
|
|
|
49
57
|
[object_id, true]
|
|
50
58
|
end
|
|
51
59
|
|
|
52
|
-
# Stops the server if
|
|
60
|
+
# Stops the server if its current lifecycle still requires cleanup.
|
|
53
61
|
#
|
|
54
|
-
# Calls {#perform_stop}
|
|
62
|
+
# Calls {#perform_stop} even if startup never created a worker thread, then waits up to the
|
|
63
|
+
# configured timeout for any owned thread to finish. A thread that exceeds the timeout is
|
|
64
|
+
# terminated and reported through {Nonnative::Pool}. A later stop retries draining a retained
|
|
65
|
+
# thread without calling {#perform_stop} again.
|
|
55
66
|
#
|
|
56
|
-
# @return [Integer] the server identifier
|
|
67
|
+
# @return [Integer, Array<(Integer, FalseClass)>] the server identifier when cleanup finishes, or
|
|
68
|
+
# the identifier and `false` when the owned thread exceeds the configured timeout
|
|
57
69
|
def stop
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
thread.terminate
|
|
70
|
+
owned_thread = thread
|
|
71
|
+
return object_id unless @cleanup_required || owned_thread
|
|
61
72
|
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
perform_cleanup
|
|
74
|
+
stopped = drain_thread(owned_thread) == :drained
|
|
75
|
+
wait_stop
|
|
76
|
+
@thread = nil unless owned_thread&.alive?
|
|
64
77
|
|
|
65
|
-
|
|
66
|
-
end
|
|
78
|
+
Nonnative.logger.info "stopped server '#{service.name}'" if stopped
|
|
67
79
|
|
|
68
|
-
object_id
|
|
80
|
+
stopped ? object_id : [object_id, false]
|
|
69
81
|
end
|
|
70
82
|
|
|
71
83
|
# Describes how the server thread terminated before becoming ready, for lifecycle diagnostics.
|
|
@@ -85,5 +97,20 @@ module Nonnative
|
|
|
85
97
|
private
|
|
86
98
|
|
|
87
99
|
attr_reader :thread, :timeout, :error
|
|
100
|
+
|
|
101
|
+
def perform_cleanup
|
|
102
|
+
return unless @cleanup_required
|
|
103
|
+
|
|
104
|
+
perform_stop
|
|
105
|
+
@cleanup_required = false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def drain_thread(owned_thread)
|
|
109
|
+
return :drained if owned_thread.nil? || !owned_thread.alive?
|
|
110
|
+
return :drained if timeout.perform { owned_thread.join }
|
|
111
|
+
|
|
112
|
+
owned_thread.terminate
|
|
113
|
+
:timed_out
|
|
114
|
+
end
|
|
88
115
|
end
|
|
89
116
|
end
|
data/lib/nonnative/version.rb
CHANGED