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