Fix processing of remote Delete activities (#16084)
* Add tests * Ensure deleted statuses are marked as such * Save some redis memory by not storing URIs in delete_upon_arrival values * Avoid possible race condition when processing incoming Deletes * Avoid potential duplicate Delete forwards * Lower lock durations to reduce issues in case of hard crash of the Rails process * Check for `lock.aquired?` and improve comment * Refactor RedisLock usage in app/lib/activitypub * Fix using incorrect or non-existent sender for relaying Deleteslolsob-rspec
parent
24c66f14c9
commit
05df66f8d7
|
@ -144,7 +144,7 @@ class ActivityPub::Activity
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_later!(uri)
|
def delete_later!(uri)
|
||||||
redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, uri)
|
redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
def status_from_object
|
def status_from_object
|
||||||
|
@ -210,12 +210,22 @@ class ActivityPub::Activity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def lock_or_return(key, expire_after = 7.days.seconds)
|
def lock_or_return(key, expire_after = 2.hours.seconds)
|
||||||
yield if redis.set(key, true, nx: true, ex: expire_after)
|
yield if redis.set(key, true, nx: true, ex: expire_after)
|
||||||
ensure
|
ensure
|
||||||
redis.del(key)
|
redis.del(key)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def lock_or_fail(key)
|
||||||
|
RedisLock.acquire({ redis: Redis.current, key: key }) do |lock|
|
||||||
|
if lock.acquired?
|
||||||
|
yield
|
||||||
|
else
|
||||||
|
raise Mastodon::RaceConditionError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def fetch?
|
def fetch?
|
||||||
!@options[:delivery]
|
!@options[:delivery]
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,8 +4,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
|
||||||
def perform
|
def perform
|
||||||
return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity?
|
return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity?
|
||||||
|
|
||||||
RedisLock.acquire(lock_options) do |lock|
|
lock_or_fail("announce:#{@object['id']}") do
|
||||||
if lock.acquired?
|
|
||||||
original_status = status_from_object
|
original_status = status_from_object
|
||||||
|
|
||||||
return reject_payload! if original_status.nil? || !announceable?(original_status)
|
return reject_payload! if original_status.nil? || !announceable?(original_status)
|
||||||
|
@ -24,9 +23,6 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
|
||||||
)
|
)
|
||||||
|
|
||||||
distribute(@status)
|
distribute(@status)
|
||||||
else
|
|
||||||
raise Mastodon::RaceConditionError
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@status
|
@status
|
||||||
|
@ -69,8 +65,4 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
|
||||||
def reblog_of_local_status?
|
def reblog_of_local_status?
|
||||||
status_from_uri(object_uri)&.account&.local?
|
status_from_uri(object_uri)&.account&.local?
|
||||||
end
|
end
|
||||||
|
|
||||||
def lock_options
|
|
||||||
{ redis: Redis.current, key: "announce:#{@object['id']}" }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -45,8 +45,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
def create_status
|
def create_status
|
||||||
return reject_payload! if unsupported_object_type? || invalid_origin?(object_uri) || tombstone_exists? || !related_to_local_activity?
|
return reject_payload! if unsupported_object_type? || invalid_origin?(object_uri) || tombstone_exists? || !related_to_local_activity?
|
||||||
|
|
||||||
RedisLock.acquire(lock_options) do |lock|
|
lock_or_fail("create:#{object_uri}") do
|
||||||
if lock.acquired?
|
|
||||||
return if delete_arrived_first?(object_uri) || poll_vote? # rubocop:disable Lint/NonLocalExitFromIterator
|
return if delete_arrived_first?(object_uri) || poll_vote? # rubocop:disable Lint/NonLocalExitFromIterator
|
||||||
|
|
||||||
@status = find_existing_status
|
@status = find_existing_status
|
||||||
|
@ -56,9 +55,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
elsif @options[:delivered_to_account_id].present?
|
elsif @options[:delivered_to_account_id].present?
|
||||||
postprocess_audience_and_deliver
|
postprocess_audience_and_deliver
|
||||||
end
|
end
|
||||||
else
|
|
||||||
raise Mastodon::RaceConditionError
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@status
|
@status
|
||||||
|
@ -313,13 +309,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
poll = replied_to_status.preloadable_poll
|
poll = replied_to_status.preloadable_poll
|
||||||
already_voted = true
|
already_voted = true
|
||||||
|
|
||||||
RedisLock.acquire(poll_lock_options) do |lock|
|
lock_or_fail("vote:#{replied_to_status.poll_id}:#{@account.id}") do
|
||||||
if lock.acquired?
|
|
||||||
already_voted = poll.votes.where(account: @account).exists?
|
already_voted = poll.votes.where(account: @account).exists?
|
||||||
poll.votes.create!(account: @account, choice: poll.options.index(@object['name']), uri: object_uri)
|
poll.votes.create!(account: @account, choice: poll.options.index(@object['name']), uri: object_uri)
|
||||||
else
|
|
||||||
raise Mastodon::RaceConditionError
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
increment_voters_count! unless already_voted
|
increment_voters_count! unless already_voted
|
||||||
|
@ -508,12 +500,4 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
poll.reload
|
poll.reload
|
||||||
retry
|
retry
|
||||||
end
|
end
|
||||||
|
|
||||||
def lock_options
|
|
||||||
{ redis: Redis.current, key: "create:#{object_uri}" }
|
|
||||||
end
|
|
||||||
|
|
||||||
def poll_lock_options
|
|
||||||
{ redis: Redis.current, key: "vote:#{replied_to_status.poll_id}:#{@account.id}" }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,8 +20,15 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
||||||
def delete_note
|
def delete_note
|
||||||
return if object_uri.nil?
|
return if object_uri.nil?
|
||||||
|
|
||||||
|
lock_or_return("delete_status_in_progress:#{object_uri}", 5.minutes.seconds) do
|
||||||
unless invalid_origin?(object_uri)
|
unless invalid_origin?(object_uri)
|
||||||
RedisLock.acquire(lock_options) { |_lock| delete_later!(object_uri) }
|
# This lock ensures a concurrent `ActivityPub::Activity::Create` either
|
||||||
|
# does not create a status at all, or has finished saving it to the
|
||||||
|
# database before we try to load it.
|
||||||
|
# Without the lock, `delete_later!` could be called after `delete_arrived_first?`
|
||||||
|
# and `Status.find` before `Status.create!`
|
||||||
|
lock_or_fail("create:#{object_uri}") { delete_later!(object_uri) }
|
||||||
|
|
||||||
Tombstone.find_or_create_by(uri: object_uri, account: @account)
|
Tombstone.find_or_create_by(uri: object_uri, account: @account)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -30,23 +37,18 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
||||||
|
|
||||||
return if @status.nil?
|
return if @status.nil?
|
||||||
|
|
||||||
if @status.distributable?
|
forward! if @json['signature'].present? && @status.distributable?
|
||||||
forward_for_reply
|
|
||||||
forward_for_reblogs
|
|
||||||
end
|
|
||||||
|
|
||||||
delete_now!
|
delete_now!
|
||||||
end
|
end
|
||||||
|
|
||||||
def forward_for_reblogs
|
|
||||||
return if @json['signature'].blank?
|
|
||||||
|
|
||||||
rebloggers_ids = @status.reblogs.includes(:account).references(:account).merge(Account.local).pluck(:account_id)
|
|
||||||
inboxes = Account.where(id: ::Follow.where(target_account_id: rebloggers_ids).select(:account_id)).inboxes - [@account.preferred_inbox_url]
|
|
||||||
|
|
||||||
ActivityPub::LowPriorityDeliveryWorker.push_bulk(inboxes) do |inbox_url|
|
|
||||||
[payload, rebloggers_ids.first, inbox_url]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def rebloggers_ids
|
||||||
|
return @rebloggers_ids if defined?(@rebloggers_ids)
|
||||||
|
@rebloggers_ids = @status.reblogs.includes(:account).references(:account).merge(Account.local).pluck(:account_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def inboxes_for_reblogs
|
||||||
|
Account.where(id: ::Follow.where(target_account_id: rebloggers_ids).select(:account_id)).inboxes
|
||||||
end
|
end
|
||||||
|
|
||||||
def replied_to_status
|
def replied_to_status
|
||||||
|
@ -58,13 +60,19 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
||||||
!replied_to_status.nil? && replied_to_status.account.local?
|
!replied_to_status.nil? && replied_to_status.account.local?
|
||||||
end
|
end
|
||||||
|
|
||||||
def forward_for_reply
|
def inboxes_for_reply
|
||||||
return unless @json['signature'].present? && reply_to_local?
|
replied_to_status.account.followers.inboxes
|
||||||
|
end
|
||||||
|
|
||||||
inboxes = replied_to_status.account.followers.inboxes - [@account.preferred_inbox_url]
|
def forward!
|
||||||
|
inboxes = inboxes_for_reblogs
|
||||||
|
inboxes += inboxes_for_reply if reply_to_local?
|
||||||
|
inboxes -= [@account.preferred_inbox_url]
|
||||||
|
|
||||||
ActivityPub::LowPriorityDeliveryWorker.push_bulk(inboxes) do |inbox_url|
|
sender_id = reply_to_local? ? replied_to_status.account_id : rebloggers_ids.first
|
||||||
[payload, replied_to_status.account_id, inbox_url]
|
|
||||||
|
ActivityPub::LowPriorityDeliveryWorker.push_bulk(inboxes.uniq) do |inbox_url|
|
||||||
|
[payload, sender_id, inbox_url]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -75,8 +83,4 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
||||||
def payload
|
def payload
|
||||||
@payload ||= Oj.dump(@json)
|
@payload ||= Oj.dump(@json)
|
||||||
end
|
end
|
||||||
|
|
||||||
def lock_options
|
|
||||||
{ redis: Redis.current, key: "create:#{object_uri}" }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,6 +16,8 @@ class RemoveStatusService < BaseService
|
||||||
@account = status.account
|
@account = status.account
|
||||||
@options = options
|
@options = options
|
||||||
|
|
||||||
|
@status.discard
|
||||||
|
|
||||||
RedisLock.acquire(lock_options) do |lock|
|
RedisLock.acquire(lock_options) do |lock|
|
||||||
if lock.acquired?
|
if lock.acquired?
|
||||||
remove_from_self if @account.local?
|
remove_from_self if @account.local?
|
||||||
|
|
|
@ -49,4 +49,24 @@ RSpec.describe ActivityPub::Activity::Delete do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the status has been reported' do
|
||||||
|
describe '#perform' do
|
||||||
|
subject { described_class.new(json, sender) }
|
||||||
|
let!(:reporter) { Fabricate(:account) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
reporter.reports.create!(target_account: status.account, status_ids: [status.id], forwarded: false)
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'marks the status as deleted' do
|
||||||
|
expect(Status.find_by(id: status.id)).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'actually keeps a copy for inspection' do
|
||||||
|
expect(Status.with_discarded.find_by(id: status.id)).to_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue