nonnative 3.36.0 → 3.38.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: 6969c5c21e416e67752d3421af108506fe44730230537c9b426529e28dad7934
4
- data.tar.gz: 148ab5d39ef4f367501cca5786de14bc50be175616048a19a792671eee27fc1a
3
+ metadata.gz: 1d53bdf381bf4cb897538cc4520e499b0116e8fc31566930ae1612859dda4d6a
4
+ data.tar.gz: 809f8545b4fe5e6d49289840572247148c38f2a0cd3647919b5b77f473b88b5e
5
5
  SHA512:
6
- metadata.gz: 85218dfe59a108dad82130912aaaa1bb0a79e60017595fd089383aafc005e4b9c9e72c74341cd38efc97ac2f31dbf460967cb2dc4eb2fcb34c565ad663b12e41
7
- data.tar.gz: 8125a4ef344c00e450f8b03df400cbe2df991f8e2905420673fa2fbd09032c1623f52b540c92cf7ab08c60967b7dfb1d9239c68fcf737341e97c21e1feffa10a
6
+ metadata.gz: 34adbd873e0eedd1b994aff7afe5ebc33834ab3f820dcbd2b551d619c851b083ae897828c88ad6c5b185b3d174390364aca1fdd9be85440fc2a5175e574eb686
7
+ data.tar.gz: 06ea5ded95fd98a9ba589cf5b5a8a8d567e07e295fbe72445332997ccb21b1649aa8edb0fe21054a17b91c527e023aa4e44c0724ce66ca0e43c4f39c3f7c4a69
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. Defaults to `1.0`.
82
- A value of `0` fails immediately; setting it to `nil` programmatically does the same.
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
 
@@ -437,6 +438,13 @@ module Nonnative
437
438
  end
438
439
  ```
439
440
 
441
+ Rollback calls `perform_stop` for every successfully constructed server, even when a later
442
+ constructor fails before any server thread starts. Keep `perform_stop` safe in that state and use it
443
+ to release resources acquired during initialization. After `perform_stop` requests shutdown,
444
+ Nonnative waits up to the server `timeout` for the owned thread to finish. A thread that exceeds the
445
+ timeout is terminated and causes `Nonnative.stop` to raise `Nonnative::StopError`; the server can
446
+ still be started again afterward.
447
+
440
448
  Set it up programmatically:
441
449
 
442
450
  ```ruby
@@ -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] readiness timeout (seconds) used when waiting for ports to open/close (defaults to `1.0`)
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 close
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, :logger, :stopping
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
- connection_thread = Thread.start(local_socket) do |accepted_socket|
244
- accept_connection id, accepted_socket
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 attach_connection_thread(id, thread)
330
- mutex.synchronize { connections[id]&.thread = thread }
331
- end
348
+ def register_connection_thread(id)
349
+ mutex.synchronize do
350
+ connection = connections[id]
351
+ return false unless connection && !stopping
332
352
 
333
- def attach_connection_pair(id, pair)
334
- mutex.synchronize { connections[id]&.pair = pair }
353
+ connection.thread = Thread.current
354
+ true
355
+ end
335
356
  end
336
357
 
337
- def delete_connection(id)
338
- mutex.synchronize { connections.delete(id) }
339
- end
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.close
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.
@@ -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.graceful_shutdown
82
+ server.stop(true)
78
83
  ensure
79
84
  close_log
80
85
  end
@@ -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, then service proxies are
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 process did not exit in time" unless stopped
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 process did not exit in time" unless stopped
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
@@ -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 started.
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 it is running.
60
+ # Stops the server if its current lifecycle still requires cleanup.
53
61
  #
54
- # Calls {#perform_stop}, terminates the server thread, and waits briefly.
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 (`object_id`)
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
- if thread
59
- perform_stop
60
- thread.terminate
70
+ owned_thread = thread
71
+ return object_id unless @cleanup_required || owned_thread
61
72
 
62
- @thread = nil
63
- wait_stop
73
+ perform_cleanup
74
+ stopped = drain_thread(owned_thread) == :drained
75
+ wait_stop
76
+ @thread = nil unless owned_thread&.alive?
64
77
 
65
- Nonnative.logger.info "stopped server '#{service.name}'"
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
@@ -4,5 +4,5 @@ module Nonnative
4
4
  # The current gem version.
5
5
  #
6
6
  # @return [String]
7
- VERSION = '3.36.0'
7
+ VERSION = '3.38.0'
8
8
  end
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.36.0
4
+ version: 3.38.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alejandro Falkowski