diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-08 21:08:59 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-08 21:08:59 +0300 |
commit | 8b0d3151ae81cef695647771d1781c535d6f6cf5 (patch) | |
tree | f58d72ada21f6f7598a1e9f69fc80cdbbae8f2b6 /spec | |
parent | ec9dd96cd876d8778bb757a1e1e0252a58fdcbbb (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
28 files changed, 533 insertions, 106 deletions
diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index 3f7d25e42da..8281cd595f8 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -441,13 +441,39 @@ RSpec.describe GraphqlController, feature_category: :integrations do post :execute, params: { query: query, operationName: 'IntrospectionQuery' } end - it 'does not cache an unknown introspection query' do - query = File.read(Rails.root.join('spec/fixtures/api/graphql/fake_introspection.graphql')) - - expect(GitlabSchema).to receive(:execute).exactly(:twice) + it 'logs that it will try to hit the cache' do + expect(Gitlab::AppLogger).to receive(:info).with( + message: "IntrospectionQueryCache", + can_use_introspection_query_cache: true, + query: query.to_s, + variables: {}, + introspection_query_cache_key: ['introspection-query-cache', Gitlab.revision, false] + ) post :execute, params: { query: query, operationName: 'IntrospectionQuery' } - post :execute, params: { query: query, operationName: 'IntrospectionQuery' } + end + + context 'when there is an unknown introspection query' do + let(:query) { File.read(Rails.root.join('spec/fixtures/api/graphql/fake_introspection.graphql')) } + + it 'logs that it did not try to hit the cache' do + expect(Gitlab::AppLogger).to receive(:info).with( + message: "IntrospectionQueryCache", + can_use_introspection_query_cache: false, + query: query.to_s, + variables: {}, + introspection_query_cache_key: ['introspection-query-cache', Gitlab.revision, false] + ) + + post :execute, params: { query: query, operationName: 'IntrospectionQuery' } + end + + it 'does not cache an unknown introspection query' do + expect(GitlabSchema).to receive(:execute).exactly(:twice) + + post :execute, params: { query: query, operationName: 'IntrospectionQuery' } + post :execute, params: { query: query, operationName: 'IntrospectionQuery' } + end end it 'hits the cache even if the whitespace in the query differs' do diff --git a/spec/graphql/graphql_triggers_spec.rb b/spec/graphql/graphql_triggers_spec.rb index a8a37289ddd..f59ce805fcd 100644 --- a/spec/graphql/graphql_triggers_spec.rb +++ b/spec/graphql/graphql_triggers_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' RSpec.describe GraphqlTriggers, feature_category: :shared do - let_it_be(:issuable, refind: true) { create(:work_item) } + let_it_be(:project) { create(:project) } + let_it_be(:issuable, refind: true) { create(:work_item, project: project) } describe '.issuable_assignees_updated' do let(:assignees) { create_list(:user, 2) } @@ -144,4 +145,31 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do GraphqlTriggers.merge_request_approval_state_updated(merge_request) end end + + describe '.work_item_updated' do + it 'triggers the work_item_updated subscription' do + expect(GitlabSchema.subscriptions).to receive(:trigger).with( + 'workItemUpdated', + { work_item_id: issuable.to_gid }, + issuable + ).and_call_original + + GraphqlTriggers.work_item_updated(issuable) + end + + context 'when triggered with an Issue' do + it 'triggers the subscription with a work item' do + issue = create(:issue, project: project) + work_item = WorkItem.find(issue.id) + + expect(GitlabSchema.subscriptions).to receive(:trigger).with( + 'workItemUpdated', + { work_item_id: work_item.to_gid }, + work_item + ).and_call_original + + GraphqlTriggers.work_item_updated(issue) + end + end + end end diff --git a/spec/graphql/types/subscription_type_spec.rb b/spec/graphql/types/subscription_type_spec.rb index a57a8e751ac..d3e5b6ffa3a 100644 --- a/spec/graphql/types/subscription_type_spec.rb +++ b/spec/graphql/types/subscription_type_spec.rb @@ -15,6 +15,7 @@ RSpec.describe GitlabSchema.types['Subscription'] do merge_request_reviewers_updated merge_request_merge_status_updated merge_request_approval_state_updated + work_item_updated ] expect(described_class).to include_graphql_fields(*expected_fields) diff --git a/spec/lib/api/entities/merge_request_basic_spec.rb b/spec/lib/api/entities/merge_request_basic_spec.rb index 33f8a806c50..89e19f8529e 100644 --- a/spec/lib/api/entities/merge_request_basic_spec.rb +++ b/spec/lib/api/entities/merge_request_basic_spec.rb @@ -21,7 +21,7 @@ RSpec.describe ::API::Entities::MergeRequestBasic do merged_by merge_user merged_at closed_by closed_at target_branch user_notes_count upvotes downvotes author assignees assignee reviewers source_project_id target_project_id labels draft work_in_progress milestone merge_when_pipeline_succeeds merge_status detailed_merge_status sha merge_commit_sha - squash_commit_sha discussion_locked should_remove_source_branch force_remove_source_branch + squash_commit_sha discussion_locked should_remove_source_branch force_remove_source_branch prepared_at reference references web_url time_stats squash task_completion_status has_conflicts blocking_discussions_resolved ] diff --git a/spec/lib/gitlab/avatar_cache_spec.rb b/spec/lib/gitlab/avatar_cache_spec.rb index db2fcf9d140..35ba767a5c5 100644 --- a/spec/lib/gitlab/avatar_cache_spec.rb +++ b/spec/lib/gitlab/avatar_cache_spec.rb @@ -103,7 +103,7 @@ RSpec.describe Gitlab::AvatarCache, :clean_gitlab_redis_cache do context 'when deleting over 1000 emails' do it 'deletes in batches of 1000' do Gitlab::Redis::Cache.with do |redis| - expect(redis).to receive(:pipelined).twice.and_call_original + expect(redis).to receive(:pipelined).at_least(2).and_call_original end described_class.delete_by_email(*(Array.new(1001) { |i| i })) diff --git a/spec/lib/gitlab/database/partitioning/list/convert_table_spec.rb b/spec/lib/gitlab/database/partitioning/list/convert_table_spec.rb index 8e2a53ea76f..b30501cce21 100644 --- a/spec/lib/gitlab/database/partitioning/list/convert_table_spec.rb +++ b/spec/lib/gitlab/database/partitioning/list/convert_table_spec.rb @@ -15,8 +15,7 @@ RSpec.describe Gitlab::Database::Partitioning::List::ConvertTable, feature_categ table_name: table_name, partitioning_column: partitioning_column, parent_table_name: parent_table_name, - zero_partition_value: partitioning_default, - lock_tables: lock_tables + zero_partition_value: partitioning_default ) end @@ -227,16 +226,6 @@ RSpec.describe Gitlab::Database::Partitioning::List::ConvertTable, feature_categ end end - context 'with locking tables' do - let(:lock_tables) { [table_name] } - - it 'locks the table' do - recorder = ActiveRecord::QueryRecorder.new { partition } - - expect(recorder.log).to include(/LOCK "_test_table_to_partition" IN ACCESS EXCLUSIVE MODE/) - end - end - context 'when an error occurs during the conversion' do before do # Set up the fault that we'd like to inject @@ -264,7 +253,6 @@ RSpec.describe Gitlab::Database::Partitioning::List::ConvertTable, feature_categ with_them do it 'recovers from a fault', :aggregate_failures do expect { converter.partition }.to raise_error(/fault/) - expect(Gitlab::Database::PostgresPartition.for_parent_table(parent_table_name).count).to eq(0) expect { converter.partition }.not_to raise_error expect(Gitlab::Database::PostgresPartition.for_parent_table(parent_table_name).count).to eq(1) @@ -286,26 +274,6 @@ RSpec.describe Gitlab::Database::Partitioning::List::ConvertTable, feature_categ expect(migration_context.has_loose_foreign_key?(table_name)).to be_truthy expect(migration_context.has_loose_foreign_key?(parent_table_name)).to be_truthy end - - context 'with locking tables' do - let(:lock_tables) { [table_name] } - - it 'locks the table before dropping the triggers' do - recorder = ActiveRecord::QueryRecorder.new { partition } - - lock_index = recorder.log.find_index do |log| - log.start_with?('LOCK "_test_table_to_partition" IN ACCESS EXCLUSIVE MODE') - end - - trigger_index = recorder.log.find_index do |log| - log.start_with?('DROP TRIGGER IF EXISTS _test_table_to_partition_loose_fk_trigger') - end - - expect(lock_index).to be_present - expect(trigger_index).to be_present - expect(lock_index).to be < trigger_index - end - end end end diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb index 571c67db597..6a947044317 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb @@ -68,7 +68,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe describe '#convert_table_to_first_list_partition' do it_behaves_like 'delegates to ConvertTable' do let(:lock_tables) { [source_table] } - let(:extra_options) { { lock_tables: lock_tables } } let(:expected_method) { :partition } let(:migrate) do migration.convert_table_to_first_list_partition(table_name: source_table, diff --git a/spec/lib/gitlab/instrumentation/redis_spec.rb b/spec/lib/gitlab/instrumentation/redis_spec.rb index 3e02eadba4b..1b7774bc229 100644 --- a/spec/lib/gitlab/instrumentation/redis_spec.rb +++ b/spec/lib/gitlab/instrumentation/redis_spec.rb @@ -35,13 +35,13 @@ RSpec.describe Gitlab::Instrumentation::Redis do # will be an extra SELECT command to choose the right database. We # don't want to make the spec less precise, so we force that to # happen (if needed) first, then clear the counts. - Gitlab::Redis::Cache.with { |redis| redis.info } + Gitlab::Redis::Sessions.with { |redis| redis.info } RequestStore.clear! stub_rails_env('staging') # to avoid raising CrossSlotError - Gitlab::Redis::Cache.with { |redis| redis.mset('cache-test', 321, 'cache-test-2', 321) } + Gitlab::Redis::Sessions.with { |redis| redis.mset('cache-test', 321, 'cache-test-2', 321) } Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do - Gitlab::Redis::Cache.with { |redis| redis.mget('cache-test', 'cache-test-2') } + Gitlab::Redis::Sessions.with { |redis| redis.mget('cache-test', 'cache-test-2') } end Gitlab::Redis::SharedState.with { |redis| redis.set('shared-state-test', 123) } end @@ -56,13 +56,13 @@ RSpec.describe Gitlab::Instrumentation::Redis do redis_read_bytes: be >= 0, redis_write_bytes: be >= 0, - # Cache results - redis_cache_calls: 2, - redis_cache_cross_slot_calls: 1, - redis_cache_allowed_cross_slot_calls: 1, - redis_cache_duration_s: be >= 0, - redis_cache_read_bytes: be >= 0, - redis_cache_write_bytes: be >= 0, + # Queues results + redis_sessions_calls: 2, + redis_sessions_cross_slot_calls: 1, + redis_sessions_allowed_cross_slot_calls: 1, + redis_sessions_duration_s: be >= 0, + redis_sessions_read_bytes: be >= 0, + redis_sessions_write_bytes: be >= 0, # Shared state results redis_shared_state_calls: 1, diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb index 8a88328e0c1..698c8a37d48 100644 --- a/spec/lib/gitlab/instrumentation_helper_spec.rb +++ b/spec/lib/gitlab/instrumentation_helper_spec.rb @@ -41,9 +41,9 @@ RSpec.describe Gitlab::InstrumentationHelper, :clean_gitlab_redis_repository_cac context 'when Redis calls are made' do it 'adds Redis data and omits Gitaly data' do stub_rails_env('staging') # to avoid raising CrossSlotError - Gitlab::Redis::Cache.with { |redis| redis.mset('test-cache', 123, 'test-cache2', 123) } + Gitlab::Redis::Sessions.with { |redis| redis.mset('test-cache', 123, 'test-cache2', 123) } Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do - Gitlab::Redis::Cache.with { |redis| redis.mget('cache-test', 'cache-test-2') } + Gitlab::Redis::Sessions.with { |redis| redis.mget('cache-test', 'cache-test-2') } end Gitlab::Redis::Queues.with { |redis| redis.set('test-queues', 321) } @@ -63,13 +63,13 @@ RSpec.describe Gitlab::InstrumentationHelper, :clean_gitlab_redis_repository_cac expect(payload[:redis_queues_read_bytes]).to be >= 0 expect(payload[:redis_queues_write_bytes]).to be >= 0 - # Cache payload - expect(payload[:redis_cache_calls]).to eq(2) - expect(payload[:redis_cache_cross_slot_calls]).to eq(1) - expect(payload[:redis_cache_allowed_cross_slot_calls]).to eq(1) - expect(payload[:redis_cache_duration_s]).to be >= 0 - expect(payload[:redis_cache_read_bytes]).to be >= 0 - expect(payload[:redis_cache_write_bytes]).to be >= 0 + # Sessions payload + expect(payload[:redis_sessions_calls]).to eq(2) + expect(payload[:redis_sessions_cross_slot_calls]).to eq(1) + expect(payload[:redis_sessions_allowed_cross_slot_calls]).to eq(1) + expect(payload[:redis_sessions_duration_s]).to be >= 0 + expect(payload[:redis_sessions_read_bytes]).to be >= 0 + expect(payload[:redis_sessions_write_bytes]).to be >= 0 # Gitaly expect(payload[:gitaly_calls]).to be_nil diff --git a/spec/lib/gitlab/omniauth_initializer_spec.rb b/spec/lib/gitlab/omniauth_initializer_spec.rb index 112fdb183ab..1c665ec6e18 100644 --- a/spec/lib/gitlab/omniauth_initializer_spec.rb +++ b/spec/lib/gitlab/omniauth_initializer_spec.rb @@ -216,6 +216,14 @@ RSpec.describe Gitlab::OmniauthInitializer do expect { subject.execute([hash_config]) }.to raise_error(NameError) end + it 'configures fail_with_empty_uid for shibboleth' do + shibboleth_config = { 'name' => 'shibboleth', 'args' => {} } + + expect(devise_config).to receive(:omniauth).with(:shibboleth, { fail_with_empty_uid: true }) + + subject.execute([shibboleth_config]) + end + it 'configures defaults for google_oauth2' do google_config = { 'name' => 'google_oauth2', diff --git a/spec/lib/gitlab/patch/redis_cache_store_spec.rb b/spec/lib/gitlab/patch/redis_cache_store_spec.rb index fdb220276a4..1b3813d03a2 100644 --- a/spec/lib/gitlab/patch/redis_cache_store_spec.rb +++ b/spec/lib/gitlab/patch/redis_cache_store_spec.rb @@ -16,7 +16,11 @@ RSpec.describe Gitlab::Patch::RedisCacheStore, :use_clean_rails_redis_caching, f describe '#read_multi_mget' do it 'runs multi-key command if no cross-slot command is expected' do Rails.cache.redis.with do |redis| - expect(redis).not_to receive(:pipelined) + if Gitlab::Redis::ClusterUtil.cluster?(redis) + expect(redis).to receive(:pipelined).once.and_call_original + else + expect(redis).not_to receive(:pipelined) + end end expect( @@ -24,13 +28,15 @@ RSpec.describe Gitlab::Patch::RedisCacheStore, :use_clean_rails_redis_caching, f ).to eq({ '{user1}:x' => 1, '{user1}:y' => 2, '{user1}:z' => 3 }) end - it 'skips patch if input is above 100' do - Rails.cache.redis.with do |redis| - expect(redis).not_to receive(:pipelined) - end + context 'when deleting large amount of keys' do + it 'batches get into pipelines of 100' do + Rails.cache.redis.with do |redis| + expect(redis).to receive(:pipelined).at_least(2).and_call_original + end - Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do - Rails.cache.read_multi(*Array.new(101) { |i| i }) + Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do + Rails.cache.read_multi(*Array.new(101) { |i| i }) + end end end @@ -38,19 +44,23 @@ RSpec.describe Gitlab::Patch::RedisCacheStore, :use_clean_rails_redis_caching, f it 'reads multiple keys' do if patched Rails.cache.redis.with do |redis| - expect(redis).to receive(:pipelined).once.and_call_original + expect(redis).to receive(:pipelined).at_least(1).and_call_original end end - expect(::Feature).to receive(:enabled?) - .with(:feature_flag_state_logs, { default_enabled_if_undefined: nil, type: :ops }) - .exactly(:once) - .and_call_original - - expect(::Feature).to receive(:enabled?) - .with(:enable_rails_cache_pipeline_patch) - .exactly(:once) - .and_call_original + Gitlab::Redis::Cache.with do |redis| + unless Gitlab::Redis::ClusterUtil.cluster?(redis) + expect(::Feature).to receive(:enabled?) + .with(:feature_flag_state_logs, { default_enabled_if_undefined: nil, type: :ops }) + .exactly(:once) + .and_call_original + + expect(::Feature).to receive(:enabled?) + .with(:enable_rails_cache_pipeline_patch) + .exactly(:once) + .and_call_original + end + end expect( Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do @@ -65,7 +75,7 @@ RSpec.describe Gitlab::Patch::RedisCacheStore, :use_clean_rails_redis_caching, f shared_examples 'reading using non redis cache stores' do |klass| it 'does not affect non Redis::Cache cache stores' do klass.cache_store.redis.with do |redis| - expect(redis).not_to receive(:pipelined) + expect(redis).not_to receive(:pipelined) unless Gitlab::Redis::ClusterUtil.cluster?(redis) end Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do @@ -75,8 +85,8 @@ RSpec.describe Gitlab::Patch::RedisCacheStore, :use_clean_rails_redis_caching, f end context 'when reading from non redis-cache stores' do - it_behaves_like 'reading using non redis cache stores', Gitlab::Redis::RepositoryCache it_behaves_like 'reading using non redis cache stores', Gitlab::Redis::FeatureFlag + it_behaves_like 'reading using non redis cache stores', Gitlab::Redis::RepositoryCache end context 'when feature flag is disabled' do @@ -95,19 +105,23 @@ RSpec.describe Gitlab::Patch::RedisCacheStore, :use_clean_rails_redis_caching, f it 'deletes multiple keys' do if patched Rails.cache.redis.with do |redis| - expect(redis).to receive(:pipelined).once.and_call_original + expect(redis).to receive(:pipelined).at_least(1).and_call_original end end - expect(::Feature).to receive(:enabled?) - .with(:feature_flag_state_logs, { default_enabled_if_undefined: nil, type: :ops }) - .exactly(:once) - .and_call_original - - expect(::Feature).to receive(:enabled?) - .with(:enable_rails_cache_pipeline_patch) - .exactly(:once) - .and_call_original + Gitlab::Redis::Cache.with do |redis| + unless Gitlab::Redis::ClusterUtil.cluster?(redis) + expect(::Feature).to receive(:enabled?) + .with(:feature_flag_state_logs, { default_enabled_if_undefined: nil, type: :ops }) + .exactly(:once) + .and_call_original + + expect(::Feature).to receive(:enabled?) + .with(:enable_rails_cache_pipeline_patch) + .exactly(:once) + .and_call_original + end + end expect( Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do @@ -120,7 +134,7 @@ RSpec.describe Gitlab::Patch::RedisCacheStore, :use_clean_rails_redis_caching, f shared_examples 'deleting using non redis cache stores' do |klass| it 'does not affect non Redis::Cache cache stores' do klass.cache_store.redis.with do |redis| - expect(redis).not_to receive(:pipelined) + expect(redis).not_to receive(:pipelined) unless Gitlab::Redis::ClusterUtil.cluster?(redis) end Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do @@ -130,8 +144,8 @@ RSpec.describe Gitlab::Patch::RedisCacheStore, :use_clean_rails_redis_caching, f end context 'when deleting from non redis-cache stores' do - it_behaves_like 'deleting using non redis cache stores', Gitlab::Redis::RepositoryCache it_behaves_like 'deleting using non redis cache stores', Gitlab::Redis::FeatureFlag + it_behaves_like 'deleting using non redis cache stores', Gitlab::Redis::RepositoryCache end context 'when deleting large amount of keys' do @@ -141,7 +155,9 @@ RSpec.describe Gitlab::Patch::RedisCacheStore, :use_clean_rails_redis_caching, f it 'calls pipeline multiple times' do Rails.cache.redis.with do |redis| - expect(redis).to receive(:pipelined).twice.and_call_original + # no expectation on number of times as it could vary depending on cluster size + # if the Redis is a Redis Cluster + expect(redis).to receive(:pipelined).at_least(2).and_call_original end expect( @@ -154,7 +170,11 @@ RSpec.describe Gitlab::Patch::RedisCacheStore, :use_clean_rails_redis_caching, f it 'runs multi-key command if no cross-slot command is expected' do Rails.cache.redis.with do |redis| - expect(redis).not_to receive(:pipelined) + if Gitlab::Redis::ClusterUtil.cluster?(redis) + expect(redis).to receive(:pipelined).once.and_call_original + else + expect(redis).not_to receive(:pipelined) + end end expect( diff --git a/spec/lib/gitlab/reactive_cache_set_cache_spec.rb b/spec/lib/gitlab/reactive_cache_set_cache_spec.rb index 1a71b23533e..9956078999f 100644 --- a/spec/lib/gitlab/reactive_cache_set_cache_spec.rb +++ b/spec/lib/gitlab/reactive_cache_set_cache_spec.rb @@ -75,7 +75,7 @@ RSpec.describe Gitlab::ReactiveCacheSetCache, :clean_gitlab_redis_cache do it 'sends multiple pipelines of 1000 unlinks' do Gitlab::Redis::Cache.with do |redis| - expect(redis).to receive(:pipelined).twice.and_call_original + expect(redis).to receive(:pipelined).at_least(2).and_call_original end cache.clear_cache!(cache_prefix) diff --git a/spec/lib/gitlab/redis/cluster_util_spec.rb b/spec/lib/gitlab/redis/cluster_util_spec.rb new file mode 100644 index 00000000000..3993004518d --- /dev/null +++ b/spec/lib/gitlab/redis/cluster_util_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Redis::ClusterUtil, feature_category: :scalability do + using RSpec::Parameterized::TableSyntax + + describe '.cluster?' do + context 'when MultiStore' do + let(:redis_cluster) { instance_double(::Redis::Cluster) } + + where(:pri_store, :sec_store, :expected_val) do + :cluster | :cluster | true + :cluster | :single | true + :single | :cluster | true + :single | :single | false + end + + before do + # stub all initialiser steps in Redis::Cluster.new to avoid connecting to a Redis Cluster node + allow(::Redis::Cluster).to receive(:new).and_return(redis_cluster) + allow(redis_cluster).to receive(:is_a?).with(::Redis::Cluster).and_return(true) + allow(redis_cluster).to receive(:id).and_return(1) + + allow(Gitlab::Redis::MultiStore).to receive(:same_redis_store?).and_return(false) + skip_feature_flags_yaml_validation + skip_default_enabled_yaml_check + end + + with_them do + it 'returns expected value' do + primary_store = pri_store == :cluster ? ::Redis.new(cluster: ['redis://localhost:6000']) : ::Redis.new + secondary_store = sec_store == :cluster ? ::Redis.new(cluster: ['redis://localhost:6000']) : ::Redis.new + multistore = Gitlab::Redis::MultiStore.new(primary_store, secondary_store, 'teststore') + expect(described_class.cluster?(multistore)).to eq(expected_val) + end + end + end + + context 'when is not Redis::Cluster' do + it 'returns false' do + expect(described_class.cluster?(::Redis.new)).to be_falsey + end + end + + context 'when is Redis::Cluster' do + let(:redis_cluster) { instance_double(::Redis::Cluster) } + + before do + # stub all initialiser steps in Redis::Cluster.new to avoid connecting to a Redis Cluster node + allow(::Redis::Cluster).to receive(:new).and_return(redis_cluster) + allow(redis_cluster).to receive(:is_a?).with(::Redis::Cluster).and_return(true) + end + + it 'returns true' do + expect(described_class.cluster?(::Redis.new(cluster: ['redis://localhost:6000']))).to be_truthy + end + end + end +end diff --git a/spec/lib/gitlab/redis/cross_slot_spec.rb b/spec/lib/gitlab/redis/cross_slot_spec.rb new file mode 100644 index 00000000000..b3eac4357e8 --- /dev/null +++ b/spec/lib/gitlab/redis/cross_slot_spec.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Redis::CrossSlot, feature_category: :redis do + describe '.pipelined' do + context 'when using redis client' do + before do + Gitlab::Redis::Queues.with { |redis| redis.set('a', 1) } + end + + it 'performs redis-rb pipelined' do + expect(Gitlab::Redis::CrossSlot::Router).not_to receive(:new) + + expect( + Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do + Gitlab::Redis::Queues.with do |redis| + described_class::Pipeline.new(redis).pipelined do |p| + p.get('a') + p.set('b', 1) + end + end + end + ).to eq(%w[1 OK]) + end + end + + context 'when using with MultiStore' do + let(:multistore) do + Gitlab::Redis::MultiStore.new( + ::Redis.new(::Gitlab::Redis::SharedState.params), + ::Redis.new(::Gitlab::Redis::Sessions.params), + 'testing') + end + + before do + Gitlab::Redis::SharedState.with { |redis| redis.set('a', 1) } + Gitlab::Redis::Sessions.with { |redis| redis.set('a', 1) } + skip_feature_flags_yaml_validation + skip_default_enabled_yaml_check + end + + it 'performs multistore pipelined' do + expect(Gitlab::Redis::CrossSlot::Router).not_to receive(:new) + + expect( + Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do + described_class::Pipeline.new(multistore).pipelined do |p| + p.get('a') + p.set('b', 1) + end + end + ).to eq(%w[1 OK]) + end + end + + context 'when using Redis::Cluster' do + # Only stub redis client internals since the CI pipeline does not run a Redis Cluster + let(:redis) { double(:redis) } # rubocop:disable RSpec/VerifiedDoubles + let(:client) { double(:client) } # rubocop:disable RSpec/VerifiedDoubles + let(:pipeline) { double(:pipeline) } # rubocop:disable RSpec/VerifiedDoubles + + let(:arguments) { %w[a b c d] } + + subject do + described_class::Pipeline.new(redis).pipelined do |p| + arguments.each { |key| p.get(key) } + end + end + + before do + allow(redis).to receive(:_client).and_return(client) + allow(redis).to receive(:pipelined).and_yield(pipeline) + allow(client).to receive(:instance_of?).with(::Redis::Cluster).and_return(true) + end + + it 'fan-out and fan-in commands to separate shards' do + # simulate fan-out to 3 shards with random order + expect(client).to receive(:_find_node_key).exactly(4).times.and_return(3, 2, 1, 3) + + arguments.each do |key| + f = double('future') # rubocop:disable RSpec/VerifiedDoubles + expect(pipeline).to receive(:get).with(key).and_return(f) + expect(f).to receive(:value).and_return(key) + end + + expect(subject).to eq(arguments) + end + + shared_examples 'fallback on cross-slot' do |redirection| + context 'when redis cluster undergoing slot migration' do + before do + allow(pipeline).to receive(:get).and_raise(::Redis::CommandError.new("#{redirection} 1 127.0.0.1:7001")) + end + + it 'logs error and executes sequentially' do + expect(client).to receive(:_find_node_key).exactly(4).times.and_return(3, 2, 1, 3) + expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(::Redis::CommandError)) + + arguments.each do |key| + expect(redis).to receive(:get).with(key).and_return(key) + end + + subject + end + end + end + + it_behaves_like 'fallback on cross-slot', 'MOVED' + it_behaves_like 'fallback on cross-slot', 'ASK' + + context 'when receiving non-MOVED/ASK command errors' do + before do + allow(pipeline).to receive(:get).and_raise(::Redis::CommandError.new) + allow(client).to receive(:_find_node_key).exactly(4).times.and_return(3, 2, 1, 3) + end + + it 'raises error' do + expect { subject }.to raise_error(::Redis::CommandError) + end + end + end + end +end diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb index e45c29a9dd2..75e6e2db6e7 100644 --- a/spec/lib/gitlab/redis/multi_store_spec.rb +++ b/spec/lib/gitlab/redis/multi_store_spec.rb @@ -777,6 +777,25 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do end end end + + context 'when either store is a an instance of ::Redis::Cluster' do + before do + client = double + allow(client).to receive(:instance_of?).with(::Redis::Cluster).and_return(true) + allow(primary_store).to receive(:_client).and_return(client) + end + + it 'calls cross-slot pipeline within multistore' do + if name == :pipelined + # we intentionally exclude `.and_call_original` since primary_store/secondary_store + # may not be running on a proper Redis Cluster. + expect(Gitlab::Redis::CrossSlot::Pipeline).to receive(:new).with(primary_store).exactly(:once) + expect(Gitlab::Redis::CrossSlot::Pipeline).not_to receive(:new).with(secondary_store) + end + + subject + end + end end end diff --git a/spec/lib/gitlab/repository_cache/preloader_spec.rb b/spec/lib/gitlab/repository_cache/preloader_spec.rb index e6fb0da6412..44d7d0e1db1 100644 --- a/spec/lib/gitlab/repository_cache/preloader_spec.rb +++ b/spec/lib/gitlab/repository_cache/preloader_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::RepositoryCache::Preloader, :use_clean_rails_redis_caching, +RSpec.describe Gitlab::RepositoryCache::Preloader, :use_clean_rails_repository_cache_store_caching, feature_category: :source_code_management do let(:projects) { create_list(:project, 2, :repository) } let(:repositories) { projects.map(&:repository) } diff --git a/spec/lib/gitlab/repository_hash_cache_spec.rb b/spec/lib/gitlab/repository_hash_cache_spec.rb index 6b52c315a70..e3cc6ed69fb 100644 --- a/spec/lib/gitlab/repository_hash_cache_spec.rb +++ b/spec/lib/gitlab/repository_hash_cache_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -RSpec.describe Gitlab::RepositoryHashCache, :clean_gitlab_redis_cache do +RSpec.describe Gitlab::RepositoryHashCache, :clean_gitlab_redis_repository_cache, feature_category: :source_code_management do let_it_be(:project) { create(:project) } let(:repository) { project.repository } diff --git a/spec/lib/gitlab/repository_set_cache_spec.rb b/spec/lib/gitlab/repository_set_cache_spec.rb index df980b50d03..dc2081fe3a9 100644 --- a/spec/lib/gitlab/repository_set_cache_spec.rb +++ b/spec/lib/gitlab/repository_set_cache_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do +RSpec.describe Gitlab::RepositorySetCache, :clean_gitlab_redis_repository_cache, feature_category: :source_code_management do let_it_be(:project) { create(:project) } let(:repository) { project.repository } @@ -59,8 +59,13 @@ RSpec.describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do it 'writes the value to the cache' do write_cache - redis_keys = Gitlab::Redis::Cache.with { |redis| redis.scan(0, match: "*") }.last - expect(redis_keys).to include("#{gitlab_cache_namespace}:branch_names:#{namespace}:set") + cursor, redis_keys = Gitlab::Redis::RepositoryCache.with { |redis| redis.scan(0, match: "*") } + while cursor != "0" + cursor, keys = Gitlab::Redis::RepositoryCache.with { |redis| redis.scan(cursor, match: "*") } + redis_keys << keys + end + + expect(redis_keys.flatten).to include("#{gitlab_cache_namespace}:branch_names:#{namespace}:set") expect(cache.fetch('branch_names')).to contain_exactly('main') end @@ -120,7 +125,7 @@ RSpec.describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do context 'when deleting over 1000 keys' do it 'deletes in batches of 1000' do Gitlab::Redis::RepositoryCache.with do |redis| - expect(redis).to receive(:pipelined).twice.and_call_original + expect(redis).to receive(:pipelined).at_least(2).and_call_original end cache.expire(*(Array.new(1001) { |i| i })) diff --git a/spec/models/plan_limits_spec.rb b/spec/models/plan_limits_spec.rb index e1f441405ef..58abd153d28 100644 --- a/spec/models/plan_limits_spec.rb +++ b/spec/models/plan_limits_spec.rb @@ -211,11 +211,8 @@ RSpec.describe PlanLimits do ] end - # Remove ci_active_pipelines when db column is removed - # https://gitlab.com/gitlab-org/gitlab/-/issues/408141 let(:columns_with_zero) do %w[ - ci_active_pipelines ci_pipeline_size ci_active_jobs storage_size_limit diff --git a/spec/requests/api/graphql/subscriptions/work_item_updated_spec.rb b/spec/requests/api/graphql/subscriptions/work_item_updated_spec.rb new file mode 100644 index 00000000000..6c0962e7ec0 --- /dev/null +++ b/spec/requests/api/graphql/subscriptions/work_item_updated_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Subscriptions::WorkItemUpdated, feature_category: :team_planning do + include GraphqlHelpers + include Graphql::Subscriptions::WorkItems::Helper + + let_it_be(:reporter) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:task) { create(:work_item, :task, project: project) } + + let(:current_user) { nil } + let(:subscribe) { work_item_subscription('workItemUpdated', task, current_user) } + let(:updated_work_item) { graphql_dig_at(graphql_data(response[:result]), :workItemUpdated) } + + before do + stub_const('GitlabSchema', Graphql::Subscriptions::ActionCable::MockGitlabSchema) + Graphql::Subscriptions::ActionCable::MockActionCable.clear_mocks + project.add_reporter(reporter) + end + + subject(:response) do + subscription_response do + GraphqlTriggers.work_item_updated(task) + end + end + + context 'when user is unauthorized' do + it 'does not receive any data' do + expect(response).to be_nil + end + end + + context 'when user is authorized' do + let(:current_user) { reporter } + + it 'receives updated work_item data' do + expect(updated_work_item['id']).to eq(task.to_gid.to_s) + expect(updated_work_item['iid']).to eq(task.iid.to_s) + end + end +end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index f96fbf54f08..ea4e582b239 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -104,6 +104,10 @@ RSpec.describe Issues::UpdateService, :mailer, feature_category: :team_planning expect(issue.issue_customer_relations_contacts.last.contact).to eq contact end + it_behaves_like 'update service that triggers GraphQL work_item_updated subscription' do + subject(:execute_service) { update_issue(opts) } + end + context 'when updating milestone' do before do update_issue({ milestone_id: nil }) diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb index 6a56e1a72e6..30c16458353 100644 --- a/spec/services/work_items/update_service_spec.rb +++ b/spec/services/work_items/update_service_spec.rb @@ -81,6 +81,10 @@ RSpec.describe WorkItems::UpdateService, feature_category: :team_planning do let(:user) { current_user } subject(:service_action) { update_work_item[:status] } end + + it_behaves_like 'update service that triggers GraphQL work_item_updated subscription' do + subject(:execute_service) { update_work_item } + end end context 'when title is not changed' do @@ -107,6 +111,10 @@ RSpec.describe WorkItems::UpdateService, feature_category: :team_planning do update_work_item end + + it_behaves_like 'update service that triggers GraphQL work_item_updated subscription' do + subject(:execute_service) { update_work_item } + end end context 'when decription is changed' do @@ -117,6 +125,10 @@ RSpec.describe WorkItems::UpdateService, feature_category: :team_planning do update_work_item end + + it_behaves_like 'update service that triggers GraphQL work_item_updated subscription' do + subject(:execute_service) { update_work_item } + end end context 'when decription is not changed' do @@ -219,6 +231,10 @@ RSpec.describe WorkItems::UpdateService, feature_category: :team_planning do expect(work_item.description).to eq('changed') end + it_behaves_like 'update service that triggers GraphQL work_item_updated subscription' do + subject(:execute_service) { update_work_item } + end + context 'with mentions', :mailer, :sidekiq_might_not_need_inline do shared_examples 'creates the todo and sends email' do |attribute| it 'creates a todo and sends email' do @@ -298,6 +314,10 @@ RSpec.describe WorkItems::UpdateService, feature_category: :team_planning do expect(work_item.work_item_children).to include(child_work_item) end + it_behaves_like 'update service that triggers GraphQL work_item_updated subscription' do + subject(:execute_service) { update_work_item } + end + context 'when child type is invalid' do let_it_be(:child_work_item) { create(:work_item, project: project) } @@ -344,6 +364,10 @@ RSpec.describe WorkItems::UpdateService, feature_category: :team_planning do update_work_item end + + it_behaves_like 'update service that triggers GraphQL work_item_updated subscription' do + subject(:execute_service) { update_work_item } + end end context 'when milestone remains unchanged' do @@ -375,6 +399,10 @@ RSpec.describe WorkItems::UpdateService, feature_category: :team_planning do update_work_item end + it_behaves_like 'update service that triggers GraphQL work_item_updated subscription' do + subject(:execute_service) { update_work_item } + end + it_behaves_like 'broadcasting issuable labels updates' do let(:issuable) { work_item } let(:label_a) { label1 } diff --git a/spec/support/caching.rb b/spec/support/caching.rb index b18223523db..119c521f732 100644 --- a/spec/support/caching.rb +++ b/spec/support/caching.rb @@ -36,6 +36,19 @@ RSpec.configure do |config| Rails.cache = original_null_store end + config.around(:each, :use_clean_rails_repository_cache_store_caching) do |example| + original_null_store = Rails.cache + Rails.cache = Gitlab::Redis::RepositoryCache.cache_store + + redis_repository_cache_cleanup! + + example.run + + redis_repository_cache_cleanup! + + Rails.cache = original_null_store + end + config.around(:each, :use_sql_query_cache) do |example| base_models = Gitlab::Database.database_base_models_with_gitlab_shared.values inner_proc = proc { example.run } diff --git a/spec/support/helpers/graphql/subscriptions/work_items/helper.rb b/spec/support/helpers/graphql/subscriptions/work_items/helper.rb new file mode 100644 index 00000000000..9e5817c4134 --- /dev/null +++ b/spec/support/helpers/graphql/subscriptions/work_items/helper.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Graphql + module Subscriptions + module WorkItems + module Helper + def subscription_response + subscription_channel = subscribe + yield + subscription_channel.mock_broadcasted_messages.first + end + + def work_item_subscription(name, work_item, current_user) + mock_channel = Graphql::Subscriptions::ActionCable::MockActionCable.get_mock_channel + + query = case name + when 'workItemUpdated' + work_item_updated_subscription_query(name, work_item) + else + raise "Subscription query unknown: #{name}" + end + + GitlabSchema.execute(query, context: { current_user: current_user, channel: mock_channel }) + + mock_channel + end + + def note_subscription(name, work_item, current_user) + mock_channel = Graphql::Subscriptions::ActionCable::MockActionCable.get_mock_channel + + query = <<~SUBSCRIPTION + subscription { + #{name}(workItemId: \"#{work_item.to_gid}\") { + id + iid + } + } + SUBSCRIPTION + + GitlabSchema.execute(query, context: { current_user: current_user, channel: mock_channel }) + + mock_channel + end + + private + + def work_item_updated_subscription_query(name, work_item) + <<~SUBSCRIPTION + subscription { + #{name}(workItemId: \"#{work_item.to_gid}\") { + id + iid + } + } + SUBSCRIPTION + end + end + end + end +end diff --git a/spec/support/helpers/models/ci/partitioning_testing/schema_helpers.rb b/spec/support/helpers/models/ci/partitioning_testing/schema_helpers.rb index 4107bbcb976..849d9ea117e 100644 --- a/spec/support/helpers/models/ci/partitioning_testing/schema_helpers.rb +++ b/spec/support/helpers/models/ci/partitioning_testing/schema_helpers.rb @@ -67,8 +67,10 @@ module Ci def drop_test_partition(table_name, connection:) return unless table_available?(table_name, connection: connection) + return unless connection.table_exists?(full_partition_name(table_name)) connection.execute(<<~SQL.squish) + ALTER TABLE #{table_name} DETACH PARTITION #{full_partition_name(table_name)}; DROP TABLE IF EXISTS #{full_partition_name(table_name)}; SQL end diff --git a/spec/support/shared_contexts/lib/gitlab/database/partitioning/list_partitioning_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/database/partitioning/list_partitioning_shared_context.rb index e9cd1bdbbf5..3d978a6fde4 100644 --- a/spec/support/shared_contexts/lib/gitlab/database/partitioning/list_partitioning_shared_context.rb +++ b/spec/support/shared_contexts/lib/gitlab/database/partitioning/list_partitioning_shared_context.rb @@ -19,7 +19,6 @@ RSpec.shared_context 'with a table structure for converting a table to a list pa let(:other_referencing_table_name) { '_test_other_referencing_table' } let(:parent_table_name) { "#{table_name}_parent" } let(:parent_table_identifier) { "#{connection.current_schema}.#{parent_table_name}" } - let(:lock_tables) { [] } let(:model) { define_batchable_model(table_name, connection: connection) } diff --git a/spec/support/shared_examples/work_items/update_service_shared_examples.rb b/spec/support/shared_examples/work_items/update_service_shared_examples.rb new file mode 100644 index 00000000000..2d220c0ef58 --- /dev/null +++ b/spec/support/shared_examples/work_items/update_service_shared_examples.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'update service that triggers GraphQL work_item_updated subscription' do + let(:update_subject) do + if defined?(work_item) + work_item + elsif defined?(issue) + issue + end + end + + it 'triggers graphql subscription workItemUpdated' do + expect(GraphqlTriggers).to receive(:work_item_updated).with(update_subject).and_call_original + + execute_service + end +end diff --git a/spec/tasks/cache_rake_spec.rb b/spec/tasks/cache_rake_spec.rb index 375d01bf2ba..046f8b107f2 100644 --- a/spec/tasks/cache_rake_spec.rb +++ b/spec/tasks/cache_rake_spec.rb @@ -53,6 +53,12 @@ RSpec.describe 'clearing redis cache', :clean_gitlab_redis_repository_cache, :cl end def redis_keys - Gitlab::Redis::Cache.with { |redis| redis.scan(0, match: "*") }.last + # multiple scans to look across different shards if cache is using a Redis Cluster + cursor, scanned_keys = Gitlab::Redis::Cache.with { |redis| redis.scan(0, match: "*") } + while cursor != "0" + cursor, keys = Gitlab::Redis::Cache.with { |redis| redis.scan(cursor, match: "*") } + scanned_keys << keys + end + scanned_keys.flatten end end |