forked from asonix/mastodon
* Fix account deletion sometimes failing because of optimistic locks In some rare occasions[1], deleting accounts would fail with a `StaleObjectError` exception. Indeed, account deletion manually sets the `AccountStat` values without handling cases where the optimistic locking on `AccountStat` would fail. To my knowledge, with the rewrite of account counters in #15913, the `DeleteAccountService` is now the only place that changes the counters in a way that is not atomic. Since in this specific case, we do not care about the previous values of the account counters, it appears we don't need locking at all for this table anymore. [1]: https://discourse.joinmastodon.org/t/account-cant-be-deleted/3602 * Bump MAX_SUPPORTED_VERSION in maintenance script
87 lines
2.8 KiB
Ruby
87 lines
2.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module AccountCounters
|
|
extend ActiveSupport::Concern
|
|
|
|
ALLOWED_COUNTER_KEYS = %i(statuses_count following_count followers_count).freeze
|
|
|
|
included do
|
|
has_one :account_stat, inverse_of: :account
|
|
after_save :save_account_stat
|
|
end
|
|
|
|
delegate :statuses_count,
|
|
:statuses_count=,
|
|
:following_count,
|
|
:following_count=,
|
|
:followers_count,
|
|
:followers_count=,
|
|
:last_status_at,
|
|
to: :account_stat
|
|
|
|
# @param [Symbol] key
|
|
def increment_count!(key)
|
|
update_count!(key, 1)
|
|
end
|
|
|
|
# @param [Symbol] key
|
|
def decrement_count!(key)
|
|
update_count!(key, -1)
|
|
end
|
|
|
|
# @param [Symbol] key
|
|
# @param [Integer] value
|
|
def update_count!(key, value)
|
|
raise ArgumentError, "Invalid key #{key}" unless ALLOWED_COUNTER_KEYS.include?(key)
|
|
raise ArgumentError, 'Do not call update_count! on dirty objects' if association(:account_stat).loaded? && account_stat&.changed? && account_stat.changed_attribute_names_to_save == %w(id)
|
|
|
|
value = value.to_i
|
|
default_value = value.positive? ? value : 0
|
|
|
|
# We do an upsert using manually written SQL, as Rails' upsert method does
|
|
# not seem to support writing expressions in the UPDATE clause, but only
|
|
# re-insert the provided values instead.
|
|
# Even ARel seem to be missing proper handling of upserts.
|
|
sql = if value.positive? && key == :statuses_count
|
|
<<-SQL.squish
|
|
INSERT INTO account_stats(account_id, #{key}, created_at, updated_at, last_status_at)
|
|
VALUES (:account_id, :default_value, now(), now(), now())
|
|
ON CONFLICT (account_id) DO UPDATE
|
|
SET #{key} = account_stats.#{key} + :value,
|
|
last_status_at = now(),
|
|
updated_at = now()
|
|
RETURNING id;
|
|
SQL
|
|
else
|
|
<<-SQL.squish
|
|
INSERT INTO account_stats(account_id, #{key}, created_at, updated_at)
|
|
VALUES (:account_id, :default_value, now(), now())
|
|
ON CONFLICT (account_id) DO UPDATE
|
|
SET #{key} = account_stats.#{key} + :value,
|
|
updated_at = now()
|
|
RETURNING id;
|
|
SQL
|
|
end
|
|
|
|
sql = AccountStat.sanitize_sql([sql, account_id: id, default_value: default_value, value: value])
|
|
account_stat_id = AccountStat.connection.exec_query(sql)[0]['id']
|
|
|
|
# Reload account_stat if it was loaded, taking into account newly-created unsaved records
|
|
if association(:account_stat).loaded?
|
|
account_stat.id = account_stat_id if account_stat.new_record?
|
|
account_stat.reload
|
|
end
|
|
end
|
|
|
|
def account_stat
|
|
super || build_account_stat
|
|
end
|
|
|
|
private
|
|
|
|
def save_account_stat
|
|
return unless association(:account_stat).loaded? && account_stat&.changed?
|
|
|
|
account_stat.save
|
|
end
|
|
end
|