

Avoiding ActiveRecord::PreparedStatementCacheExpired Errors
source link: https://flexport.engineering/avoiding-activerecord-preparedstatementcacheexpired-errors-4499a4f961cf
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Avoiding ActiveRecord::PreparedStatementCacheExpired Errors


Adding columns to a database table without a default value or non-null constraint is generally considered a safe schema change — it won’t lock the database and is backwards compatible with code on deploy and won’t require taking the servers down for a maintenance window. However, we’ve noticed it is not completely zero downtime for us and occasionally run into a spike of ActiveRecord::PreparedStatementCacheExpired errors during deployment that resolve afterwards.
Digging into the error, we’ve discovered that it has to deal with our setup using a Postgres DB with prepared statements enabled in Rails. Sam Davies wrote an excellent post with a lot of background on the root of the error as well as how Rails handles it.
To summarize what we learned (original post here):
- Rails is configured to use prepared statements for efficiency — most Rails apps use the same set of SQL queries over and over again, and using prepared statements lets the database cache the compiled plan for queries and reuse them to reduce the overhead on repeated queries.
- The prepared statements become invalidated if the return result changes — for instance if you cache a SELECT * and then add columns on the table you select from
- In this case, you have to deallocate the prepared statement and retry:
- If this happens outside of a transaction, Rails will handle deallocation of the prepared statement and retry
- If this happens inside of a transaction, Rails will handle deallocation of the prepared statement and raises ActiveRecord::PreparedStatementCacheExpired (this is what was happening to us)
At a framework level, Rails could not retry transactions, because it’s possible for users to write code with side effects such as external API calls, email sends, etc… inside of the transaction that could have unintended effects if repeated, but it was possible to check for transactions that only touched the DB and add a catch/retry there and transparently fix the issue.
The post also included this snippet of code that we could use to retry the errors:
# 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
Armed with this knowledge, we thought we were well on our way to fixing this problem. However, we quickly discovered that it would be a huge task given our large monolithic codebase. There were transactions in a lot of different places and many transactions ran code owned by other teams that we could need their sign off on to verify the safety of retrying. We would have to dig through a lot of old code and coordinate across many teams to employ this solution and unfortunately our level of tech debt made it very costly — we wanted to get rid of this blip of downtime on our deploys but it was worth seeking out alternatives.
Looking back at our learning about the cause of the statement cache error, a second approach became apparent — we would not have to deal with ActiveRecord::PreparedStatementCacheExpired errors in the first place if our prepared statements didn’t get invalidated. “SELECT * from foo” will return different results after a schema change and become invalidated, but “SELECT bar FROM foo” will stay valid no matter how many columns we add to table foo (but it will become invalid if we remove column bar), all we had to do was get Rails to issue SELECT statements with a list of columns instead of *.
Luckily, we’ve noticed such queries before in our Rails logs when loading models that had ignored columns. Digging into the code confirmed that Rails will enumerate columns in a SELECT statement if there are any ignored columns:
def build_select(arel)
if select_values.any?
arel.project(*arel_columns(select_values.uniq))
elsif klass.ignored_columns.any?
arel.project(*klass.column_names.map { |field| arel_attribute(field) })
else
arel.project(table[Arel.star])
end
end
Which lead to this initial solution we applied to some of our models that are most actively modified
class Model < ApplicationRecord
self.ignored_columns = [:__fake_column__]
end
Which solved our problem, no transaction retries necessary, and could be done model by model, which aligned better with our team structure than going transaction by transaction. One risk with having columns listed in select queries is that ActiveRecord generates these queries using a cached list of columns generated on app start; if someone removes a column from the database in an unsafe way, our app would start erroring on reads for the model (there would only be errors on writes without our modification). However, that scenario is an exception that we already have guards against, we were ok with having the risk.
Hope this is useful to others! And curious to hear alternative approaches.
Recommend
-
186
文章分四个部分介绍了 ActiveRecord 中的重要内容,模型的创建过程、Scope 和查询的实现、模型关系的实现以及最后的 Migrations 任务的实现和执行过程,各个模块之间没有太多的关联,由于文章内容比较多,如果读者只对某一部分的内容感兴趣,可以只挑选一部分进行...
-
70
ActiveRecord Position Extension for Yii2 This extension provides support for ActiveRecord custom records order setup. For license information check the
-
44
GitHub is where people build software. More than 27 million people use GitHub to discover, fork, and contribute to over 80 million projects.
-
6
Quick Tip – Using to_s as a label and simplified link_to calls to your ActiveRecord models One of the things you’ll find in every rails application is links like this one: <%= link_to u...
-
10
Building your own ActiveRecord validation macros with validates_each A common task when writing your own Rails applications using ActiveRecord is creating your own validations for your models. While it’s...
-
13
Postgres Indexes for ActiveRecord Join Tables in Rails Apps Updated Jun 23, 2020 3 comments 9 minute read ...
-
10
UUID Primary Key in Rails 6 with PostgreSQL and Active Record Updated May 27, 2020 16 comments 8 minute read ...
-
6
Rails ActiveRecord PostgreSQL Foreign Keys and Data Integrity Updated Jun 28, 2019 5 minute read
-
8
欢迎转载,请支持原创,保留原文链接:blog.ilibrary.me Monkey patch 这篇日志起源于rail...
-
12
How Do You Know What ActiveRecord Table Has a user_id Attribute? Dec 28, 2019 Even though I'm a firm, firm believer...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK