Skip to content

Correctly deallocate prepared statements if we fail inside a transaction#22170

Merged
matthewd merged 2 commits intorails:masterfrom
samsondav:sam/properly_deallocate_prepared_statements_outside_of_transaction
Mar 1, 2016
Merged

Correctly deallocate prepared statements if we fail inside a transaction#22170
matthewd merged 2 commits intorails:masterfrom
samsondav:sam/properly_deallocate_prepared_statements_outside_of_transaction

Conversation

@samsondav
Copy link

Overview

Cached postgres prepared statements become invalidated if the schema
changes in a way that it affects the returned result.

Examples:

  • adding or removing a column then doing a 'SELECT *'
  • removing the foo column then doing a 'SELECT bar.foo'

In normal operation this isn't a problem, we can rescue the error,
deallocate the prepared statement and re-issue the command.

However in PostgreSQL transactions, once any command fails, the
transaction becomes 'poisoned' and any subsequent commands will raise
InFailedSQLTransaction.

This includes DEALLOCATE statements, so the default deallocation
strategy instead of removing the cached prepared statement instead
raises InFailedSQLTransaction.

Why this is bad

  1. InFailedSQLTransaction is a fairly cryptic error and doesn't
    communicate any useful information about what has actually gone wrong.
  2. In the naive implementation the prepared statement never gets
    deallocated - it stays alive for the length of the session taking up
    memory on the postgres server.
  3. It is unsafe to retry the transaction because the bad prepared
    statement is still in the cache and we would see the exact same failure
    repeated.

Solution

If we are outside a transaction we can continue to handle these failures
gracefully in the usual way.

Inside a transaction instead of issuing a DEALLOCATE command that will
certainly fail, we now raise
ActiveRecord::PreparedStatementCacheExpired.

This can be handled further up the stack, notably inside
TransactionManager#within_new_transaction. Here we can make sure to
first rollback the transaction, then safely issue DEALLOCATE statements
to invalidate the rest of the cached prepared statements.

This also allows the user (or some gem) the opportunity to catch this error and
voluntarily retry the transaction if a schema change causes the prepared
statement cache to become invalidated.

Because the outdated statement has been deallocated, we can expect the
transaction to succeed on the second try.

@rails-bot
Copy link

Thanks for the pull request, and welcome! The Rails team is excited to review your changes, and you should hear from @schneems (or someone else) soon.

If any changes to this PR are deemed necessary, please add them as extra commits. This ensures that the reviewer can see what has changed since they last reviewed the code. Due to the way GitHub handles out-of-date commits, this should also make it reasonably obvious what issues have or haven't been addressed. Large or tricky changes may require several passes of review and changes.

Please see the contribution instructions for more information.

@schneems
Copy link
Member

schneems commented Nov 3, 2015

r? @sgrif

@rails-bot rails-bot assigned sgrif and unassigned schneems Nov 3, 2015
@samsondav samsondav force-pushed the sam/properly_deallocate_prepared_statements_outside_of_transaction branch 2 times, most recently from e87a9c0 to 0d72268 Compare November 3, 2015 16:50
@samsondav
Copy link
Author

@sgrif Going to look into the test failures on this one, will ping you when they are resolved.

@samsondav
Copy link
Author

@sgrif OK tests passing and I cleaned up a few things, this is ready for review.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, would this be raise e?

I'm tracking this PR for our own issues with prepared statements in transactions in Rails 4-2. This is really awesome work.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this particular case, raise e and raise have identical behavior. When called without an argument, raise simply re-raises the last exception, or a RuntimeError if there isn't one.

See http://ruby-doc.org/core-2.1.1/Kernel.html#method-i-raise .

@samsondav
Copy link
Author

I also considered adding a after_failed_transaction_hooks array to the transaction manager, but this turned out to be too general for just this one specific case.

@samsondav samsondav force-pushed the sam/properly_deallocate_prepared_statements_outside_of_transaction branch 2 times, most recently from 7f13991 to c5cd2ac Compare November 5, 2015 20:38
- Addresses issue rails#12330

Overview
========

Cached postgres prepared statements become invalidated if the schema
changes in a way that it affects the returned result.

Examples:
- adding or removing a column then doing a 'SELECT *'
- removing the foo column  then doing a 'SELECT bar.foo'

In normal operation this isn't a problem, we can rescue the error,
deallocate the prepared statement and re-issue the command.

