From 1113831c15c11d671271f2182cdcf7a62d2682b3 Mon Sep 17 00:00:00 2001
From: Matthias Kaeppler <mkaeppler@gitlab.com>
Date: Tue, 8 Nov 2022 16:41:33 +0100
Subject: [PATCH] Add lifecycle hook for worker stop event

This will be used soon in order to produce
heap dumps when a worker was terminated due
to high memory use.
---
 config/initializers/sidekiq.rb                |  4 ++
 config/puma.example.development.rb            |  5 ++
 config/puma.rb.example                        |  5 ++
 lib/gitlab/cluster/lifecycle_events.rb        | 17 ++++++
 .../gitlab/cluster/lifecycle_events_spec.rb   | 59 ++++++++++++-------
 5 files changed, 69 insertions(+), 21 deletions(-)

diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index d1b734fb4bccd..c68ba000e0443 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -83,6 +83,10 @@ def enable_semi_reliable_fetch_mode?
     end
   end
 
+  config.on(:shutdown) do
+    Gitlab::Cluster::LifecycleEvents.do_worker_stop
+  end
+
   if enable_reliable_fetch?
     config.options[:semi_reliable_fetch] = enable_semi_reliable_fetch_mode?
     Sidekiq::ReliableFetch.setup_reliable_fetch!(config)
diff --git a/config/puma.example.development.rb b/config/puma.example.development.rb
index 3164ffe3ef42f..71a4e9b36f1e2 100644
--- a/config/puma.example.development.rb
+++ b/config/puma.example.development.rb
@@ -67,6 +67,11 @@
   Gitlab::Cluster::LifecycleEvents.do_worker_start
 end
 
+on_worker_shutdown do
+  # Signal application hooks that a worker is shutting down
+  Gitlab::Cluster::LifecycleEvents.do_worker_stop
+end
+
 # Preload the application before starting the workers; this conflicts with
 # phased restart feature. (off by default)
 
diff --git a/config/puma.rb.example b/config/puma.rb.example
index c70baf6570e60..59844b4aecffe 100644
--- a/config/puma.rb.example
+++ b/config/puma.rb.example
@@ -57,6 +57,11 @@ on_worker_boot do
   Gitlab::Cluster::LifecycleEvents.do_worker_start
 end
 
+on_worker_shutdown do
+  # Signal application hooks that a worker is shutting down
+  Gitlab::Cluster::LifecycleEvents.do_worker_stop
+end
+
 # Preload the application before starting the workers; this conflicts with
 # phased restart feature. (off by default)
 preload_app!
diff --git a/lib/gitlab/cluster/lifecycle_events.rb b/lib/gitlab/cluster/lifecycle_events.rb
index be08ada9d2f61..b39d2a02f0285 100644
--- a/lib/gitlab/cluster/lifecycle_events.rb
+++ b/lib/gitlab/cluster/lifecycle_events.rb
@@ -63,6 +63,15 @@ module Cluster
     #
     #     Sidekiq/Puma Single: This is called immediately.
     #
+    # - on_worker_stop (on worker process):
+    #
+    #     Puma Cluster: Called in the worker process
+    #       exactly once after it stops processing requests
+    #       but before it shuts down.
+    #
+    #     Sidekiq: Called after the scheduler shuts down but
+    #       before the worker finishes ongoing jobs.
+    #
     # Blocks will be executed in the order in which they are registered.
     #
     class LifecycleEvents
@@ -113,6 +122,10 @@ def on_master_start(&block)
           end
         end
 
+        def on_worker_stop(&block)
+          (@worker_stop_hooks ||= []) << block
+        end
+
         #
         # Lifecycle integration methods (called from puma.rb, etc.)
         #
@@ -137,6 +150,10 @@ def do_before_master_restart
           call(:master_restart_hooks, @master_restart_hooks)
         end
 
+        def do_worker_stop
+          call(:worker_stop_hooks, @worker_stop_hooks)
+        end
+
         # DEPRECATED
         alias_method :do_master_restart, :do_before_master_restart
 
diff --git a/spec/lib/gitlab/cluster/lifecycle_events_spec.rb b/spec/lib/gitlab/cluster/lifecycle_events_spec.rb
index 5eea78acd98cc..45becb8370c98 100644
--- a/spec/lib/gitlab/cluster/lifecycle_events_spec.rb
+++ b/spec/lib/gitlab/cluster/lifecycle_events_spec.rb
@@ -3,38 +3,55 @@
 require 'spec_helper'
 
 RSpec.describe Gitlab::Cluster::LifecycleEvents do
+  using RSpec::Parameterized::TableSyntax
+
   # we create a new instance to ensure that we do not touch existing hooks
   let(:replica) { Class.new(described_class) }
 
-  context 'hooks execution' do
-    using RSpec::Parameterized::TableSyntax
+  before do
+    # disable blackout period to speed-up tests
+    stub_config(shutdown: { blackout_seconds: 0 })
+  end
 
-    where(:method, :hook_names) do
-      :do_worker_start             | %i[worker_start_hooks]
-      :do_before_fork              | %i[before_fork_hooks]
-      :do_before_graceful_shutdown | %i[master_blackout_period master_graceful_shutdown]
-      :do_before_master_restart    | %i[master_restart_hooks]
+  context 'outside of clustered environments' do
+    where(:hook, :was_executed_immediately) do
+      :on_worker_start             | true
+      :on_before_fork              | false
+      :on_before_graceful_shutdown | false
+      :on_before_master_restart    | false
+      :on_worker_stop              | false
     end
 
-    before do
-      # disable blackout period to speed-up tests
-      stub_config(shutdown: { blackout_seconds: 0 })
+    with_them do
+      it 'executes the given block immediately' do
+        was_executed = false
+        replica.public_send(hook, &proc { was_executed = true })
+
+        expect(was_executed).to eq(was_executed_immediately)
+      end
     end
+  end
 
-    with_them do
-      subject { replica.public_send(method) }
+  context 'in clustered environments' do
+    before do
+      allow(Gitlab::Runtime).to receive(:puma?).and_return(true)
+      replica.set_puma_options(workers: 2)
+    end
 
-      it 'executes all hooks' do
-        hook_names.each do |hook_name|
-          hook = double
-          replica.instance_variable_set(:"@#{hook_name}", [hook])
+    where(:hook, :execution_helper) do
+      :on_worker_start             | :do_worker_start
+      :on_before_fork              | :do_before_fork
+      :on_before_graceful_shutdown | :do_before_graceful_shutdown
+      :on_before_master_restart    | :do_before_master_restart
+      :on_worker_stop              | :do_worker_stop
+    end
 
-          # ensure that proper hooks are called
-          expect(hook).to receive(:call)
-          expect(replica).to receive(:call).with(hook_name, anything).and_call_original
-        end
+    with_them do
+      it 'requires explicit execution via do_* helper' do
+        was_executed = false
+        replica.public_send(hook, &proc { was_executed = true })
 
-        subject
+        expect { replica.public_send(execution_helper) }.to change { was_executed }.from(false).to(true)
       end
     end
   end
-- 
GitLab