Turns out this caused a *different* problem. It would work for the first delete batch in `BulkSqlDelete`, but subsequent `DELETE` queries would then get their `statement_timeout`s reset to the global setting. The issue was that `BulkSqlDelete` uses methods that don't respect nested transactions, and we were already resetting the value of the setting to its previous value anyway, so there was really no reason to run it inside a transaction. The only failure mode I know of here is that the `ensure` block breaks because `connection` is severed. In that case, the value is reset anyway when the connection pool replaces the connection so the failure does not propagate.
76 lines
2.5 KiB
Ruby
76 lines
2.5 KiB
Ruby
class ApplicationRecord < ActiveRecord::Base
|
|
self.abstract_class = true
|
|
|
|
include Purgeable
|
|
|
|
# see <https://www.postgresql.org/docs/11/catalog-pg-class.html> for details
|
|
# on the `pg_class` table
|
|
QUERY_ESTIMATED_COUNT = <<~SQL.squish.freeze
|
|
SELECT (
|
|
(reltuples / GREATEST(relpages, 1)) *
|
|
(pg_relation_size(?) / (GREATEST(current_setting('block_size')::integer, 1)))
|
|
)::bigint AS count
|
|
FROM pg_class
|
|
WHERE relname = ? AND relkind = 'r';
|
|
SQL
|
|
|
|
# Computes an estimated count of the number of rows using stats collected by VACUUM
|
|
# inspired by <https://www.citusdata.com/blog/2016/10/12/count-performance/#dup_counts_estimated_full>
|
|
# and <https://stackoverflow.com/a/48391562/4186181>
|
|
def self.estimated_count
|
|
query = sanitize_sql_array([QUERY_ESTIMATED_COUNT, table_name, table_name])
|
|
result = connection.execute(query)
|
|
|
|
count = result.first["count"]
|
|
result.clear # PG::Result is manually managed in memory, we need to release its resources
|
|
count
|
|
end
|
|
|
|
# Decorate object with appropriate decorator
|
|
def decorate
|
|
self.class.decorator_class.new(self)
|
|
end
|
|
|
|
def decorated?
|
|
false
|
|
end
|
|
|
|
# Decorate collection with appropriate decorator
|
|
def self.decorate
|
|
decorator_class.decorate_collection(all)
|
|
end
|
|
|
|
# Infers the decorator class to be used by (e.g. `User` maps to `UserDecorator`).
|
|
# adapted from https://github.com/drapergem/draper/blob/157eb955072a941e6455e0121fca09a989fcbc21/lib/draper/decoratable.rb#L71
|
|
def self.decorator_class(called_on = self)
|
|
prefix = respond_to?(:model_name) ? model_name : name
|
|
decorator_name = "#{prefix}Decorator"
|
|
decorator_name_constant = decorator_name.safe_constantize
|
|
return decorator_name_constant unless decorator_name_constant.nil?
|
|
|
|
return superclass.decorator_class(called_on) if superclass.respond_to?(:decorator_class)
|
|
|
|
raise UninferrableDecoratorError, "Could not infer a decorator for #{called_on.class.name}."
|
|
end
|
|
|
|
def self.statement_timeout
|
|
connection.execute("SELECT setting FROM pg_settings WHERE name = 'statement_timeout'")
|
|
.first["setting"]
|
|
.to_i
|
|
.seconds / 1000
|
|
end
|
|
|
|
def self.with_statement_timeout(duration, connection: self.connection)
|
|
original_timeout = statement_timeout
|
|
milliseconds = (duration.to_f * 1000).to_i
|
|
connection.execute "SET statement_timeout = #{milliseconds}"
|
|
yield
|
|
ensure
|
|
milliseconds = original_timeout.to_i * 1000
|
|
connection.execute "SET statement_timeout = #{milliseconds}"
|
|
end
|
|
|
|
def errors_as_sentence
|
|
errors.full_messages.to_sentence
|
|
end
|
|
end
|