However in PostgreSQL transactions, once any command fails, the
transaction becomes 'poisoned' and any subsequent commands will raise
InFailedSQLTransaction.

This includes DEALLOCATE statements, so the default deallocation
strategy instead of removing the cached prepared statement instead
raises InFailedSQLTransaction.

Why this is bad
===============

1. InFailedSQLTransaction is a fairly cryptic error and doesn't
communicate any useful information about what has actually gone wrong.
2. In the naive implementation the prepared statement never gets
deallocated - it stays alive for the length of the session taking up
memory on the postgres server.
3. It is unsafe to retry the transaction because the bad prepared
statement is still in the cache and we would see the exact same failure
repeated.

Solution
========

If we are outside a transaction we can continue to handle these failures
gracefully in the usual way.

Inside a transaction instead of issuing a DEALLOCATE command that will
certainly fail, we now raise
ActiveRecord::PreparedStatementCacheExpired.

This can be handled further up the stack, notably inside
TransactionManager#within_new_transaction. Here we can make sure to
first rollback the transaction, then safely issue DEALLOCATE statements
to invalidate the rest of the cached prepared statements.

This also allows the user (or some gem) the opportunity to catch this error and
voluntarily retry the transaction if a schema change causes the prepared
statement cache to become invalidated.

Because the outdated statement has been deallocated, we can expect the
transaction to succeed on the second try.
@samsondav samsondav force-pushed the sam/properly_deallocate_prepared_statements_outside_of_transaction branch from c5cd2ac to 50c5334 Compare November 5, 2015 20:40
@samsondav samsondav changed the title Correctly deallocate prepared statements if we are inside a transaction Correctly deallocate prepared statements if we fail inside a transaction Nov 5, 2015
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is a Postgress specific error, I am not sure if we should have handled by all adapters.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree in principle, except I couldn't find any place to put adapter-specific transaction behavior.

This method is the 'lowest' possible point in which we can access something outside of the transaction. While it's not ideal, I can't see a less invasive way of implementing this.

@matthewd matthewd added this to the 5.0.0 milestone Feb 14, 2016
@matthewd matthewd assigned matthewd and unassigned sgrif Feb 14, 2016
matthewd added a commit that referenced this pull request Mar 1, 2016
…pared_statements_outside_of_transaction

 Correctly deallocate prepared statements if we fail inside a transaction
@matthewd matthewd merged commit 833b14c into rails:master Mar 1, 2016
@samsondav samsondav deleted the sam/properly_deallocate_prepared_statements_outside_of_transaction branch April 26, 2016 02:03
@mecampbellsoup
Copy link
Contributor

mecampbellsoup commented May 5, 2017

@samphilipd great work on this PR and the issue it originated from - saves us a lot of time and headache, shout out to you! 😄

I wanted to ask: do you have any patterns you'd recommend for handling the ActiveRecord::PreparedStatementCacheExpired exceptions which are now raised?

I'm trying to determine where to rescue the exception - e.g. as far upstream as ApplicationController? Maybe in ApplicationRecord callback/hook so that all the models communicating with the DB can be programmed to auto-retry the Postgres DB command when they see this exception?

@samsondav
Copy link
Author

samsondav commented May 5, 2017

@mecampbellsoup you could do something like this:

# Make all transactions for all records automatically retriable in the event of
# cache failure
class ApplicationRecord
  class << self
    # Retry automatically on ActiveRecord::PreparedStatementCacheExpired.
    #
    # Do not use this for transactions with side-effects unless it is acceptable
    # for these side-effects to occasionally happen twice
    def transaction(*args, &block)
      retried ||= false
      super
    rescue ActiveRecord::PreparedStatementCacheExpired
      if retried
        raise
      else
        retried = true
        retry
      end
    end
  end
end

You can call a retriable transaction like this:

# Automatically retries in the event of ActiveRecord::PreparedStatementCacheExpired
ApplicationRecord.transaction do
  ...
end

or

# Automatically retries in the event of ActiveRecord::PreparedStatementCacheExpired
MyModel.transaction do
  ...
end

Note that if you are sending emails, POSTing to an API or doing other such things that interact with the outside world inside your transactions, this could result in some of those things occasionally happening twice.

If you have a transaction with side-effects and would prefer to raise an error rather than auto-retry in the event of this error, you can call the original like this:

