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

Class Method Summary collapse

Class Attribute Details

.in_flight_countObject (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

.timeoutObject (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_shutdownObject



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_resourcesObject

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

Returns:

  • (Boolean)


49
50
51
# File 'lib/tina4/shutdown.rb', line 49

def shutting_down?
  @shutting_down
end

.track_requestObject



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