diff --git a/config/initializers/active_record_test_patch.rb b/config/initializers/active_record_test_patch.rb new file mode 100644 index 0000000000000000000000000000000000000000..cb05c7de6018019549925b769170f35f4c0b1f56 --- /dev/null +++ b/config/initializers/active_record_test_patch.rb @@ -0,0 +1,140 @@ +require 'active_record' + +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + def active? + @lock.synchronize do + unless @raw_connection + STDERR.puts "=== current Connection object is #{self.object_id}" + STDERR.puts "=== @raw_connection is empty" + return false + end + + @raw_connection.query ";" + end + true + rescue PG::Error => e + STDERR.puts "=== PG::Error" + STDERR.puts e + false + end + + # Close then reopen the connection. + def reconnect + STDERR.puts "=== PostgreSQL reconnect!" + STDERR.puts caller.join("\n") + + begin + @raw_connection&.reset + rescue PG::ConnectionBad + @raw_connection = nil + end + + connect unless @raw_connection + end + + def reset! + STDERR.puts "=== PostgreSQL reset!" + STDERR.puts caller.join("\n") + + @lock.synchronize do + return connect! unless @raw_connection + + unless @raw_connection.transaction_status == ::PG::PQTRANS_IDLE + @raw_connection.query "ROLLBACK" + end + @raw_connection.query "DISCARD ALL" + + super + end + end + + # Disconnects from the database if already connected. Otherwise, this + # method does nothing. + def disconnect! + STDERR.puts "=== PostgreSQL disconnect!" + STDERR.puts caller.join("\n") + + @lock.synchronize do + super + @raw_connection&.close rescue nil + @raw_connection = nil + end + end + + def discard! # :nodoc: + STDERR.puts "=== PostgreSQL discard!" + STDERR.puts caller.join("\n") + + super + @raw_connection&.socket_io&.reopen(IO::NULL) rescue nil + @raw_connection = nil + end + end + + class ConnectionPool + def try_to_checkout_new_connection + STDERR.puts "=== adding new connection to the pool:" + STDERR.puts caller.join("\n") + + # first in synchronized section check if establishing new conns is allowed + # and increment @now_connecting, to prevent overstepping this pool's @size + # constraint + do_checkout = synchronize do + if @threads_blocking_new_connections.zero? && (@connections.size + @now_connecting) < @size + @now_connecting += 1 + end + end + if do_checkout + begin + # if successfully incremented @now_connecting establish new connection + # outside of synchronized section + conn = checkout_new_connection + ensure + synchronize do + if conn + adopt_connection(conn) + # returned conn needs to be already leased + conn.lease + end + @now_connecting -= 1 + end + end + end + end + end + + class ConnectionHandler + + # Returns any connections in use by the current thread back to the pool, + # and also returns connections to the pool cached by threads that are no + # longer alive. + def clear_active_connections!(role = ActiveRecord::Base.current_role) +# STDERR.puts "=== clear_active_connections! called:" + each_connection_pool(role).each(&:release_connection) + end + + # Clears the cache which maps classes. + # + # See ConnectionPool#clear_reloadable_connections! for details. + def clear_reloadable_connections!(role = ActiveRecord::Base.current_role) + STDERR.puts "=== clear_reloadable_connections! called:" + each_connection_pool(role).each(&:clear_reloadable_connections!) + end + + def clear_all_connections!(role = ActiveRecord::Base.current_role) + STDERR.puts "=== clear_all_connections! called:" + each_connection_pool(role).each(&:disconnect!) + end + + # Disconnects all currently idle connections. + # + # See ConnectionPool#flush! for details. + def flush_idle_connections!(role = ActiveRecord::Base.current_role) + STDERR.puts "=== flush_idle_connections! called:" + each_connection_pool(role).each(&:flush!) + end + end + end +end diff --git a/ee/spec/features/work_items/epics/epic_work_item_detail_spec.rb b/ee/spec/features/work_items/epics/epic_work_item_detail_spec.rb index fd43f13a9028d7422e761ea917183083029bcfab..b4126944384e3a96ba8aef53c44e40f91a68fe13 100644 --- a/ee/spec/features/work_items/epics/epic_work_item_detail_spec.rb +++ b/ee/spec/features/work_items/epics/epic_work_item_detail_spec.rb @@ -20,6 +20,12 @@ let(:work_items_path) { group_epic_path(group, work_item.iid) } let(:list_path) { group_epics_path(group) } + before(:each) do + STDERR.puts "=== current PID is #{ActiveRecord::Base.connection.select_all('SELECT pg_backend_pid()').first['pg_backend_pid']}" + STDERR.puts "=== current Connection count: #{ActiveRecord::Base.connection.load_balancer.pool.connections.count}" + STDERR.puts "=== current Connection object is #{ActiveRecord::Base.connection.load_balancer.pool.connection.object_id}" + end + context 'for signed in user' do let(:child_item) { create(:work_item, :epic_with_legacy_epic, namespace: group) } let(:linked_item) { child_item } diff --git a/lib/feature.rb b/lib/feature.rb index e8f70c149d86bb27d26bc2d486c0f58521384286..49434e14779ca8a325d6d83711217521c965345a 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -102,7 +102,7 @@ def get(key) end def persisted_names - return [] unless FlipperRecord.database.exists? + return [] unless database_exists? # This loads names of all stored feature flags # and returns a stable Set in the following order: @@ -307,6 +307,10 @@ def group_ids_for(feature_key) private + def database_exists? + FlipperRecord.database.exists? + end + def sanitized_thing(thing) case thing when :instance @@ -354,7 +358,7 @@ def unsafe_get(key) # During setup the database does not exist yet. So we haven't stored a value # for the feature yet and return the default. - return unless FlipperRecord.database.exists? + return unless database_exists? flag_stack = ::Thread.current[:feature_flag_recursion_check] || [] Thread.current[:feature_flag_recursion_check] = flag_stack diff --git a/scripts/utils.sh b/scripts/utils.sh index efb2118325dab10eb3ea1f3c839e47a6e3b83be1..8f5d1178056b730ce47d6c262e4f68afc09c1093 100644 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -351,7 +351,7 @@ function fail_pipeline_early() { echoinfo "This pipeline cannot be interrupted due to \`dont-interrupt-me\` job ${dont_interrupt_me_job_id}" else echoinfo "Failing pipeline early for fast feedback due to test failures in rspec fail-fast." - scripts/api/cancel_pipeline.rb + #scripts/api/cancel_pipeline.rb fi } diff --git a/spec/support/helpers/stubbed_feature.rb b/spec/support/helpers/stubbed_feature.rb index 4113a28182ba5834efd52a99e8000e6a0828dacb..7a3229c8c58e32f8ee63e850c505b88e54dd983a 100644 --- a/spec/support/helpers/stubbed_feature.rb +++ b/spec/support/helpers/stubbed_feature.rb @@ -42,5 +42,14 @@ def enabled?(*args, **kwargs) feature_flag end + + private + + def database_exists? + # We use an in-memory store when stubbed so we can skip the DB existence checks + return true if stub? + + super + end end end diff --git a/spec/support/rspec_order.rb b/spec/support/rspec_order.rb index 5e0afb58beccf4bf5d4ad71e364445b7eb47827c..e54fd938dace68dca2d0711be5c45624bcd2a9ca 100644 --- a/spec/support/rspec_order.rb +++ b/spec/support/rspec_order.rb @@ -87,7 +87,7 @@ def self.add_formatter_to(config) config.register_ordering(:reverse, &:reverse) # Randomization can be reproduced across test runs. - Kernel.srand config.seed + Kernel.srand 434 config.on_example_group_definition do |example_group| order = Support::RspecOrder.order_for(example_group) diff --git a/tooling/lib/tooling/parallel_rspec_runner.rb b/tooling/lib/tooling/parallel_rspec_runner.rb index ba4bdb9df416fa3cc8c4e9017b1de0058d49ad92..80b6912227343ff5bf7f8f65b8a669854e5c3c50 100644 --- a/tooling/lib/tooling/parallel_rspec_runner.rb +++ b/tooling/lib/tooling/parallel_rspec_runner.rb @@ -74,6 +74,8 @@ module Tooling class ParallelRSpecRunner include Tooling::Helpers::DurationFormatter + TEST_FILES = "ee/spec/features/work_items/epics/epic_work_item_detail_spec.rb" + # rubocop:disable Gitlab/Json -- standard JSON is sufficient def self.run(rspec_args: nil, filter_tests_file: nil) new(rspec_args: rspec_args, filter_tests_file: filter_tests_file).run @@ -136,7 +138,7 @@ def rspec_command %w[bundle exec rspec].tap do |cmd| cmd.push(*rspec_args) cmd.push('--') - cmd.push(*node_tests) + cmd.push(*TEST_FILES.split(' ')) end end