# Raises instead of retries on ActiveRecord::PreparedStatementCacheExpired
ActiveRecord::Base.transaction do
  ...
end

@samsondav
Copy link
Author

@mecampbellsoup here is a more detailed article on how to handle these errors.

@marisveide
Copy link

I am on Rails 5.1.6 and still are getting this error when underlying schema changes while the Rails process is running.
Just want to make sure - is this fix actually released?

Thanks!

@bf4
Copy link
Contributor

bf4 commented Feb 12, 2019

@marisveide there are a number of ways to check if a commit is present in a given branch using a version control system like git. They probably all are quicker for you to do than the time everyone on this issue takes to read your question.

I'm just going to paste the link the merge commit github provides up on the page since it references this commit being present from Rails 5.0 beta through Rails 6 833b14c

@marisveide
Copy link

Hi, @bf4 !
Thank you for taking your time to reply.
The issue is that we are getting this issue on Rails 5.1.6. The solution which helped was to switch off the prepared statements - by setting the prepared_statements: false in database.yml.
That results in higher DB load, but is not causing the ActiveRecord::PreparedStatementCacheExpired exception when altering the DB schema, and consequently - all those workers die.

We have long-running workers on Rails (with Sidekiq) and are getting this error in all workers which were running while DB schema was modified.

Just in case, here is the stack trace from that exception:

Error: ActiveRecord::PreparedStatementCacheExpired - ERROR:  cached plan must not change result type

