diff --git a/Gemfile b/Gemfile index 66fa7e85..49f18179 100644 --- a/Gemfile +++ b/Gemfile @@ -71,6 +71,9 @@ gem 'openstax_api' gem 'apipie-rails' gem 'maruku' +# Retry failed database transactions +gem 'transaction_retry', github: 'openstax/transaction_retry' + # Lev framework gem 'lev' diff --git a/Gemfile.lock b/Gemfile.lock index 0cd3c269..57566fea 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,11 @@ +GIT + remote: https://github.com/openstax/transaction_retry.git + revision: 5a148396c3b332a7e6a1ad5c7e6eb86b68f09829 + specs: + transaction_retry (1.0.3) + activerecord (>= 3.0.11) + transaction_isolation (>= 1.0.2) + GEM remote: https://rubygems.org/ specs: @@ -480,9 +488,6 @@ GEM timecop (0.9.2) transaction_isolation (1.0.5) activerecord (>= 3.0.11) - transaction_retry (1.0.3) - activerecord (>= 3.0.11) - transaction_isolation (>= 1.0.2) turbolinks (5.2.1) turbolinks-source (~> 5.2) turbolinks-source (5.2.0) @@ -575,6 +580,7 @@ DEPENDENCIES sortability thin timecop + transaction_retry! turbolinks uglifier (>= 1.3.0) web-console diff --git a/config/initializers/transaction_retry.rb b/config/initializers/transaction_retry.rb new file mode 100644 index 00000000..4f05ae56 --- /dev/null +++ b/config/initializers/transaction_retry.rb @@ -0,0 +1,4 @@ +# TransactionRetry.max_retries = 3 +# TransactionRetry.wait_times = [0, 1, 2, 4, 8, 16, 32] # seconds to sleep after retry n +TransactionRetry.retry_on = ActiveRecord::PreparedStatementCacheExpired # or an array of classes is valid too (ActiveRecord::TransactionIsolationConflict is by default always included) +# TransactionRetry.before_retry = ->(retry_num, error) { ... } diff --git a/spec/lib/transaction_retry_spec.rb b/spec/lib/transaction_retry_spec.rb new file mode 100644 index 00000000..60dbab8e --- /dev/null +++ b/spec/lib/transaction_retry_spec.rb @@ -0,0 +1,20 @@ +require 'rails_helper' + +# Need truncation: true because transaction_retry only retries top-level transactions +RSpec.describe TransactionRetry, type: :lib, truncation: true do + it 'retries ActiveRecord::PreparedStatementCacheExpired' do + result = nil + + ActiveRecord::Base.transaction do + if result.nil? + result = 21 + + raise ActiveRecord::PreparedStatementCacheExpired.new('test') + else + result = 42 + end + end + + expect(result).to eq 42 + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index ea6c0e1e..f219c3de 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -43,7 +43,7 @@ # If you're not using ActiveRecord, or you'd prefer not to run each of your # examples within a transaction, remove the following line or assign false # instead of true. - config.use_transactional_fixtures = true + config.use_transactional_fixtures = false # RSpec Rails can automatically mix in different behaviours to your tests # based on their file location, for example enabling you to call `get` and @@ -59,6 +59,31 @@ # The different available types are documented in the features, such as in # https://relishapp.com/rspec/rspec-rails/docs config.infer_spec_type_from_file_location! + + config.prepend_before(:suite) do + DatabaseCleaner.clean_with(:truncation) + end + + config.prepend_before(:all) do + metadata = self.class.metadata + DatabaseCleaner.strategy = metadata[:js] || metadata[:truncation] ? :truncation : :transaction + DatabaseCleaner.start + end + + config.prepend_before(:each) do + DatabaseCleaner.start + end + + # https://github.com/DatabaseCleaner/database_cleaner#rspec-with-capybara-example says: + # "It's also recommended to use append_after to ensure DatabaseCleaner.clean + # runs after the after-test cleanup capybara/rspec installs." + config.append_after(:each) do + DatabaseCleaner.clean + end + + config.append_after(:all) do + DatabaseCleaner.clean + end end # Adds a convenience method to get interpret the body as JSON and convert to a hash;