Files
gitlab-foss/spec/lib/gitlab/sidekiq_middleware_spec.rb
2025-04-23 09:11:51 +00:00

330 lines
12 KiB
Ruby

# frozen_string_literal: true
require 'spec_helper'
require 'sidekiq/testing'
RSpec.describe Gitlab::SidekiqMiddleware, feature_category: :shared do
let(:job_args) { [0.01] }
let(:disabled_sidekiq_middlewares) { [] }
let(:chain) { Sidekiq::Middleware::Chain.new(Sidekiq) }
let(:queue) { 'test' }
let(:enabled_sidekiq_middlewares) { all_sidekiq_middlewares - disabled_sidekiq_middlewares }
let(:worker_class) do
Class.new do
def self.name
'TestWorker'
end
include ApplicationWorker
def perform(*args)
Gitlab::SafeRequestStore['gitaly_call_actual'] = 1
Gitlab::SafeRequestStore[:gitaly_query_time] = 5
end
end
end
before do
stub_const('TestWorker', worker_class)
end
shared_examples "a middleware chain" do
before do
configurator.call(chain)
stub_feature_flags("drop_sidekiq_jobs_#{worker_class.name}": false) # not dropping the job
end
it "passes through the right middlewares", :aggregate_failures do
enabled_sidekiq_middlewares.each do |middleware|
expect_next_instances_of(middleware, 1, true) do |middleware_instance|
expect(middleware_instance).to receive(:call).with(*middleware_expected_args).once.and_call_original
end
end
expect { |b| chain.invoke(*worker_args, &b) }.to yield_control.once
end
end
shared_examples "a middleware chain for mailer" do
let(:worker_class) { ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper }
it_behaves_like "a middleware chain"
end
describe 'Server.configurator' do
let(:configurator) { described_class::Server.configurator }
let(:worker_args) { [worker_class.new, { 'args' => job_args }, queue] }
let(:middleware_expected_args) { [a_kind_of(worker_class), hash_including({ 'args' => job_args }), queue] }
let(:all_sidekiq_middlewares) { ::Gitlab::SidekiqMiddleware::Server.middlewares }
describe "server metrics" do
around do |example|
with_sidekiq_server_middleware do |chain|
described_class::Server.configurator(
metrics: true,
arguments_logger: true,
skip_jobs: false
).call(chain)
Sidekiq::Testing.inline! { example.run }
end
end
let(:gitaly_histogram) { double(:gitaly_histogram) }
before do
allow(Gitlab::Metrics).to receive(:histogram).and_call_original
allow(Gitlab::Metrics).to receive(:histogram)
.with(:sidekiq_jobs_gitaly_seconds, anything, anything, anything)
.and_return(gitaly_histogram)
end
it "records correct Gitaly duration" do
expect(gitaly_histogram).to receive(:observe).with(anything, 5.0)
worker_class.perform_async(*job_args)
end
end
context "all optional middlewares on" do
it_behaves_like "a middleware chain"
it_behaves_like "a middleware chain for mailer"
end
context "all optional middlewares off" do
let(:configurator) do
described_class::Server.configurator(
metrics: false,
arguments_logger: false,
skip_jobs: false
)
end
let(:disabled_sidekiq_middlewares) do
[
Gitlab::SidekiqMiddleware::ServerMetrics,
Gitlab::SidekiqMiddleware::ArgumentsLogger,
Gitlab::SidekiqMiddleware::SkipJobs
]
end
it_behaves_like "a middleware chain"
it_behaves_like "a middleware chain for mailer"
end
context 'when a job is concurrency limited' do
let(:concurrency_limit_middleware_index) do
all_sidekiq_middlewares.index(::Gitlab::SidekiqMiddleware::ConcurrencyLimit::Server)
end
let(:disabled_sidekiq_middlewares) do
# all middlewares after ConcurrencyLimit::Server
all_sidekiq_middlewares[(concurrency_limit_middleware_index + 1)..]
end
before do
configurator.call(chain)
stub_feature_flags("drop_sidekiq_jobs_#{worker_class.name}": false) # not dropping the job
# Apply concurrency limiting
allow(::Gitlab::SidekiqMiddleware::ConcurrencyLimit::WorkersMap).to receive(:over_the_limit?).and_return(true)
allow(::Gitlab::SidekiqMiddleware::ConcurrencyLimit::ConcurrencyLimitService)
.to receive(:has_jobs_in_queue?).and_return(true)
allow(::Gitlab::SidekiqMiddleware::ConcurrencyLimit::ConcurrencyLimitService)
.to receive(:add_to_queue!)
end
it "passes through the right middlewares and clears idempotency key", :aggregate_failures do
expect_next_instance_of(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob) do |dj|
expect(dj).to receive(:delete!).and_call_original
end
enabled_sidekiq_middlewares.each do |middleware|
expect_next_instances_of(middleware, 1, true) do |middleware_instance|
expect(middleware_instance).to receive(:call).with(*middleware_expected_args).once.and_call_original
end
end
chain.invoke(*worker_args)
end
end
end
describe 'Client.configurator' do
let(:configurator) { described_class::Client.configurator }
let(:redis_pool) { Sidekiq.redis_pool }
let(:middleware_expected_args) { [worker_class, hash_including({ 'args' => job_args }), queue, redis_pool] }
let(:worker_args) { [worker_class, { 'args' => job_args }, queue, redis_pool] }
let(:all_sidekiq_middlewares) { ::Gitlab::SidekiqMiddleware::Client.middlewares }
it_behaves_like "a middleware chain"
it_behaves_like "a middleware chain for mailer"
end
context 'in between DuplicateJobs::Client and DuplicateJobs::Server' do
# Everything from DuplicateJobs::Client to DuplicateJobs::Server must yield
# no returning or job interception as it will leave the duplicate job redis key
# dangling and erroneously deduplicating future jobs until key expires.
#
# If a new middleware is added in between
# DuplicateJobs::Client and DuplicateJobs::Server, please adjust
# allowed_middlewares below accordingly.
let(:allowed_middlewares_after_duplicate_jobs_client) do
[
::Gitlab::SidekiqStatus::ClientMiddleware,
::Gitlab::SidekiqMiddleware::AdminMode::Client,
::Gitlab::SidekiqMiddleware::SizeLimiter::Client,
::Gitlab::SidekiqMiddleware::ClientMetrics,
::Gitlab::SidekiqMiddleware::Identity::Passthrough
]
end
let(:allowed_middlewares_before_duplicate_jobs_server) do
[
::Gitlab::SidekiqMiddleware::SizeLimiter::Server,
::Gitlab::SidekiqMiddleware::ShardAwarenessValidator,
::Gitlab::SidekiqMiddleware::Monitor,
::Labkit::Middleware::Sidekiq::Server,
::Gitlab::SidekiqMiddleware::RequestStoreMiddleware,
::Gitlab::QueryLimiting::SidekiqMiddleware,
::Gitlab::SidekiqMiddleware::ServerMetrics,
::Gitlab::SidekiqMiddleware::ArgumentsLogger,
::Gitlab::SidekiqMiddleware::ExtraDoneLogMetadata,
::Gitlab::SidekiqMiddleware::BatchLoader,
::Gitlab::SidekiqMiddleware::InstrumentationLogger,
::Gitlab::SidekiqMiddleware::SetIpAddress,
::Gitlab::SidekiqMiddleware::AdminMode::Server,
::Gitlab::SidekiqMiddleware::QueryAnalyzer,
::Gitlab::SidekiqVersioning::Middleware,
::Gitlab::SidekiqStatus::ServerMiddleware,
::Gitlab::SidekiqMiddleware::WorkerContext::Server,
::ClickHouse::MigrationSupport::SidekiqMiddleware
]
end
shared_examples 'a middleware chain not intercepting job' do
it 'must not have any middleware intercepting job' do
expect { |b| chain.invoke(*worker_args, &b) }.to yield_control.once
end
end
context 'after DuplicateJobs::Client' do
before do
allow(described_class::Client).to receive(:middlewares).and_return(middlewares_after_duplicate_jobs_client)
configurator.call(chain)
end
let(:configurator) { described_class::Client.configurator }
let(:redis_pool) { Sidekiq.redis_pool }
let(:worker_args) { [worker_class, { 'args' => job_args }, queue, redis_pool] }
let(:client_middlewares) { described_class::Client.middlewares }
let(:duplicate_jobs_client_middleware_index) do
client_middlewares.index(::Gitlab::SidekiqMiddleware::DuplicateJobs::Client)
end
let(:middlewares_after_duplicate_jobs_client) do
client_middlewares[(duplicate_jobs_client_middleware_index + 1)..]
end
it_behaves_like 'a middleware chain not intercepting job'
it 'only contains allowed middlewares' do
expect(middlewares_after_duplicate_jobs_client).to contain_sidekiq_middlewares_exactly(
allowed_middlewares_after_duplicate_jobs_client)
end
end
context 'before DuplicateJobs::Server' do
before do
allow(described_class::Server).to receive(:middlewares).and_return(middlewares_before_duplicate_jobs_server)
configurator.call(chain)
end
let(:configurator) { described_class::Server.configurator }
let(:worker_args) { [worker_class.new, { 'args' => job_args }, queue] }
let(:server_middlewares) { described_class::Server.middlewares }
let(:duplicate_jobs_server_middleware_index) do
server_middlewares.index(::Gitlab::SidekiqMiddleware::DuplicateJobs::Server)
end
let(:middlewares_before_duplicate_jobs_server) do
server_middlewares[0..(duplicate_jobs_server_middleware_index - 1)]
end
it_behaves_like 'a middleware chain not intercepting job'
it 'only contains allowed middlewares' do
expect(middlewares_before_duplicate_jobs_server).to contain_sidekiq_middlewares_exactly(
allowed_middlewares_before_duplicate_jobs_server)
end
end
end
describe 'Client.middlewares' do
let(:middlewares) { described_class::Client.middlewares }
context 'ConcurrencyLimit::Resume' do
it 'is placed first' do
expect(middlewares.first).to eq(::Gitlab::SidekiqMiddleware::ConcurrencyLimit::Resume)
end
end
context 'WorkerContext::Client' do
it 'comes before Labkit middleware' do
expect(::Gitlab::SidekiqMiddleware::WorkerContext::Client)
.to come_before(::Labkit::Middleware::Sidekiq::Client)
.in(middlewares)
end
end
context 'Gitlab::Database::LoadBalancing::SidekiqClientMiddleware' do
it 'comes before DuplicateJobs::Client' do
expect(::Gitlab::Database::LoadBalancing::SidekiqClientMiddleware)
.to come_before(::Gitlab::SidekiqMiddleware::DuplicateJobs::Client)
.in(middlewares)
end
end
context 'SizeLimiter::Client' do
it 'comes before ClientMetrics' do
expect(::Gitlab::SidekiqMiddleware::SizeLimiter::Client)
.to come_before(::Gitlab::SidekiqMiddleware::ClientMetrics)
.in(middlewares)
end
end
end
describe 'Server.middlewares' do
let(:middlewares) { described_class::Server.middlewares }
context 'SizeLimiter::Server' do
it 'is placed first' do
expect(middlewares.first).to eq(::Gitlab::SidekiqMiddleware::SizeLimiter::Server)
end
end
context 'Labkit::Middleware::Sidekiq::Server' do
it 'comes before ServerMetrics' do
expect(::Labkit::Middleware::Sidekiq::Server)
.to come_before(::Gitlab::SidekiqMiddleware::ServerMetrics)
.in(middlewares)
end
end
context 'DuplicateJobs::Server' do
it 'comes before Gitlab::Database::LoadBalancing::SidekiqServerMiddleware' do
expect(::Gitlab::SidekiqMiddleware::DuplicateJobs::Server)
.to come_before(::Gitlab::Database::LoadBalancing::SidekiqServerMiddleware)
.in(middlewares)
end
end
context 'PauseControl::Server' do
it 'does not come before DuplicateJobs::Server' do
expect(::Gitlab::SidekiqMiddleware::PauseControl::Server)
.not_to come_before(::Gitlab::SidekiqMiddleware::DuplicateJobs::Server)
.in(middlewares)
end
end
end
end