Module: Tina4::Shutdown
- Defined in:
- lib/tina4/shutdown.rb
Overview
Graceful shutdown on SIGTERM / SIGINT.
The order is the contract, identical in all four Tina4 frameworks:
1. Stop accepting FIRST. The listening socket closes before anything is
drained, so a connection arriving after the signal gets a clean
CONNECTION REFUSED - not a 503, not a TCP reset.
2. Tell live WebSocket peers we are going away (RFC 6455 close code 1001).
3. Drain in-flight requests, bounded by TINA4_SHUTDOWN_TIMEOUT.
4. Stop background tasks, close database connections, exit 0.
SIGHUP is deliberately NOT trapped: the Rust CLI owns file watching and production logs go to stdout, so neither Puma's log-reopen nor gunicorn's config-reload use for SIGHUP is a Tina4 need.
Constant Summary collapse
- DEFAULT_TIMEOUT =
Matches Kubernetes' default terminationGracePeriodSeconds and Gunicorn's graceful_timeout, and is the same default in Python, PHP and Node.
30
Class Attribute Summary collapse
-
.in_flight_count ⇒ Object
readonly
Returns the value of attribute in_flight_count.
-
.timeout ⇒ Object
readonly
The resolved TINA4_SHUTDOWN_TIMEOUT.
Class Method Summary collapse
- .initiate_shutdown ⇒ Object
-
.release_resources ⇒ Object
The teardown NO web server can do for us, because no web server knows these things exist: live WebSocket peers owed an RFC 6455 close frame, Tina4 background threads, and ORM-bound database connections.
-
.setup(server: nil, timeout: nil, trap_signals: true) ⇒ Object
trap_signals: false when another server owns INT/TERM (Puma does).
- .shutting_down? ⇒ Boolean
- .track_request ⇒ Object
-
.wait_for_completion(timeout = nil) ⇒ Object
Block until initiate_shutdown has finished every teardown step.
Class Attribute Details
.in_flight_count ⇒ Object (readonly)
Returns the value of attribute in_flight_count.
24 25 26 |
# File 'lib/tina4/shutdown.rb', line 24 def in_flight_count @in_flight_count end |
.timeout ⇒ Object (readonly)
The resolved TINA4_SHUTDOWN_TIMEOUT. Public because the production path does not drain in Ruby at all - it maps this onto Puma's own force_shutdown_after so the documented env var means the same thing whichever server owns the socket.
30 31 32 |
# File 'lib/tina4/shutdown.rb', line 30 def timeout @timeout end |
Class Method Details
.initiate_shutdown ⇒ Object
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/tina4/shutdown.rb', line 65 def initiate_shutdown return if @shutting_down @shutting_down = true Tina4::Log.info("Shutdown signal received, stopping gracefully...") stop_accepting drained = wait_for_in_flight release_resources Tina4::Log.info("Shutdown complete") @shutdown_complete = true return if drained # The deadline expired with requests still in flight. WEBrick's accept # loop joins its worker threads with NO timeout of its own, so simply # returning here would hang the process for as long as the slowest # handler runs - exactly what TINA4_SHUTDOWN_TIMEOUT exists to prevent. # Flush first: exit! runs no at_exit handlers and does not flush stdio, # which would swallow the warning that explains the forced exit. $stdout.flush $stderr.flush exit!(0) end |
.release_resources ⇒ Object
The teardown NO web server can do for us, because no web server knows these things exist: live WebSocket peers owed an RFC 6455 close frame, Tina4 background threads, and ORM-bound database connections.
Public so the production path can run exactly the same teardown from an ensure around Puma's launcher: Puma owns the socket, the drain and the signals there, but a database connection it has never heard of would otherwise leak on every single shutdown.
98 99 100 101 102 |
# File 'lib/tina4/shutdown.rb', line 98 def release_resources close_websockets stop_background_tasks close_database end |
.setup(server: nil, timeout: nil, trap_signals: true) ⇒ Object
trap_signals: false when another server owns INT/TERM (Puma does). A Tina4 trap on that path would be installed but never usefully serviced: there is no listener to close and nothing calls track_request, so if the other server's own trap install ever failed, ours would swallow the default terminate and do nothing - the process would survive the signal.
37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/tina4/shutdown.rb', line 37 def setup(server: nil, timeout: nil, trap_signals: true) @server = server @timeout = resolve_timeout(timeout) @shutting_down = false @shutdown_complete = false @mutex = Mutex.new @in_flight_count = 0 @in_flight_cv = ConditionVariable.new install_signal_handlers if trap_signals end |
.shutting_down? ⇒ Boolean
49 50 51 |
# File 'lib/tina4/shutdown.rb', line 49 def shutting_down? @shutting_down end |
.track_request ⇒ Object
53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/tina4/shutdown.rb', line 53 def track_request @mutex.synchronize { @in_flight_count += 1 } begin yield ensure @mutex.synchronize do @in_flight_count -= 1 @in_flight_cv.broadcast if @in_flight_count <= 0 end end end |
.wait_for_completion(timeout = nil) ⇒ Object
Block until initiate_shutdown has finished every teardown step.
stop_accepting unblocks WEBrick's accept loop immediately, so its #start returns as soon as the in-flight workers are joined - which can be while the signal handler's thread is still stopping background tasks and closing database connections. The server's main thread calls this so the process does not exit out from under that teardown.
111 112 113 114 115 116 117 118 119 |
# File 'lib/tina4/shutdown.rb', line 111 def wait_for_completion(timeout = nil) return unless @shutting_down deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + (timeout || @timeout.to_f + 5) until @shutdown_complete || Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline sleep 0.02 end end |