1# frozen_string_literal: true 2 3require_relative '../utils' # Gitlab::Utils 4 5module Gitlab 6 module Cluster 7 # 8 # LifecycleEvents lets Rails initializers register application startup hooks 9 # that are sensitive to forking. For example, to defer the creation of 10 # watchdog threads. This lets us abstract away the Unix process 11 # lifecycles of Sidekiq, Puma, Puma Cluster, etc. 12 # 13 # We have the following lifecycle events. 14 # 15 # - on_before_fork (on master process): 16 # 17 # Puma Cluster: This will be called exactly once, 18 # on startup, before the workers are forked. This is 19 # called in the PARENT/MASTER process. 20 # 21 # Sidekiq/Puma Single: This is not called. 22 # 23 # - on_master_start (on master process): 24 # 25 # Puma Cluster: This will be called exactly once, 26 # on startup, before the workers are forked. This is 27 # called in the PARENT/MASTER process. 28 # 29 # Sidekiq/Puma Single: This is called immediately. 30 # 31 # - on_before_blackout_period (on master process): 32 # 33 # Puma Cluster: This will be called before a blackout 34 # period when performing graceful shutdown of master. 35 # This is called on `master` process. 36 # 37 # Sidekiq/Puma Single: This is not called. 38 # 39 # - on_before_graceful_shutdown (on master process): 40 # 41 # Puma Cluster: This will be called before a graceful 42 # shutdown of workers starts happening, but after blackout period. 43 # This is called on `master` process. 44 # 45 # Sidekiq/Puma Single: This is not called. 46 # 47 # - on_before_master_restart (on master process): 48 # 49 # Puma Cluster: This will be called before a new master is spun up. 50 # This is called on `master` process. 51 # 52 # Sidekiq/Puma Single: This is not called. 53 # 54 # - on_worker_start (on worker process): 55 # 56 # Puma Cluster: This is called in the worker process 57 # exactly once before processing requests. 58 # 59 # Sidekiq/Puma Single: This is called immediately. 60 # 61 # Blocks will be executed in the order in which they are registered. 62 # 63 class LifecycleEvents 64 FatalError = Class.new(Exception) # rubocop:disable Lint/InheritException 65 66 USE_FATAL_LIFECYCLE_EVENTS = Gitlab::Utils.to_boolean(ENV.fetch('GITLAB_FATAL_LIFECYCLE_EVENTS', 'true')) 67 68 class << self 69 # 70 # Hook registration methods (called from initializers) 71 # 72 def on_worker_start(&block) 73 if in_clustered_environment? 74 # Defer block execution 75 (@worker_start_hooks ||= []) << block 76 else 77 yield 78 end 79 end 80 81 def on_before_fork(&block) 82 # Defer block execution 83 (@before_fork_hooks ||= []) << block 84 end 85 86 # Read the config/initializers/cluster_events_before_phased_restart.rb 87 def on_before_blackout_period(&block) 88 # Defer block execution 89 (@master_blackout_period ||= []) << block 90 end 91 92 # Read the config/initializers/cluster_events_before_phased_restart.rb 93 def on_before_graceful_shutdown(&block) 94 # Defer block execution 95 (@master_graceful_shutdown ||= []) << block 96 end 97 98 def on_before_master_restart(&block) 99 # Defer block execution 100 (@master_restart_hooks ||= []) << block 101 end 102 103 def on_master_start(&block) 104 if in_clustered_environment? 105 on_before_fork(&block) 106 else 107 on_worker_start(&block) 108 end 109 end 110 111 # 112 # Lifecycle integration methods (called from puma.rb, etc.) 113 # 114 def do_worker_start 115 call(:worker_start_hooks, @worker_start_hooks) 116 end 117 118 def do_before_fork 119 call(:before_fork_hooks, @before_fork_hooks) 120 end 121 122 def do_before_graceful_shutdown 123 call(:master_blackout_period, @master_blackout_period) 124 125 blackout_seconds = ::Settings.shutdown.blackout_seconds.to_i 126 sleep(blackout_seconds) if blackout_seconds > 0 127 128 call(:master_graceful_shutdown, @master_graceful_shutdown) 129 end 130 131 def do_before_master_restart 132 call(:master_restart_hooks, @master_restart_hooks) 133 end 134 135 # DEPRECATED 136 alias_method :do_master_restart, :do_before_master_restart 137 138 # Puma doesn't use singletons (which is good) but 139 # this means we need to pass through whether the 140 # puma server is running in single mode or cluster mode 141 def set_puma_options(options) 142 @puma_options = options 143 end 144 145 private 146 147 def call(name, hooks) 148 return unless hooks 149 150 hooks.each do |hook| 151 hook.call 152 rescue StandardError => e 153 Gitlab::ErrorTracking.track_exception(e, type: 'LifecycleEvents', hook: hook) 154 warn("ERROR: The hook #{name} failed with exception (#{e.class}) \"#{e.message}\".") 155 156 # we consider lifecycle hooks to be fatal errors 157 raise FatalError, e if USE_FATAL_LIFECYCLE_EVENTS 158 end 159 end 160 161 def in_clustered_environment? 162 # Sidekiq doesn't fork 163 return false if Gitlab::Runtime.sidekiq? 164 165 # Puma sometimes forks 166 return true if in_clustered_puma? 167 168 # Default assumption is that we don't fork 169 false 170 end 171 172 def in_clustered_puma? 173 return false unless Gitlab::Runtime.puma? 174 175 @puma_options && @puma_options[:workers] && @puma_options[:workers] > 0 176 end 177 end 178 end 179 end 180end 181