Class: Fractor::ShutdownHandler

Inherits:
Object
  • Object
show all
Defined in:
lib/fractor/shutdown_handler.rb

Overview

Manages the shutdown process for a Supervisor. Responsible for gracefully stopping all components in the correct order.

This class extracts shutdown logic from Supervisor to follow the Single Responsibility Principle.

Instance Method Summary collapse

Constructor Details

#initialize(workers, wakeup_ractor, timer_thread, performance_monitor, main_loop_thread: nil, debug: false, continuous_mode: false) ⇒ ShutdownHandler

Returns a new instance of ShutdownHandler.



10
11
12
13
14
15
16
17
18
19
# File 'lib/fractor/shutdown_handler.rb', line 10

def initialize(workers, wakeup_ractor, timer_thread, performance_monitor,
               main_loop_thread: nil, debug: false, continuous_mode: false)
  @workers = workers
  @wakeup_ractor = wakeup_ractor
  @timer_thread = timer_thread
  @performance_monitor = performance_monitor
  @main_loop_thread = main_loop_thread
  @debug = debug
  @continuous_mode = continuous_mode
end

Instance Method Details

#complete?Boolean

Check if the shutdown process has completed. This is useful for testing and monitoring.

Returns:

  • (Boolean)

    true if all components are stopped



154
155
156
157
158
159
# File 'lib/fractor/shutdown_handler.rb', line 154

def complete?
  timer_stopped = @timer_thread.nil? || !@timer_thread.alive?
  workers_stopped = @workers.empty? || @workers.all?(&:closed?)

  timer_stopped && workers_stopped
end

#shutdown(wait_for_completion: false, timeout: 10) ⇒ void

This method returns an undefined value.

Execute a graceful shutdown of all supervisor components. Components are stopped in the correct order to prevent issues:

  1. Stop performance monitor (to stop metric collection)
  2. Skip stopping timer thread in continuous mode (it will exit when workers close)
  3. Stop timer thread in batch mode (to stop periodic wakeups)
  4. Signal wakeup ractor (to unblock Ractor.select)
  5. Signal all workers (to stop processing)
  6. Wait for main loop thread and workers to finish

Parameters:

  • wait_for_completion (Boolean) (defaults to: false)

    Whether to wait for all workers to close

  • timeout (Integer) (defaults to: 10)

    Maximum seconds to wait for shutdown (default: 10)



33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/fractor/shutdown_handler.rb', line 33

def shutdown(wait_for_completion: false, timeout: 10)
  stop_performance_monitor

  # Only stop timer thread in batch mode. In continuous mode, the timer thread
  # will exit on its own when workers are closed (it checks this condition).
  stop_timer_thread unless @continuous_mode

  signal_wakeup_ractor
  signal_all_workers

  wait_for_shutdown_completion(timeout) if wait_for_completion
end

#signal_all_workersvoid

This method returns an undefined value.

Send shutdown signal to all workers. Workers should gracefully finish their current work and exit.



138
139
140
141
142
143
144
145
146
147
148
# File 'lib/fractor/shutdown_handler.rb', line 138

def signal_all_workers
  @workers.each do |w|
    begin
      w.send(:shutdown)
    rescue StandardError
      # Ignore errors when sending shutdown to workers
      nil
    end
    puts "Sent shutdown signal to #{w.name}" if @debug
  end
end

#signal_wakeup_ractorvoid

This method returns an undefined value.

Signal the wakeup ractor to unblock Ractor.select. This is done first to allow the main loop to process the shutdown.



123
124
125
126
127
128
129
130
131
132
# File 'lib/fractor/shutdown_handler.rb', line 123

def signal_wakeup_ractor
  return unless @wakeup_ractor

  begin
    @wakeup_ractor.send(:shutdown)
    puts "Sent shutdown signal to wakeup ractor" if @debug
  rescue StandardError => e
    puts "Error sending shutdown to wakeup ractor: #{e.message}" if @debug
  end
end

#status_summaryHash

Get a summary of the shutdown status.

Returns:

  • (Hash)

    Status summary with component states



164
165
166
167
168
169
170
171
172
# File 'lib/fractor/shutdown_handler.rb', line 164

def status_summary
  {
    performance_monitor: @performance_monitor&.send(:monitoring?) || false,
    timer_thread: @timer_thread&.alive? || false,
    wakeup_ractor: !@wakeup_ractor.nil?,
    workers_count: @workers.size,
    workers_closed: @workers.count(&:closed?),
  }
end

#stop_performance_monitorvoid

This method returns an undefined value.

Stop the performance monitor if it's enabled.



94
95
96
97
98
99
100
101
102
103
# File 'lib/fractor/shutdown_handler.rb', line 94

def stop_performance_monitor
  return unless @performance_monitor

  begin
    @performance_monitor.stop
    puts "Performance monitor stopped" if @debug
  rescue StandardError => e
    puts "Error stopping performance monitor: #{e.message}" if @debug
  end
end

#stop_timer_threadvoid

This method returns an undefined value.

Wait for the timer thread to finish if it exists.



108
109
110
111
112
113
114
115
116
117
# File 'lib/fractor/shutdown_handler.rb', line 108

def stop_timer_thread
  return unless @timer_thread

  # Only wait if thread is alive
  if @timer_thread.alive?
    @timer_thread.join(1) # Wait up to 1 second
    @timer_thread.kill # Ensure thread is stopped
    puts "Timer thread stopped" if @debug
  end
end

#wait_for_shutdown_completion(timeout = 10) ⇒ Boolean

Wait for all components to finish after shutdown signals have been sent. This waits for both the main loop thread (if provided) and all workers to close. This is important for tests and for ensuring clean shutdown.

Parameters:

  • timeout (Integer) (defaults to: 10)

    Maximum seconds to wait

Returns:

  • (Boolean)

    true if all components finished, false if timeout



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/fractor/shutdown_handler.rb', line 52

def wait_for_shutdown_completion(timeout = 10)
  start_time = Time.now
  poll_interval = 0.1

  loop do
    # Check if timeout exceeded
    break if Time.now - start_time > timeout

    # Check main loop thread status (if provided and alive)
    main_loop_done = @main_loop_thread.nil? || !@main_loop_thread.alive?

    # Check if all workers are closed
    workers_done = @workers.empty? || @workers.all?(&:closed?)

    # If both main loop and workers are done, we're finished
    if main_loop_done && workers_done
      puts "All components closed successfully" if @debug
      return true
    end

    # Show status while waiting
    if @debug
      closed_count = @workers.count(&:closed?)
      main_status = @main_loop_thread&.alive? ? "running" : "stopped"
      puts "Waiting for shutdown: main_loop=#{main_status}, workers=#{closed_count}/#{@workers.size} closed"
    end

    sleep(poll_interval)
  end

  # Timeout exceeded
  if @debug
    closed_count = @workers.count(&:closed?)
    main_status = @main_loop_thread&.alive? ? "running" : "stopped"
    puts "Shutdown timeout: main_loop=#{main_status}, workers=#{closed_count}/#{@workers.size} closed after #{timeout}s"
  end
  false
end