Commit 3683ce9f authored by Aleksei Lipniagov's avatar Aleksei Lipniagov

Fix /-/readiness probe for Puma Single

Introduces `puma_in_clustered_mode?` check.
Introduces `.available?` for health checks.
Sets MasterCheck as not available for Puma in a Single mode.
parent 211062fa
---
title: Fix /-/readiness probe for Puma Single
merge_request: 53708
author:
type: other
...@@ -4,7 +4,7 @@ def max_puma_workers ...@@ -4,7 +4,7 @@ def max_puma_workers
Puma.cli_config.options[:workers].to_i Puma.cli_config.options[:workers].to_i
end end
if Gitlab::Runtime.puma? && max_puma_workers == 0 if Gitlab::Runtime.puma? && !Gitlab::Runtime.puma_in_clustered_mode?
raise 'Puma is only supported in Clustered mode (workers > 0)' if Gitlab.com? raise 'Puma is only supported in Clustered mode (workers > 0)' if Gitlab.com?
warn 'WARNING: Puma is running in Single mode (workers = 0). Some features may not work. Please refer to https://gitlab.com/groups/gitlab-org/-/epics/5303 for info.' warn 'WARNING: Puma is running in Single mode (workers = 0). Some features may not work. Please refer to https://gitlab.com/groups/gitlab-org/-/epics/5303 for info.'
......
...@@ -11,6 +11,10 @@ module Gitlab ...@@ -11,6 +11,10 @@ module Gitlab
name.sub(/_check$/, '').capitalize name.sub(/_check$/, '').capitalize
end end
def available?
true
end
def readiness def readiness
raise NotImplementedError raise NotImplementedError
end end
......
...@@ -8,7 +8,16 @@ module Gitlab ...@@ -8,7 +8,16 @@ module Gitlab
extend SimpleAbstractCheck extend SimpleAbstractCheck
class << self class << self
extend ::Gitlab::Utils::Override
override :available?
def available?
Gitlab::Runtime.puma_in_clustered_mode?
end
def register_master def register_master
return unless available?
# when we fork, we pass the read pipe to child # when we fork, we pass the read pipe to child
# child can then react on whether the other end # child can then react on whether the other end
# of pipe is still available # of pipe is still available
...@@ -16,11 +25,15 @@ module Gitlab ...@@ -16,11 +25,15 @@ module Gitlab
end end
def finish_master def finish_master
return unless available?
close_read close_read
close_write close_write
end end
def register_worker def register_worker
return unless available?
# fork needs to close the pipe # fork needs to close the pipe
close_write close_write
end end
......
...@@ -48,6 +48,7 @@ module Gitlab ...@@ -48,6 +48,7 @@ module Gitlab
def probe_readiness def probe_readiness
checks checks
.select(&:available?)
.flat_map(&:readiness) .flat_map(&:readiness)
.compact .compact
.group_by(&:name) .group_by(&:name)
......
...@@ -12,6 +12,10 @@ module Gitlab ...@@ -12,6 +12,10 @@ module Gitlab
Gitlab::HealthChecks::Result.new( Gitlab::HealthChecks::Result.new(
'web_exporter', exporter.running) 'web_exporter', exporter.running)
end end
def available?
true
end
end end
attr_reader :running attr_reader :running
......
...@@ -81,6 +81,10 @@ module Gitlab ...@@ -81,6 +81,10 @@ module Gitlab
puma? || sidekiq? || action_cable? puma? || sidekiq? || action_cable?
end end
def puma_in_clustered_mode?
puma? && Puma.cli_config.options[:workers].to_i > 0
end
def max_threads def max_threads
threads = 1 # main thread threads = 1 # main thread
......
...@@ -4,47 +4,67 @@ require 'spec_helper' ...@@ -4,47 +4,67 @@ require 'spec_helper'
require_relative './simple_check_shared' require_relative './simple_check_shared'
RSpec.describe Gitlab::HealthChecks::MasterCheck do RSpec.describe Gitlab::HealthChecks::MasterCheck do
let(:result_class) { Gitlab::HealthChecks::Result }
before do before do
stub_const('SUCCESS_CODE', 100) stub_const('SUCCESS_CODE', 100)
stub_const('FAILURE_CODE', 101) stub_const('FAILURE_CODE', 101)
described_class.register_master
end end
after do context 'when Puma runs in Clustered mode' do
described_class.finish_master before do
end allow(Gitlab::Runtime).to receive(:puma_in_clustered_mode?).and_return(true)
describe '#readiness' do described_class.register_master
context 'when master is running' do end
it 'worker does return success' do
_, child_status = run_worker
expect(child_status.exitstatus).to eq(SUCCESS_CODE) after do
end described_class.finish_master
end end
context 'when master finishes early' do describe '.available?' do
before do specify { expect(described_class.available?).to be true }
described_class.send(:close_write) end
describe '.readiness' do
context 'when master is running' do
it 'worker does return success' do
_, child_status = run_worker
expect(child_status.exitstatus).to eq(SUCCESS_CODE)
end
end end
it 'worker does return failure' do context 'when master finishes early' do
_, child_status = run_worker before do
described_class.send(:close_write)
end
expect(child_status.exitstatus).to eq(FAILURE_CODE) it 'worker does return failure' do
_, child_status = run_worker
expect(child_status.exitstatus).to eq(FAILURE_CODE)
end
end end
end
def run_worker def run_worker
pid = fork do pid = fork do
described_class.register_worker described_class.register_worker
exit(described_class.readiness.success ? SUCCESS_CODE : FAILURE_CODE) exit(described_class.readiness.success ? SUCCESS_CODE : FAILURE_CODE)
end
Process.wait2(pid)
end end
end
end
# '.readiness' check is not invoked if '.available?' returns false
context 'when Puma runs in Single mode' do
before do
allow(Gitlab::Runtime).to receive(:puma_in_clustered_mode?).and_return(false)
end
Process.wait2(pid) describe '.available?' do
specify { expect(described_class.available?).to be false }
end end
end end
end end
...@@ -61,6 +61,35 @@ RSpec.describe Gitlab::HealthChecks::Probes::Collection do ...@@ -61,6 +61,35 @@ RSpec.describe Gitlab::HealthChecks::Probes::Collection do
expect(subject.json[:message]).to eq('Redis::CannotConnectError : Redis down') expect(subject.json[:message]).to eq('Redis::CannotConnectError : Redis down')
end end
end end
context 'when some checks are not available' do
before do
allow(Gitlab::Runtime).to receive(:puma_in_clustered_mode?).and_return(false)
end
let(:checks) do
[
Gitlab::HealthChecks::MasterCheck
]
end
it 'asks for check availability' do
expect(Gitlab::HealthChecks::MasterCheck).to receive(:available?)
subject
end
it 'does not call `readiness` on checks that are not available' do
expect(Gitlab::HealthChecks::MasterCheck).not_to receive(:readiness)
subject
end
it 'does not fail collection check' do
expect(subject.http_status).to eq(200)
expect(subject.json[:status]).to eq('ok')
end
end
end end
context 'without checks' do context 'without checks' do
......
...@@ -44,10 +44,11 @@ RSpec.describe Gitlab::Runtime do ...@@ -44,10 +44,11 @@ RSpec.describe Gitlab::Runtime do
context "puma" do context "puma" do
let(:puma_type) { double('::Puma') } let(:puma_type) { double('::Puma') }
let(:max_workers) { 2 }
before do before do
stub_const('::Puma', puma_type) stub_const('::Puma', puma_type)
allow(puma_type).to receive_message_chain(:cli_config, :options).and_return(max_threads: 2) allow(puma_type).to receive_message_chain(:cli_config, :options).and_return(max_threads: 2, workers: max_workers)
stub_env('ACTION_CABLE_IN_APP', 'false') stub_env('ACTION_CABLE_IN_APP', 'false')
end end
...@@ -70,6 +71,20 @@ RSpec.describe Gitlab::Runtime do ...@@ -70,6 +71,20 @@ RSpec.describe Gitlab::Runtime do
it_behaves_like "valid runtime", :puma, 11 it_behaves_like "valid runtime", :puma, 11
end end
describe ".puma_in_clustered_mode?" do
context 'when Puma is set up with workers > 0' do
let(:max_workers) { 4 }
specify { expect(described_class.puma_in_clustered_mode?).to be true }
end
context 'when Puma is set up with workers = 0' do
let(:max_workers) { 0 }
specify { expect(described_class.puma_in_clustered_mode?).to be false }
end
end
end end
context "unicorn" do context "unicorn" do
......
...@@ -77,91 +77,129 @@ RSpec.describe HealthController do ...@@ -77,91 +77,129 @@ RSpec.describe HealthController do
shared_context 'endpoint responding with readiness data' do shared_context 'endpoint responding with readiness data' do
context 'when requesting instance-checks' do context 'when requesting instance-checks' do
it 'responds with readiness checks data' do context 'when Puma runs in Clustered mode' do
expect(Gitlab::HealthChecks::MasterCheck).to receive(:check) { true } before do
allow(Gitlab::Runtime).to receive(:puma_in_clustered_mode?).and_return(true)
subject end
expect(json_response).to include({ 'status' => 'ok' })
expect(json_response['master_check']).to contain_exactly({ 'status' => 'ok' })
end
it 'responds with readiness checks data when a failure happens' do it 'responds with readiness checks data' do
expect(Gitlab::HealthChecks::MasterCheck).to receive(:check) { false } expect(Gitlab::HealthChecks::MasterCheck).to receive(:check) { true }
subject subject
expect(json_response).to include({ 'status' => 'failed' }) expect(json_response).to include({ 'status' => 'ok' })
expect(json_response['master_check']).to contain_exactly( expect(json_response['master_check']).to contain_exactly({ 'status' => 'ok' })
{ 'status' => 'failed', 'message' => 'unexpected Master check result: false' }) end
expect(response).to have_gitlab_http_status(:service_unavailable) it 'responds with readiness checks data when a failure happens' do
expect(response.headers['X-GitLab-Custom-Error']).to eq(1) expect(Gitlab::HealthChecks::MasterCheck).to receive(:check) { false }
end
end
context 'when requesting all checks' do subject
before do
params.merge!(all: true)
end
it 'responds with readiness checks data' do expect(json_response).to include({ 'status' => 'failed' })
subject expect(json_response['master_check']).to contain_exactly(
{ 'status' => 'failed', 'message' => 'unexpected Master check result: false' })
expect(json_response['db_check']).to contain_exactly({ 'status' => 'ok' }) expect(response).to have_gitlab_http_status(:service_unavailable)
expect(json_response['cache_check']).to contain_exactly({ 'status' => 'ok' }) expect(response.headers['X-GitLab-Custom-Error']).to eq(1)
expect(json_response['queues_check']).to contain_exactly({ 'status' => 'ok' }) end
expect(json_response['shared_state_check']).to contain_exactly({ 'status' => 'ok' })
expect(json_response['gitaly_check']).to contain_exactly(
{ 'status' => 'ok', 'labels' => { 'shard' => 'default' } })
end end
it 'responds with readiness checks data when a failure happens' do context 'when Puma runs in Single mode' do
allow(Gitlab::HealthChecks::Redis::RedisCheck).to receive(:readiness).and_return( before do
Gitlab::HealthChecks::Result.new('redis_check', false, "check error")) allow(Gitlab::Runtime).to receive(:puma_in_clustered_mode?).and_return(false)
end
subject it 'does not invoke MasterCheck, succeedes' do
expect(Gitlab::HealthChecks::MasterCheck).not_to receive(:check) { true }
expect(json_response['cache_check']).to contain_exactly({ 'status' => 'ok' }) subject
expect(json_response['redis_check']).to contain_exactly(
{ 'status' => 'failed', 'message' => 'check error' })
expect(response).to have_gitlab_http_status(:service_unavailable) expect(json_response).to eq('status' => 'ok')
expect(response.headers['X-GitLab-Custom-Error']).to eq(1) end
end end
end
context 'when DB is not accessible and connection raises an exception' do context 'when requesting all checks' do
shared_context 'endpoint responding with readiness data for all checks' do
before do before do
expect(Gitlab::HealthChecks::DbCheck) params.merge!(all: true)
.to receive(:readiness)
.and_raise(PG::ConnectionBad, 'could not connect to server')
end end
it 'responds with 500 including the exception info' do it 'responds with readiness checks data' do
subject subject
expect(response).to have_gitlab_http_status(:internal_server_error) expect(json_response['db_check']).to contain_exactly({ 'status' => 'ok' })
expect(json_response['cache_check']).to contain_exactly({ 'status' => 'ok' })
expect(json_response['queues_check']).to contain_exactly({ 'status' => 'ok' })
expect(json_response['shared_state_check']).to contain_exactly({ 'status' => 'ok' })
expect(json_response['gitaly_check']).to contain_exactly(
{ 'status' => 'ok', 'labels' => { 'shard' => 'default' } })
end
it 'responds with readiness checks data when a failure happens' do
allow(Gitlab::HealthChecks::Redis::RedisCheck).to receive(:readiness).and_return(
Gitlab::HealthChecks::Result.new('redis_check', false, "check error"))
subject
expect(json_response['cache_check']).to contain_exactly({ 'status' => 'ok' })
expect(json_response['redis_check']).to contain_exactly(
{ 'status' => 'failed', 'message' => 'check error' })
expect(response).to have_gitlab_http_status(:service_unavailable)
expect(response.headers['X-GitLab-Custom-Error']).to eq(1) expect(response.headers['X-GitLab-Custom-Error']).to eq(1)
expect(json_response).to eq( end
{ 'status' => 'failed', 'message' => 'PG::ConnectionBad : could not connect to server' })
context 'when DB is not accessible and connection raises an exception' do
before do
expect(Gitlab::HealthChecks::DbCheck)
.to receive(:readiness)
.and_raise(PG::ConnectionBad, 'could not connect to server')
end
it 'responds with 500 including the exception info' do
subject
expect(response).to have_gitlab_http_status(:internal_server_error)
expect(response.headers['X-GitLab-Custom-Error']).to eq(1)
expect(json_response).to eq(
{ 'status' => 'failed', 'message' => 'PG::ConnectionBad : could not connect to server' })
end
end
context 'when any exception happens during the probing' do
before do
expect(Gitlab::HealthChecks::Redis::RedisCheck)
.to receive(:readiness)
.and_raise(::Redis::CannotConnectError, 'Redis down')
end
it 'responds with 500 including the exception info' do
subject
expect(response).to have_gitlab_http_status(:internal_server_error)
expect(response.headers['X-GitLab-Custom-Error']).to eq(1)
expect(json_response).to eq(
{ 'status' => 'failed', 'message' => 'Redis::CannotConnectError : Redis down' })
end
end end
end end
context 'when any exception happens during the probing' do context 'when Puma runs in Clustered mode' do
before do before do
expect(Gitlab::HealthChecks::Redis::RedisCheck) allow(Gitlab::Runtime).to receive(:puma_in_clustered_mode?).and_return(true)
.to receive(:readiness)
.and_raise(::Redis::CannotConnectError, 'Redis down')
end end
it 'responds with 500 including the exception info' do it_behaves_like 'endpoint responding with readiness data for all checks'
subject end
expect(response).to have_gitlab_http_status(:internal_server_error) context 'when Puma runs in Single mode' do
expect(response.headers['X-GitLab-Custom-Error']).to eq(1) before do
expect(json_response).to eq( allow(Gitlab::Runtime).to receive(:puma_in_clustered_mode?).and_return(false)
{ 'status' => 'failed', 'message' => 'Redis::CannotConnectError : Redis down' })
end end
it_behaves_like 'endpoint responding with readiness data for all checks'
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment