From 216b85b053d091306e3311a21f5b050f70a75130 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Dec 2020 09:06:34 +0100 Subject: Fix performance on instances list in admin UI (#15282) - Reduce duplicate queries - Remove n+1 queries - Add accounts count to detailed view - Add separate action log entry for updating existing domain blocks --- app/workers/scheduler/instance_refresh_scheduler.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 app/workers/scheduler/instance_refresh_scheduler.rb (limited to 'app/workers') diff --git a/app/workers/scheduler/instance_refresh_scheduler.rb b/app/workers/scheduler/instance_refresh_scheduler.rb new file mode 100644 index 000000000..917404bec --- /dev/null +++ b/app/workers/scheduler/instance_refresh_scheduler.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Scheduler::InstanceRefreshScheduler + include Sidekiq::Worker + + sidekiq_options lock: :until_executed, retry: 0 + + def perform + Instance.refresh + end +end -- cgit From a60d9335d8e7c4aa070f081719ee2a438b0e0202 Mon Sep 17 00:00:00 2001 From: ThibG Date: Fri, 18 Dec 2020 23:26:26 +0100 Subject: Fix resolving accounts sometimes creating duplicate records for a given AP id (#15364) * Fix ResolveAccountService accepting mismatching acct: URI * Set attributes that should be updated regardless of suspension * Fix key fetching * Automatically merge remote accounts with duplicate `uri` * Add tests * Add "tootctl accounts fix-duplicates" Finds duplicate accounts sharing a same ActivityPub `id`, re-fetch them and merge them under the canonical `acct:` URI. Co-authored-by: Claire --- .../activitypub/fetch_remote_account_service.rb | 2 +- .../activitypub/process_account_service.rb | 28 ++++++++--- app/services/resolve_account_service.rb | 17 ++----- app/workers/account_merging_worker.rb | 18 +++++++ lib/mastodon/accounts_cli.rb | 19 ++++++++ spec/services/resolve_account_service_spec.rb | 56 ++++++++++++++++++++-- 6 files changed, 116 insertions(+), 24 deletions(-) create mode 100644 app/workers/account_merging_worker.rb (limited to 'app/workers') diff --git a/app/services/activitypub/fetch_remote_account_service.rb b/app/services/activitypub/fetch_remote_account_service.rb index e5bd0c47c..9d01f5386 100644 --- a/app/services/activitypub/fetch_remote_account_service.rb +++ b/app/services/activitypub/fetch_remote_account_service.rb @@ -28,7 +28,7 @@ class ActivityPub::FetchRemoteAccountService < BaseService return unless only_key || verified_webfinger? - ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key) + ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key, verified_webfinger: !only_key) rescue Oj::ParseError nil end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 4cb8e09db..6afeb92d6 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -28,6 +28,8 @@ class ActivityPub::ProcessAccountService < BaseService update_account process_tags process_attachments + + process_duplicate_accounts! if @options[:verified_webfinger] else raise Mastodon::RaceConditionError end @@ -69,34 +71,42 @@ class ActivityPub::ProcessAccountService < BaseService @account.protocol = :activitypub set_suspension! + set_immediate_protocol_attributes! + set_fetchable_key! unless @account.suspended? && @account.suspension_origin_local? set_immediate_attributes! unless @account.suspended? - set_fetchable_attributes! unless @options[:only_keys] || @account.suspended? + set_fetchable_attributes! unless @options[:only_key] || @account.suspended? @account.save_with_optional_media! end - def set_immediate_attributes! + def set_immediate_protocol_attributes! @account.inbox_url = @json['inbox'] || '' @account.outbox_url = @json['outbox'] || '' @account.shared_inbox_url = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || '' @account.followers_url = @json['followers'] || '' - @account.featured_collection_url = @json['featured'] || '' - @account.devices_url = @json['devices'] || '' @account.url = url || @uri @account.uri = @uri + @account.actor_type = actor_type + end + + def set_immediate_attributes! + @account.featured_collection_url = @json['featured'] || '' + @account.devices_url = @json['devices'] || '' @account.display_name = @json['name'] || '' @account.note = @json['summary'] || '' @account.locked = @json['manuallyApprovesFollowers'] || false @account.fields = property_values || {} @account.also_known_as = as_array(@json['alsoKnownAs'] || []).map { |item| value_or_id(item) } - @account.actor_type = actor_type @account.discoverable = @json['discoverable'] || false end + def set_fetchable_key! + @account.public_key = public_key || '' + end + def set_fetchable_attributes! @account.avatar_remote_url = image_url('icon') || '' unless skip_download? @account.header_remote_url = image_url('image') || '' unless skip_download? - @account.public_key = public_key || '' @account.statuses_count = outbox_total_items if outbox_total_items.present? @account.following_count = following_total_items if following_total_items.present? @account.followers_count = followers_total_items if followers_total_items.present? @@ -140,6 +150,12 @@ class ActivityPub::ProcessAccountService < BaseService VerifyAccountLinksWorker.perform_async(@account.id) end + def process_duplicate_accounts! + return unless Account.where(uri: @account.uri).where.not(id: @account.id).exists? + + AccountMergingWorker.perform_async(@account.id) + end + def actor_type if @json['type'].is_a?(Array) @json['type'].find { |type| ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES.include?(type) } diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb index 74b0b82d0..3301aaf51 100644 --- a/app/services/resolve_account_service.rb +++ b/app/services/resolve_account_service.rb @@ -49,7 +49,7 @@ class ResolveAccountService < BaseService # Now it is certain, it is definitely a remote account, and it # either needs to be created, or updated from fresh data - process_account! + fetch_account! rescue Webfinger::Error, Oj::ParseError => e Rails.logger.debug "Webfinger query for #{@uri} failed: #{e}" nil @@ -104,16 +104,12 @@ class ResolveAccountService < BaseService acct.gsub(/\Aacct:/, '').split('@') end - def process_account! + def fetch_account! return unless activitypub_ready? RedisLock.acquire(lock_options) do |lock| if lock.acquired? - @account = Account.find_remote(@username, @domain) - - next if actor_json.nil? - - @account = ActivityPub::ProcessAccountService.new.call(@username, @domain, actor_json) + @account = ActivityPub::FetchRemoteAccountService.new.call(actor_url) else raise Mastodon::RaceConditionError end @@ -136,13 +132,6 @@ class ResolveAccountService < BaseService @actor_url ||= @webfinger.link('self', 'href') end - def actor_json - return @actor_json if defined?(@actor_json) - - json = fetch_resource(actor_url, false) - @actor_json = supported_context?(json) && equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) ? json : nil - end - def gone_from_origin? @gone end diff --git a/app/workers/account_merging_worker.rb b/app/workers/account_merging_worker.rb new file mode 100644 index 000000000..8c234e7ac --- /dev/null +++ b/app/workers/account_merging_worker.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AccountMergingWorker + include Sidekiq::Worker + + sidekiq_options queue: 'pull' + + def perform(account_id) + account = Account.find(account_id) + + return true if account.nil? || account.local? + + Account.where(uri: account.uri).where.not(id: account.id).find_each do |duplicate| + account.merge_with!(duplicate) + duplicate.destroy + end + end +end diff --git a/lib/mastodon/accounts_cli.rb b/lib/mastodon/accounts_cli.rb index bef4093a8..474643869 100644 --- a/lib/mastodon/accounts_cli.rb +++ b/lib/mastodon/accounts_cli.rb @@ -236,6 +236,25 @@ module Mastodon say('OK', :green) end + desc 'fix-duplicates', 'Find duplicate remote accounts and merge them' + option :dry_run, type: :boolean + long_desc <<-LONG_DESC + Merge known remote accounts sharing an ActivityPub actor identifier. + + Such duplicates can occur when a remote server admin misconfigures their + domain configuration. + LONG_DESC + def fix_duplicates + Account.remote.select(:uri, 'count(*)').group(:uri).having('count(*) > 1').pluck_each(:uri) do |uri| + say("Duplicates found for #{uri}") + begin + ActivityPub::FetchRemotAccountService.new.call(uri) unless options[:dry_run] + rescue => e + say("Error processing #{uri}: #{e}", :red) + end + end + end + desc 'backup USERNAME', 'Request a backup for a user' long_desc <<-LONG_DESC Request a new backup for an account with a given USERNAME. diff --git a/spec/services/resolve_account_service_spec.rb b/spec/services/resolve_account_service_spec.rb index 5bd0ec264..a604e90b5 100644 --- a/spec/services/resolve_account_service_spec.rb +++ b/spec/services/resolve_account_service_spec.rb @@ -60,7 +60,22 @@ RSpec.describe ResolveAccountService, type: :service do context 'with a legitimate webfinger redirection' do before do - webfinger = { subject: 'acct:foo@ap.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo' }] } + webfinger = { subject: 'acct:foo@ap.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] } + stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + end + + it 'returns new remote account' do + account = subject.call('Foo@redirected.example.com') + + expect(account.activitypub?).to eq true + expect(account.acct).to eq 'foo@ap.example.com' + expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox' + end + end + + context 'with a misconfigured redirection' do + before do + webfinger = { subject: 'acct:Foo@redirected.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] } stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end @@ -75,9 +90,9 @@ RSpec.describe ResolveAccountService, type: :service do context 'with too many webfinger redirections' do before do - webfinger = { subject: 'acct:foo@evil.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo' }] } + webfinger = { subject: 'acct:foo@evil.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] } stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - webfinger2 = { subject: 'acct:foo@ap.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo' }] } + webfinger2 = { subject: 'acct:foo@ap.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] } stub_request(:get, 'https://evil.example.com/.well-known/webfinger?resource=acct:foo@evil.example.com').to_return(body: Oj.dump(webfinger2), headers: { 'Content-Type': 'application/jrd+json' }) end @@ -111,6 +126,41 @@ RSpec.describe ResolveAccountService, type: :service do end end + context 'with an already-known actor changing acct: URI' do + let!(:duplicate) { Fabricate(:account, username: 'foo', domain: 'old.example.com', uri: 'https://ap.example.com/users/foo') } + let!(:status) { Fabricate(:status, account: duplicate, text: 'foo') } + + it 'returns new remote account' do + account = subject.call('foo@ap.example.com') + + expect(account.activitypub?).to eq true + expect(account.domain).to eq 'ap.example.com' + expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox' + expect(account.uri).to eq 'https://ap.example.com/users/foo' + end + + it 'merges accounts' do + account = subject.call('foo@ap.example.com') + + expect(status.reload.account_id).to eq account.id + expect(Account.where(uri: account.uri).count).to eq 1 + end + end + + context 'with an already-known acct: URI changing ActivityPub id' do + let!(:old_account) { Fabricate(:account, username: 'foo', domain: 'ap.example.com', uri: 'https://old.example.com/users/foo', last_webfingered_at: nil) } + let!(:status) { Fabricate(:status, account: old_account, text: 'foo') } + + it 'returns new remote account' do + account = subject.call('foo@ap.example.com') + + expect(account.activitypub?).to eq true + expect(account.domain).to eq 'ap.example.com' + expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox' + expect(account.uri).to eq 'https://ap.example.com/users/foo' + end + end + it 'processes one remote account at a time using locks' do wait_for_start = true fail_occurred = false -- cgit From 7bf3c6e57b52cd9390f2140a1cc17292c281aacf Mon Sep 17 00:00:00 2001 From: ThibG Date: Sun, 20 Dec 2020 18:25:00 +0100 Subject: Fix AccountDeletionWorker crashing and clogging sidekiq queues (#15380) * Fix account deletion workers being queued multiple times for a single account * Fix poll votes being unnecessarily instantiated on poll deletion * Fix favourites being unnecessarily instantiated on status deletion * Remove inaccurate comments * Delete polls instead of destroying them Co-authored-by: Claire --- app/models/poll.rb | 2 +- app/models/status.rb | 2 +- app/services/batched_remove_status_service.rb | 3 --- app/services/delete_account_service.rb | 4 +++- app/workers/account_deletion_worker.rb | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) (limited to 'app/workers') diff --git a/app/models/poll.rb b/app/models/poll.rb index b5deafcc2..e1ca55252 100644 --- a/app/models/poll.rb +++ b/app/models/poll.rb @@ -25,7 +25,7 @@ class Poll < ApplicationRecord belongs_to :account belongs_to :status - has_many :votes, class_name: 'PollVote', inverse_of: :poll, dependent: :destroy + has_many :votes, class_name: 'PollVote', inverse_of: :poll, dependent: :delete_all has_many :notifications, as: :activity, dependent: :destroy diff --git a/app/models/status.rb b/app/models/status.rb index 96d90e1c2..f1b3b75ce 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -56,7 +56,7 @@ class Status < ApplicationRecord belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true - has_many :favourites, inverse_of: :status, dependent: :destroy + has_many :favourites, inverse_of: :status, dependent: :delete_all has_many :bookmarks, inverse_of: :status, dependent: :destroy has_many :reblogs, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblog, dependent: :destroy has_many :replies, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :thread diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index 2295a01dc..28e5468b3 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -4,8 +4,6 @@ class BatchedRemoveStatusService < BaseService include Redisable # Delete given statuses and reblogs of them - # Dispatch PuSH updates of the deleted statuses, but only local ones - # Dispatch Salmon deletes, unique per domain, of the deleted statuses, but only local ones # Remove statuses from home feeds # Push delete events to streaming API for home feeds and public feeds # @param [Enumerable] statuses A preferably batched array of statuses @@ -19,7 +17,6 @@ class BatchedRemoveStatusService < BaseService @json_payloads = statuses.each_with_object({}) { |s, h| h[s.id] = Oj.dump(event: :delete, payload: s.id.to_s) } - # Ensure that rendered XML reflects destroyed state statuses.each do |status| status.mark_for_mass_destruction! status.destroy diff --git a/app/services/delete_account_service.rb b/app/services/delete_account_service.rb index 9cb80c95a..fe9b30b17 100644 --- a/app/services/delete_account_service.rb +++ b/app/services/delete_account_service.rb @@ -122,7 +122,9 @@ class DeleteAccountService < BaseService @account.polls.reorder(nil).find_each do |poll| next if @options[:reserve_username] && reported_status_ids.include?(poll.status_id) - poll.destroy + # We can safely delete the poll rather than destroy it, as any non-reported + # status should have been deleted already + poll.delete end associations_for_destruction.each do |association_name| diff --git a/app/workers/account_deletion_worker.rb b/app/workers/account_deletion_worker.rb index 98b67419d..fdf013e01 100644 --- a/app/workers/account_deletion_worker.rb +++ b/app/workers/account_deletion_worker.rb @@ -3,7 +3,7 @@ class AccountDeletionWorker include Sidekiq::Worker - sidekiq_options queue: 'pull' + sidekiq_options queue: 'pull', lock: :until_executed def perform(account_id, options = {}) reserve_username = options.with_indifferent_access.fetch(:reserve_username, true) -- cgit From 3249d35bdcd9a495af3277dfb4b2129d7ef80f15 Mon Sep 17 00:00:00 2001 From: ThibG Date: Tue, 22 Dec 2020 23:57:46 +0100 Subject: Improve account deletion performances further (#15407) * Delete status records by batches of 50 * Do not precompute values that are only used once * Do not generate redis events for removal of public toots older than two weeks * Filter reported toots a priori for polls and status deletion * Do not process reblogs when cleaning up public timelines As in Mastodon proper, reblogs don't appear in public TLs * Clean the deleted account's own feed in one go * Refactor Account#clean_feed_manager and List#clean_feed_manager * Delete instead of destroy a few more associations * Fix preloading Co-authored-by: Claire --- app/lib/feed_manager.rb | 30 ++++++++++++++++++++++ app/models/account.rb | 13 +--------- app/models/list.rb | 13 +--------- app/services/batched_remove_status_service.rb | 24 +++++------------ app/services/delete_account_service.rb | 20 +++++++++------ app/workers/scheduler/feed_cleanup_scheduler.rb | 30 ++-------------------- .../services/batched_remove_status_service_spec.rb | 4 --- 7 files changed, 53 insertions(+), 81 deletions(-) (limited to 'app/workers') diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 5e01ef67a..f0ad3e21f 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -230,6 +230,36 @@ class FeedManager end end + # Completely clear multiple feeds at once + # @param [Symbol] type + # @param [Array] ids + # @return [void] + def clean_feeds!(type, ids) + reblogged_id_sets = {} + + redis.pipelined do + ids.each do |feed_id| + redis.del(key(type, feed_id)) + reblog_key = key(type, feed_id, 'reblogs') + # We collect a future for this: we don't block while getting + # it, but we can iterate over it later. + reblogged_id_sets[feed_id] = redis.zrange(reblog_key, 0, -1) + redis.del(reblog_key) + end + end + + # Remove all of the reblog tracking keys we just removed the + # references to. + redis.pipelined do + reblogged_id_sets.each do |feed_id, future| + future.value.each do |reblogged_id| + reblog_set_key = key(type, feed_id, "reblogs:#{reblogged_id}") + redis.del(reblog_set_key) + end + end + end + end + private # Trim a feed to maximum size by removing older items diff --git a/app/models/account.rb b/app/models/account.rb index 80eb92a71..e6cf03fa8 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -578,17 +578,6 @@ class Account < ApplicationRecord end def clean_feed_manager - reblog_key = FeedManager.instance.key(:home, id, 'reblogs') - reblogged_id_set = Redis.current.zrange(reblog_key, 0, -1) - - Redis.current.pipelined do - Redis.current.del(FeedManager.instance.key(:home, id)) - Redis.current.del(reblog_key) - - reblogged_id_set.each do |reblogged_id| - reblog_set_key = FeedManager.instance.key(:home, id, "reblogs:#{reblogged_id}") - Redis.current.del(reblog_set_key) - end - end + FeedManager.instance.clean_feeds!(:home, [id]) end end diff --git a/app/models/list.rb b/app/models/list.rb index 655d55ff6..cdc6ebdb3 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -34,17 +34,6 @@ class List < ApplicationRecord private def clean_feed_manager - reblog_key = FeedManager.instance.key(:list, id, 'reblogs') - reblogged_id_set = Redis.current.zrange(reblog_key, 0, -1) - - Redis.current.pipelined do - Redis.current.del(FeedManager.instance.key(:list, id)) - Redis.current.del(reblog_key) - - reblogged_id_set.each do |reblogged_id| - reblog_set_key = FeedManager.instance.key(:list, id, "reblogs:#{reblogged_id}") - Redis.current.del(reblog_set_key) - end - end + FeedManager.instance.clean_feeds!(:list, [id]) end end diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index 3ec000110..61617d958 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -8,7 +8,7 @@ class BatchedRemoveStatusService < BaseService # @param [Hash] options # @option [Boolean] :skip_side_effects Do not modify feeds and send updates to streaming API def call(statuses, **options) - ActiveRecord::Associations::Preloader.new.preload(statuses, options[:skip_side_effects] ? :reblogs : [:account, reblogs: :account]) + ActiveRecord::Associations::Preloader.new.preload(statuses, options[:skip_side_effects] ? :reblogs : [:account, :tags, reblogs: :account]) statuses_and_reblogs = statuses.flat_map { |status| [status] + status.reblogs } @@ -27,7 +27,7 @@ class BatchedRemoveStatusService < BaseService # transaction lock the database, but we use the delete method instead # of destroy to avoid all callbacks. We rely on foreign keys to # cascade the delete faster without loading the associations. - statuses_and_reblogs.each(&:delete) + statuses_and_reblogs.each_slice(50) { |slice| Status.where(id: slice.map(&:id)).delete_all } # Since we skipped all callbacks, we also need to manually # deindex the statuses @@ -35,11 +35,6 @@ class BatchedRemoveStatusService < BaseService return if options[:skip_side_effects] - ActiveRecord::Associations::Preloader.new.preload(statuses_and_reblogs, :tags) - - @tags = statuses_and_reblogs.each_with_object({}) { |s, h| h[s.id] = s.tags.map { |tag| tag.name.mb_chars.downcase } } - @json_payloads = statuses_and_reblogs.each_with_object({}) { |s, h| h[s.id] = Oj.dump(event: :delete, payload: s.id.to_s) } - # Batch by source account statuses_and_reblogs.group_by(&:account_id).each_value do |account_statuses| account = account_statuses.first.account @@ -51,8 +46,9 @@ class BatchedRemoveStatusService < BaseService end # Cannot be batched + @status_id_cutoff = Mastodon::Snowflake.id_at(2.weeks.ago) redis.pipelined do - statuses_and_reblogs.each do |status| + statuses.each do |status| unpush_from_public_timelines(status) end end @@ -66,12 +62,6 @@ class BatchedRemoveStatusService < BaseService FeedManager.instance.unpush_from_home(follower, status) end end - - return unless account.local? - - statuses.each do |status| - FeedManager.instance.unpush_from_home(account, status) - end end def unpush_from_list_timelines(account, statuses) @@ -83,9 +73,9 @@ class BatchedRemoveStatusService < BaseService end def unpush_from_public_timelines(status) - return unless status.public_visibility? + return unless status.public_visibility? && status.id > @status_id_cutoff - payload = @json_payloads[status.id] + payload = Oj.dump(event: :delete, payload: status.id.to_s) redis.publish('timeline:public', payload) redis.publish(status.local? ? 'timeline:public:local' : 'timeline:public:remote', payload) @@ -95,7 +85,7 @@ class BatchedRemoveStatusService < BaseService redis.publish(status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', payload) end - @tags[status.id].each do |hashtag| + status.tags.map { |tag| tag.name.mb_chars.downcase }.each do |hashtag| redis.publish("timeline:hashtag:#{hashtag}", payload) redis.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local? end diff --git a/app/services/delete_account_service.rb b/app/services/delete_account_service.rb index 5123a4697..58f6ef2ab 100644 --- a/app/services/delete_account_service.rb +++ b/app/services/delete_account_service.rb @@ -46,10 +46,12 @@ class DeleteAccountService < BaseService featured_tags follow_requests identity_proofs + list_accounts migrations mute_relationships muted_by_relationships notifications + owned_lists scheduled_statuses status_pins ) @@ -145,15 +147,14 @@ class DeleteAccountService < BaseService purge_media_attachments! purge_polls! purge_generated_notifications! + purge_feeds! purge_other_associations! @account.destroy unless keep_account_record? end def purge_statuses! - @account.statuses.reorder(nil).find_in_batches do |statuses| - statuses.reject! { |status| reported_status_ids.include?(status.id) } if keep_account_record? - + @account.statuses.reorder(nil).where.not(id: reported_status_ids).in_batches do |statuses| BatchedRemoveStatusService.new.call(statuses, skip_side_effects: skip_side_effects?) end end @@ -167,11 +168,7 @@ class DeleteAccountService < BaseService end def purge_polls! - @account.polls.reorder(nil).find_each do |poll| - next if keep_account_record? && reported_status_ids.include?(poll.status_id) - - poll.delete - end + @account.polls.reorder(nil).where.not(status_id: reported_status_ids).in_batches.delete_all end def purge_generated_notifications! @@ -187,6 +184,13 @@ class DeleteAccountService < BaseService end end + def purge_feeds! + return unless @account.local? + + FeedManager.instance.clean_feeds!(:home, [@account.id]) + FeedManager.instance.clean_feeds!(:list, @account.owned_lists.pluck(:id)) + end + def purge_profile! # If the account is going to be destroyed # there is no point wasting time updating diff --git a/app/workers/scheduler/feed_cleanup_scheduler.rb b/app/workers/scheduler/feed_cleanup_scheduler.rb index 458fe6193..42b29f4ec 100644 --- a/app/workers/scheduler/feed_cleanup_scheduler.rb +++ b/app/workers/scheduler/feed_cleanup_scheduler.rb @@ -14,37 +14,11 @@ class Scheduler::FeedCleanupScheduler private def clean_home_feeds! - clean_feeds!(inactive_account_ids, :home) + feed_manager.clean_feeds!(:home, inactive_account_ids) end def clean_list_feeds! - clean_feeds!(inactive_list_ids, :list) - end - - def clean_feeds!(ids, type) - reblogged_id_sets = {} - - redis.pipelined do - ids.each do |feed_id| - redis.del(feed_manager.key(type, feed_id)) - reblog_key = feed_manager.key(type, feed_id, 'reblogs') - # We collect a future for this: we don't block while getting - # it, but we can iterate over it later. - reblogged_id_sets[feed_id] = redis.zrange(reblog_key, 0, -1) - redis.del(reblog_key) - end - end - - # Remove all of the reblog tracking keys we just removed the - # references to. - redis.pipelined do - reblogged_id_sets.each do |feed_id, future| - future.value.each do |reblogged_id| - reblog_set_key = feed_manager.key(type, feed_id, "reblogs:#{reblogged_id}") - redis.del(reblog_set_key) - end - end - end + feed_manager.clean_feeds!(:list, inactive_list_ids) end def inactive_account_ids diff --git a/spec/services/batched_remove_status_service_spec.rb b/spec/services/batched_remove_status_service_spec.rb index 239859f06..c1f54a6fd 100644 --- a/spec/services/batched_remove_status_service_spec.rb +++ b/spec/services/batched_remove_status_service_spec.rb @@ -43,10 +43,6 @@ RSpec.describe BatchedRemoveStatusService, type: :service do expect(Redis.current).to have_received(:publish).with("timeline:#{jeff.id}", any_args).at_least(:once) end - it 'notifies streaming API of author' do - expect(Redis.current).to have_received(:publish).with("timeline:#{alice.id}", any_args).at_least(:once) - end - it 'notifies streaming API of public timeline' do expect(Redis.current).to have_received(:publish).with('timeline:public', any_args).at_least(:once) end -- cgit