/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/connection_adapters/postgresql_adapter.rb:636:in `rescue in exec_cache'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/connection_adapters/postgresql_adapter.rb:621:in `exec_cache'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/connection_adapters/postgresql_adapter.rb:605:in `execute_and_clear'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/connection_adapters/postgresql/database_statements.rb:79:in `exec_query'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/connection_adapters/abstract/database_statements.rb:375:in `select_prepared'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/connection_adapters/abstract/database_statements.rb:40:in `select_all'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/connection_adapters/abstract/query_cache.rb:97:in `select_all'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/querying.rb:39:in `find_by_sql'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/statement_cache.rb:107:in `execute'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/associations/singular_association.rb:50:in `find_target'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/associations/association.rb:157:in `load_target'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/associations/association.rb:53:in `reload'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/associations/singular_association.rb:7:in `reader'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/associations/builder/association.rb:111:in `shop'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activemodel-5.1.6/lib/active_model/validator.rb:148:in `block in validate'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activemodel-5.1.6/lib/active_model/validator.rb:147:in `each'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activemodel-5.1.6/lib/active_model/validator.rb:147:in `validate'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activesupport-5.1.6/lib/active_support/callbacks.rb:413:in `block in make_lambda'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activesupport-5.1.6/lib/active_support/callbacks.rb:197:in `block (2 levels) in halting'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activesupport-5.1.6/lib/active_support/callbacks.rb:601:in `block (2 levels) in default_terminator'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activesupport-5.1.6/lib/active_support/callbacks.rb:600:in `catch'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activesupport-5.1.6/lib/active_support/callbacks.rb:600:in `block in default_terminator'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activesupport-5.1.6/lib/active_support/callbacks.rb:198:in `block in halting'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activesupport-5.1.6/lib/active_support/callbacks.rb:507:in `block in invoke_before'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activesupport-5.1.6/lib/active_support/callbacks.rb:507:in `each'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activesupport-5.1.6/lib/active_support/callbacks.rb:507:in `invoke_before'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activesupport-5.1.6/lib/active_support/callbacks.rb:130:in `run_callbacks'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activesupport-5.1.6/lib/active_support/callbacks.rb:827:in `_run_validate_callbacks'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activemodel-5.1.6/lib/active_model/validations.rb:405:in `run_validations!'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activemodel-5.1.6/lib/active_model/validations/callbacks.rb:114:in `block in run_validations!'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activesupport-5.1.6/lib/active_support/callbacks.rb:131:in `run_callbacks'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activesupport-5.1.6/lib/active_support/callbacks.rb:827:in `_run_validation_callbacks'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activemodel-5.1.6/lib/active_model/validations/callbacks.rb:114:in `run_validations!'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activemodel-5.1.6/lib/active_model/validations.rb:335:in `valid?'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/validations.rb:65:in `valid?'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/validations.rb:82:in `perform_validations'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/validations.rb:50:in `save!'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/attribute_methods/dirty.rb:43:in `save!'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/transactions.rb:313:in `block in save!'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/transactions.rb:384:in `block in with_transaction_returning_status'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/connection_adapters/abstract/database_statements.rb:235:in `block in transaction'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/connection_adapters/abstract/transaction.rb:194:in `block in within_new_transaction'
/opt/rubies/ruby-2.4.4/lib/ruby/2.4.0/monitor.rb:214:in `mon_synchronize'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/connection_adapters/abstract/transaction.rb:191:in `within_new_transaction'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/connection_adapters/abstract/database_statements.rb:235:in `transaction'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/transactions.rb:210:in `transaction'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/transactions.rb:381:in `with_transaction_returning_status'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/transactions.rb:313:in `save!'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activerecord-5.1.6/lib/active_record/suppressor.rb:46:in `save!'
/var/app/current/app/models/job.rb:291:in `set_step'
/var/app/current/app/workers/import/worker.rb:150:in `save_file'
/var/app/current/app/workers/import/worker.rb:144:in `do_import'
/var/app/current/app/workers/import/worker.rb:25:in `run_job'
/var/app/current/app/workers/base_worker.rb:12:in `block in perform'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/spawnling-2.1.6/lib/spawnling.rb:122:in `block in run'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/spawnling-2.1.6/lib/spawnling.rb:184:in `block in fork_it'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/spawnling-2.1.6/lib/spawnling.rb:160:in `fork'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/spawnling-2.1.6/lib/spawnling.rb:160:in `fork_it'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/spawnling-2.1.6/lib/spawnling.rb:122:in `run'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/spawnling-2.1.6/lib/spawnling.rb:102:in `initialize'
/var/app/current/app/workers/base_worker.rb:6:in `new'
/var/app/current/app/workers/base_worker.rb:6:in `perform'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/processor.rb:185:in `execute_job'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/processor.rb:167:in `block (2 levels) in process'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/middleware/chain.rb:128:in `block in invoke'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/middleware/chain.rb:133:in `invoke'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/processor.rb:166:in `block in process'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/processor.rb:137:in `block (6 levels) in dispatch'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/job_retry.rb:98:in `local'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/processor.rb:136:in `block (5 levels) in dispatch'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/rails.rb:42:in `block in call'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activesupport-5.1.6/lib/active_support/execution_wrapper.rb:85:in `wrap'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activesupport-5.1.6/lib/active_support/reloader.rb:68:in `block in wrap'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activesupport-5.1.6/lib/active_support/execution_wrapper.rb:85:in `wrap'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/activesupport-5.1.6/lib/active_support/reloader.rb:67:in `wrap'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/rails.rb:41:in `call'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/processor.rb:132:in `block (4 levels) in dispatch'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/processor.rb:243:in `stats'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/processor.rb:127:in `block (3 levels) in dispatch'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/job_logger.rb:8:in `call'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/processor.rb:126:in `block (2 levels) in dispatch'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/job_retry.rb:73:in `global'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/processor.rb:125:in `block in dispatch'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/logging.rb:48:in `with_context'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/logging.rb:42:in `with_job_hash_context'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/processor.rb:124:in `dispatch'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/processor.rb:165:in `process'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/processor.rb:83:in `process_one'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/processor.rb:71:in `run'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/util.rb:16:in `watchdog'
/opt/rubies/ruby-2.4.4/lib/ruby/gems/2.4.0/gems/sidekiq-5.2.2/lib/sidekiq/util.rb:25:in `block in safe_thread'

Any suggestions on how to proceed with seeking a fix to that?
Is there a chance that this issue is still not fixed, or that it's another one which causes the same exception?

@brasic
Copy link
Contributor

brasic commented Feb 12, 2019

@marisveide this PR does not automatically fix that error, because the only way to fix it is to retry the entire transaction block, which is not safe in the general case since it can result in side effects like API calls or background job enqueues happening more times than you expect. The PR merely provides a framework for handling the error in a way that will be application-specific.

If you are sure that every rails transaction block in your codebase has no side effects you can follow the advice in this article. Otherwise, you should define a new ApplicationRecord.retriable_transaction method modeled after rails' version that rescues ActiveRecord::PreparedStatementCacheExpired and retries, then use that method instead of transaction for your pure transactions and continue to use transaction for nonpure code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.