If migrations are pending, make CurrentSettings use existing values and populate missing columns with defaults
master was failing because `ApplicationSetting.create_from_defaults` attempted to write to a column that did not exist in the database. This occurred in a `rake db:migrate` task, which was unable to perform the migration that would have added the missing column in the first place. In 9.3 RC2, we also had a bug where password sign-ins were disabled because there were many pending migrations. The problem occurred because `fake_application_settings` was being returned with an OpenStruct that did not include the predicate method `signup_enabled?`. As a result, the value would erroneously return `nil` instead of `true`. This commit uses the values of the defaults to mimic this behavior. This commit also refactors some of the logic to be clearer.
This commit is contained in:
parent
84cfb33fbb
commit
575dced5d7
|
@ -10,43 +10,49 @@ module Gitlab
|
||||||
|
|
||||||
delegate :sidekiq_throttling_enabled?, to: :current_application_settings
|
delegate :sidekiq_throttling_enabled?, to: :current_application_settings
|
||||||
|
|
||||||
def fake_application_settings
|
def fake_application_settings(defaults = ApplicationSetting.defaults)
|
||||||
OpenStruct.new(::ApplicationSetting.defaults)
|
FakeApplicationSettings.new(defaults)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def ensure_application_settings!
|
def ensure_application_settings!
|
||||||
unless ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true'
|
return in_memory_application_settings if ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true'
|
||||||
settings = retrieve_settings_from_database?
|
|
||||||
end
|
|
||||||
|
|
||||||
settings || in_memory_application_settings
|
cached_application_settings || uncached_application_settings
|
||||||
end
|
end
|
||||||
|
|
||||||
def retrieve_settings_from_database?
|
def cached_application_settings
|
||||||
settings = retrieve_settings_from_database_cache?
|
begin
|
||||||
return settings if settings.present?
|
ApplicationSetting.cached
|
||||||
|
rescue ::Redis::BaseError, ::Errno::ENOENT
|
||||||
|
# In case Redis isn't running or the Redis UNIX socket file is not available
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def uncached_application_settings
|
||||||
return fake_application_settings unless connect_to_db?
|
return fake_application_settings unless connect_to_db?
|
||||||
|
|
||||||
|
# This loads from the database into the cache, so handle Redis errors
|
||||||
begin
|
begin
|
||||||
db_settings = ::ApplicationSetting.current
|
db_settings = ApplicationSetting.current
|
||||||
# In case Redis isn't running or the Redis UNIX socket file is not available
|
|
||||||
rescue ::Redis::BaseError, ::Errno::ENOENT
|
rescue ::Redis::BaseError, ::Errno::ENOENT
|
||||||
db_settings = ::ApplicationSetting.last
|
# In case Redis isn't running or the Redis UNIX socket file is not available
|
||||||
end
|
end
|
||||||
db_settings || ::ApplicationSetting.create_from_defaults
|
|
||||||
end
|
|
||||||
|
|
||||||
def retrieve_settings_from_database_cache?
|
# If there are pending migrations, it's possible there are columns that
|
||||||
begin
|
# need to be added to the application settings. To prevent Rake tasks
|
||||||
settings = ApplicationSetting.cached
|
# and other callers from failing, use any loaded settings and return
|
||||||
rescue ::Redis::BaseError, ::Errno::ENOENT
|
# defaults for missing columns.
|
||||||
# In case Redis isn't running or the Redis UNIX socket file is not available
|
if ActiveRecord::Migrator.needs_migration?
|
||||||
settings = nil
|
defaults = ApplicationSetting.defaults
|
||||||
|
defaults.merge!(db_settings.attributes.symbolize_keys) if db_settings.present?
|
||||||
|
return fake_application_settings(defaults)
|
||||||
end
|
end
|
||||||
settings
|
|
||||||
|
return db_settings if db_settings.present?
|
||||||
|
|
||||||
|
ApplicationSetting.create_from_defaults || in_memory_application_settings
|
||||||
end
|
end
|
||||||
|
|
||||||
def in_memory_application_settings
|
def in_memory_application_settings
|
||||||
|
@ -62,8 +68,7 @@ module Gitlab
|
||||||
active_db_connection = ActiveRecord::Base.connection.active? rescue false
|
active_db_connection = ActiveRecord::Base.connection.active? rescue false
|
||||||
|
|
||||||
active_db_connection &&
|
active_db_connection &&
|
||||||
ActiveRecord::Base.connection.table_exists?('application_settings') &&
|
ActiveRecord::Base.connection.table_exists?('application_settings')
|
||||||
!ActiveRecord::Migrator.needs_migration?
|
|
||||||
rescue ActiveRecord::NoDatabaseError
|
rescue ActiveRecord::NoDatabaseError
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
# This class extends an OpenStruct object by adding predicate methods to mimic
|
||||||
|
# ActiveRecord access. We rely on the initial values being true or false to
|
||||||
|
# determine whether to define a predicate method because for a newly-added
|
||||||
|
# column that has not been migrated yet, there is no way to determine the
|
||||||
|
# column type without parsing db/schema.rb.
|
||||||
|
module Gitlab
|
||||||
|
class FakeApplicationSettings < OpenStruct
|
||||||
|
def initialize(options = {})
|
||||||
|
super
|
||||||
|
|
||||||
|
FakeApplicationSettings.define_predicate_methods(options)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Mimic ActiveRecord predicate methods for boolean values
|
||||||
|
def self.define_predicate_methods(options)
|
||||||
|
options.each do |key, value|
|
||||||
|
next if key.to_s.end_with?('?')
|
||||||
|
next unless [true, false].include?(value)
|
||||||
|
|
||||||
|
define_method "#{key}?" do
|
||||||
|
actual_key = key.to_s.chomp('?')
|
||||||
|
self[actual_key]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,4 @@
|
||||||
|
FactoryGirl.define do
|
||||||
|
factory :application_setting do
|
||||||
|
end
|
||||||
|
end
|
|
@ -32,6 +32,37 @@ describe Gitlab::CurrentSettings do
|
||||||
|
|
||||||
expect(current_application_settings).to be_a(ApplicationSetting)
|
expect(current_application_settings).to be_a(ApplicationSetting)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with migrations pending' do
|
||||||
|
before do
|
||||||
|
expect(ActiveRecord::Migrator).to receive(:needs_migration?).and_return(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an in-memory ApplicationSetting object' do
|
||||||
|
settings = current_application_settings
|
||||||
|
|
||||||
|
expect(settings).to be_a(OpenStruct)
|
||||||
|
expect(settings.sign_in_enabled?).to eq(settings.sign_in_enabled)
|
||||||
|
expect(settings.sign_up_enabled?).to eq(settings.sign_up_enabled)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'uses the existing database settings and falls back to defaults' do
|
||||||
|
db_settings = create(:application_setting,
|
||||||
|
home_page_url: 'http://mydomain.com',
|
||||||
|
signup_enabled: false)
|
||||||
|
settings = current_application_settings
|
||||||
|
app_defaults = ApplicationSetting.last
|
||||||
|
|
||||||
|
expect(settings).to be_a(OpenStruct)
|
||||||
|
expect(settings.home_page_url).to eq(db_settings.home_page_url)
|
||||||
|
expect(settings.signup_enabled?).to be_falsey
|
||||||
|
expect(settings.signup_enabled).to be_falsey
|
||||||
|
|
||||||
|
# Check that unspecified values use the defaults
|
||||||
|
settings.reject! { |key, _| [:home_page_url, :signup_enabled].include? key }
|
||||||
|
settings.each { |key, _| expect(settings[key]).to eq(app_defaults[key]) }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with DB unavailable' do
|
context 'with DB unavailable' do
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Gitlab::FakeApplicationSettings do
|
||||||
|
let(:defaults) { { signin_enabled: false, foobar: 'asdf', signup_enabled: true, 'test?' => 123 } }
|
||||||
|
|
||||||
|
subject { described_class.new(defaults) }
|
||||||
|
|
||||||
|
it 'wraps OpenStruct variables properly' do
|
||||||
|
expect(subject.signin_enabled).to be_falsey
|
||||||
|
expect(subject.signup_enabled).to be_truthy
|
||||||
|
expect(subject.foobar).to eq('asdf')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'defines predicate methods' do
|
||||||
|
expect(subject.signin_enabled?).to be_falsey
|
||||||
|
expect(subject.signup_enabled?).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'predicate method changes when value is updated' do
|
||||||
|
subject.signin_enabled = true
|
||||||
|
|
||||||
|
expect(subject.signin_enabled?).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not define a predicate method' do
|
||||||
|
expect(subject.foobar?).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not override an existing predicate method' do
|
||||||
|
expect(subject.test?).to eq(123)
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue