From c93b617dec58f59da7d7a62c9da4c24f329413a6 Mon Sep 17 00:00:00 2001 From: Heinrich Lee Yu Date: Wed, 22 Oct 2025 17:56:46 +0800 Subject: [PATCH 1/2] Try to reproduce master failure --- .../initializers/active_record_test_patch.rb | 140 ++++++++++++++++++ .../epics/epic_work_item_detail_spec.rb | 6 + scripts/utils.sh | 2 +- spec/support/rspec_order.rb | 2 +- tooling/lib/tooling/parallel_rspec_runner.rb | 4 +- 5 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 config/initializers/active_record_test_patch.rb diff --git a/config/initializers/active_record_test_patch.rb b/config/initializers/active_record_test_patch.rb new file mode 100644 index 00000000000000..cb05c7de601801 --- /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 fd43f13a9028d7..b4126944384e3a 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/scripts/utils.sh b/scripts/utils.sh index efb2118325dab1..8f5d1178056b73 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/rspec_order.rb b/spec/support/rspec_order.rb index 5e0afb58beccf4..e54fd938dace68 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 ba4bdb9df416fa..80b6912227343f 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 -- GitLab From 8e1fb4e3796eb6dd6539a44fc50a0587af783d84 Mon Sep 17 00:00:00 2001 From: Heinrich Lee Yu Date: Thu, 23 Oct 2025 00:02:33 +0800 Subject: [PATCH 2/2] Prevent DB checks when feature flags are stubbed --- lib/feature.rb | 8 ++++++-- spec/support/helpers/stubbed_feature.rb | 9 +++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/feature.rb b/lib/feature.rb index e8f70c149d86bb..49434e14779ca8 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/spec/support/helpers/stubbed_feature.rb b/spec/support/helpers/stubbed_feature.rb index 4113a28182ba58..7a3229c8c58e32 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 -- GitLab