mirror of
https://github.com/mastodon/mastodon.git
synced 2025-09-06 18:01:05 +00:00
Add delivery failure handling to FASP jobs (#35723)
This commit is contained in:
parent
1fd147bf2b
commit
868c46bc76
|
@ -9,6 +9,7 @@
|
||||||
# capabilities :jsonb not null
|
# capabilities :jsonb not null
|
||||||
# confirmed :boolean default(FALSE), not null
|
# confirmed :boolean default(FALSE), not null
|
||||||
# contact_email :string
|
# contact_email :string
|
||||||
|
# delivery_last_failed_at :datetime
|
||||||
# fediverse_account :string
|
# fediverse_account :string
|
||||||
# name :string not null
|
# name :string not null
|
||||||
# privacy_policy :jsonb
|
# privacy_policy :jsonb
|
||||||
|
@ -22,6 +23,8 @@
|
||||||
class Fasp::Provider < ApplicationRecord
|
class Fasp::Provider < ApplicationRecord
|
||||||
include DebugConcern
|
include DebugConcern
|
||||||
|
|
||||||
|
RETRY_INTERVAL = 1.hour
|
||||||
|
|
||||||
has_many :fasp_backfill_requests, inverse_of: :fasp_provider, class_name: 'Fasp::BackfillRequest', dependent: :delete_all
|
has_many :fasp_backfill_requests, inverse_of: :fasp_provider, class_name: 'Fasp::BackfillRequest', dependent: :delete_all
|
||||||
has_many :fasp_debug_callbacks, inverse_of: :fasp_provider, class_name: 'Fasp::DebugCallback', dependent: :delete_all
|
has_many :fasp_debug_callbacks, inverse_of: :fasp_provider, class_name: 'Fasp::DebugCallback', dependent: :delete_all
|
||||||
has_many :fasp_subscriptions, inverse_of: :fasp_provider, class_name: 'Fasp::Subscription', dependent: :delete_all
|
has_many :fasp_subscriptions, inverse_of: :fasp_provider, class_name: 'Fasp::Subscription', dependent: :delete_all
|
||||||
|
@ -122,6 +125,16 @@ class Fasp::Provider < ApplicationRecord
|
||||||
@delivery_failure_tracker ||= DeliveryFailureTracker.new(base_url, resolution: :minutes)
|
@delivery_failure_tracker ||= DeliveryFailureTracker.new(base_url, resolution: :minutes)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def available?
|
||||||
|
delivery_failure_tracker.available? || retry_worthwile?
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_availability!
|
||||||
|
self.delivery_last_failed_at = (Time.current unless delivery_failure_tracker.available?)
|
||||||
|
|
||||||
|
save!
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def create_keypair
|
def create_keypair
|
||||||
|
@ -148,4 +161,8 @@ class Fasp::Provider < ApplicationRecord
|
||||||
Fasp::Request.new(self).delete(path)
|
Fasp::Request.new(self).delete(path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def retry_worthwile?
|
||||||
|
delivery_last_failed_at && delivery_last_failed_at < RETRY_INTERVAL.ago
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Fasp::AccountSearchWorker
|
class Fasp::AccountSearchWorker < Fasp::BaseWorker
|
||||||
include Sidekiq::Worker
|
sidekiq_options retry: 0
|
||||||
|
|
||||||
sidekiq_options queue: 'fasp', retry: 0
|
|
||||||
|
|
||||||
def perform(query)
|
def perform(query)
|
||||||
return unless Mastodon::Feature.fasp_enabled?
|
return unless Mastodon::Feature.fasp_enabled?
|
||||||
|
@ -17,11 +15,13 @@ class Fasp::AccountSearchWorker
|
||||||
fetch_service = ActivityPub::FetchRemoteActorService.new
|
fetch_service = ActivityPub::FetchRemoteActorService.new
|
||||||
|
|
||||||
account_search_providers.each do |provider|
|
account_search_providers.each do |provider|
|
||||||
Fasp::Request.new(provider).get("/account_search/v0/search?#{params}").each do |uri|
|
with_provider(provider) do
|
||||||
next if Account.where(uri:).any?
|
Fasp::Request.new(provider).get("/account_search/v0/search?#{params}").each do |uri|
|
||||||
|
next if Account.where(uri:).any?
|
||||||
|
|
||||||
account = fetch_service.call(uri)
|
account = fetch_service.call(uri)
|
||||||
async_refresh.increment_result_count(by: 1) if account.present?
|
async_refresh.increment_result_count(by: 1) if account.present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
ensure
|
ensure
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Fasp::AnnounceAccountLifecycleEventWorker
|
class Fasp::AnnounceAccountLifecycleEventWorker < Fasp::BaseWorker
|
||||||
include Sidekiq::Worker
|
sidekiq_options retry: 5
|
||||||
|
|
||||||
sidekiq_options queue: 'fasp', retry: 5
|
|
||||||
|
|
||||||
def perform(uri, event_type)
|
def perform(uri, event_type)
|
||||||
Fasp::Subscription.includes(:fasp_provider).category_account.lifecycle.each do |subscription|
|
Fasp::Subscription.includes(:fasp_provider).category_account.lifecycle.each do |subscription|
|
||||||
announce(subscription, uri, event_type)
|
with_provider(subscription.fasp_provider) do
|
||||||
|
announce(subscription, uri, event_type)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Fasp::AnnounceContentLifecycleEventWorker
|
class Fasp::AnnounceContentLifecycleEventWorker < Fasp::BaseWorker
|
||||||
include Sidekiq::Worker
|
sidekiq_options retry: 5
|
||||||
|
|
||||||
sidekiq_options queue: 'fasp', retry: 5
|
|
||||||
|
|
||||||
def perform(uri, event_type)
|
def perform(uri, event_type)
|
||||||
Fasp::Subscription.includes(:fasp_provider).category_content.lifecycle.each do |subscription|
|
Fasp::Subscription.includes(:fasp_provider).category_content.lifecycle.each do |subscription|
|
||||||
announce(subscription, uri, event_type)
|
with_provider(subscription.fasp_provider) do
|
||||||
|
announce(subscription, uri, event_type)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Fasp::AnnounceTrendWorker
|
class Fasp::AnnounceTrendWorker < Fasp::BaseWorker
|
||||||
include Sidekiq::Worker
|
sidekiq_options retry: 5
|
||||||
|
|
||||||
sidekiq_options queue: 'fasp', retry: 5
|
|
||||||
|
|
||||||
def perform(status_id, trend_source)
|
def perform(status_id, trend_source)
|
||||||
status = ::Status.includes(:account).find(status_id)
|
status = ::Status.includes(:account).find(status_id)
|
||||||
return unless status.account.indexable?
|
return unless status.account.indexable?
|
||||||
|
|
||||||
Fasp::Subscription.includes(:fasp_provider).category_content.trends.each do |subscription|
|
Fasp::Subscription.includes(:fasp_provider).category_content.trends.each do |subscription|
|
||||||
announce(subscription, status.uri) if trending?(subscription, status, trend_source)
|
with_provider(subscription.fasp_provider) do
|
||||||
|
announce(subscription, status.uri) if trending?(subscription, status, trend_source)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
# status might not exist anymore, in which case there is nothing to do
|
# status might not exist anymore, in which case there is nothing to do
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Fasp::BackfillWorker
|
class Fasp::BackfillWorker < Fasp::BaseWorker
|
||||||
include Sidekiq::Worker
|
sidekiq_options retry: 5
|
||||||
|
|
||||||
sidekiq_options queue: 'fasp', retry: 5
|
|
||||||
|
|
||||||
def perform(backfill_request_id)
|
def perform(backfill_request_id)
|
||||||
backfill_request = Fasp::BackfillRequest.find(backfill_request_id)
|
backfill_request = Fasp::BackfillRequest.find(backfill_request_id)
|
||||||
|
|
||||||
announce(backfill_request)
|
with_provider(backfill_request.fasp_provider) do
|
||||||
|
announce(backfill_request)
|
||||||
|
|
||||||
backfill_request.advance!
|
backfill_request.advance!
|
||||||
|
end
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
# ignore missing backfill requests
|
# ignore missing backfill requests
|
||||||
end
|
end
|
||||||
|
|
19
app/workers/fasp/base_worker.rb
Normal file
19
app/workers/fasp/base_worker.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Fasp::BaseWorker
|
||||||
|
include Sidekiq::Worker
|
||||||
|
|
||||||
|
sidekiq_options queue: 'fasp'
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def with_provider(provider)
|
||||||
|
return unless provider.available?
|
||||||
|
|
||||||
|
yield
|
||||||
|
rescue *Mastodon::HTTP_CONNECTION_ERRORS
|
||||||
|
raise if provider.available?
|
||||||
|
ensure
|
||||||
|
provider.update_availability!
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,9 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Fasp::FollowRecommendationWorker
|
class Fasp::FollowRecommendationWorker < Fasp::BaseWorker
|
||||||
include Sidekiq::Worker
|
sidekiq_options retry: 0
|
||||||
|
|
||||||
sidekiq_options queue: 'fasp', retry: 0
|
|
||||||
|
|
||||||
def perform(account_id)
|
def perform(account_id)
|
||||||
return unless Mastodon::Feature.fasp_enabled?
|
return unless Mastodon::Feature.fasp_enabled?
|
||||||
|
@ -20,14 +18,16 @@ class Fasp::FollowRecommendationWorker
|
||||||
fetch_service = ActivityPub::FetchRemoteActorService.new
|
fetch_service = ActivityPub::FetchRemoteActorService.new
|
||||||
|
|
||||||
follow_recommendation_providers.each do |provider|
|
follow_recommendation_providers.each do |provider|
|
||||||
Fasp::Request.new(provider).get("/follow_recommendation/v0/accounts?#{params}").each do |uri|
|
with_provider(provider) do
|
||||||
next if Account.where(uri:).any?
|
Fasp::Request.new(provider).get("/follow_recommendation/v0/accounts?#{params}").each do |uri|
|
||||||
|
next if Account.where(uri:).any?
|
||||||
|
|
||||||
new_account = fetch_service.call(uri)
|
new_account = fetch_service.call(uri)
|
||||||
|
|
||||||
if new_account.present?
|
if new_account.present?
|
||||||
Fasp::FollowRecommendation.find_or_create_by(requesting_account: account, recommended_account: new_account)
|
Fasp::FollowRecommendation.find_or_create_by(requesting_account: account, recommended_account: new_account)
|
||||||
async_refresh.increment_result_count(by: 1)
|
async_refresh.increment_result_count(by: 1)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddDeliveryLastFailedAtToFaspProviders < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :fasp_providers, :delivery_last_failed_at, :datetime
|
||||||
|
end
|
||||||
|
end
|
97
db/schema.rb
97
db/schema.rb
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2025_07_17_003848) do
|
ActiveRecord::Schema[8.0].define(version: 2025_08_05_075010) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_catalog.plpgsql"
|
enable_extension "pg_catalog.plpgsql"
|
||||||
|
|
||||||
|
@ -488,6 +488,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_17_003848) do
|
||||||
t.string "fediverse_account"
|
t.string "fediverse_account"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.datetime "delivery_last_failed_at"
|
||||||
t.index ["base_url"], name: "index_fasp_providers_on_base_url", unique: true
|
t.index ["base_url"], name: "index_fasp_providers_on_base_url", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1483,53 +1484,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_17_003848) do
|
||||||
add_foreign_key "web_settings", "users", name: "fk_11910667b2", on_delete: :cascade
|
add_foreign_key "web_settings", "users", name: "fk_11910667b2", on_delete: :cascade
|
||||||
add_foreign_key "webauthn_credentials", "users", on_delete: :cascade
|
add_foreign_key "webauthn_credentials", "users", on_delete: :cascade
|
||||||
|
|
||||||
create_view "instances", materialized: true, sql_definition: <<-SQL
|
|
||||||
WITH domain_counts(domain, accounts_count) AS (
|
|
||||||
SELECT accounts.domain,
|
|
||||||
count(*) AS accounts_count
|
|
||||||
FROM accounts
|
|
||||||
WHERE (accounts.domain IS NOT NULL)
|
|
||||||
GROUP BY accounts.domain
|
|
||||||
)
|
|
||||||
SELECT domain_counts.domain,
|
|
||||||
domain_counts.accounts_count
|
|
||||||
FROM domain_counts
|
|
||||||
UNION
|
|
||||||
SELECT domain_blocks.domain,
|
|
||||||
COALESCE(domain_counts.accounts_count, (0)::bigint) AS accounts_count
|
|
||||||
FROM (domain_blocks
|
|
||||||
LEFT JOIN domain_counts ON (((domain_counts.domain)::text = (domain_blocks.domain)::text)))
|
|
||||||
UNION
|
|
||||||
SELECT domain_allows.domain,
|
|
||||||
COALESCE(domain_counts.accounts_count, (0)::bigint) AS accounts_count
|
|
||||||
FROM (domain_allows
|
|
||||||
LEFT JOIN domain_counts ON (((domain_counts.domain)::text = (domain_allows.domain)::text)));
|
|
||||||
SQL
|
|
||||||
add_index "instances", "reverse(('.'::text || (domain)::text)), domain", name: "index_instances_on_reverse_domain"
|
|
||||||
add_index "instances", ["domain"], name: "index_instances_on_domain", unique: true
|
|
||||||
|
|
||||||
create_view "user_ips", sql_definition: <<-SQL
|
|
||||||
SELECT user_id,
|
|
||||||
ip,
|
|
||||||
max(used_at) AS used_at
|
|
||||||
FROM ( SELECT users.id AS user_id,
|
|
||||||
users.sign_up_ip AS ip,
|
|
||||||
users.created_at AS used_at
|
|
||||||
FROM users
|
|
||||||
WHERE (users.sign_up_ip IS NOT NULL)
|
|
||||||
UNION ALL
|
|
||||||
SELECT session_activations.user_id,
|
|
||||||
session_activations.ip,
|
|
||||||
session_activations.updated_at
|
|
||||||
FROM session_activations
|
|
||||||
UNION ALL
|
|
||||||
SELECT login_activities.user_id,
|
|
||||||
login_activities.ip,
|
|
||||||
login_activities.created_at
|
|
||||||
FROM login_activities
|
|
||||||
WHERE (login_activities.success = true)) t0
|
|
||||||
GROUP BY user_id, ip;
|
|
||||||
SQL
|
|
||||||
create_view "account_summaries", materialized: true, sql_definition: <<-SQL
|
create_view "account_summaries", materialized: true, sql_definition: <<-SQL
|
||||||
SELECT accounts.id AS account_id,
|
SELECT accounts.id AS account_id,
|
||||||
mode() WITHIN GROUP (ORDER BY t0.language) AS language,
|
mode() WITHIN GROUP (ORDER BY t0.language) AS language,
|
||||||
|
@ -1580,4 +1534,51 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_17_003848) do
|
||||||
SQL
|
SQL
|
||||||
add_index "global_follow_recommendations", ["account_id"], name: "index_global_follow_recommendations_on_account_id", unique: true
|
add_index "global_follow_recommendations", ["account_id"], name: "index_global_follow_recommendations_on_account_id", unique: true
|
||||||
|
|
||||||
|
create_view "instances", materialized: true, sql_definition: <<-SQL
|
||||||
|
WITH domain_counts(domain, accounts_count) AS (
|
||||||
|
SELECT accounts.domain,
|
||||||
|
count(*) AS accounts_count
|
||||||
|
FROM accounts
|
||||||
|
WHERE (accounts.domain IS NOT NULL)
|
||||||
|
GROUP BY accounts.domain
|
||||||
|
)
|
||||||
|
SELECT domain_counts.domain,
|
||||||
|
domain_counts.accounts_count
|
||||||
|
FROM domain_counts
|
||||||
|
UNION
|
||||||
|
SELECT domain_blocks.domain,
|
||||||
|
COALESCE(domain_counts.accounts_count, (0)::bigint) AS accounts_count
|
||||||
|
FROM (domain_blocks
|
||||||
|
LEFT JOIN domain_counts ON (((domain_counts.domain)::text = (domain_blocks.domain)::text)))
|
||||||
|
UNION
|
||||||
|
SELECT domain_allows.domain,
|
||||||
|
COALESCE(domain_counts.accounts_count, (0)::bigint) AS accounts_count
|
||||||
|
FROM (domain_allows
|
||||||
|
LEFT JOIN domain_counts ON (((domain_counts.domain)::text = (domain_allows.domain)::text)));
|
||||||
|
SQL
|
||||||
|
add_index "instances", "reverse(('.'::text || (domain)::text)), domain", name: "index_instances_on_reverse_domain"
|
||||||
|
add_index "instances", ["domain"], name: "index_instances_on_domain", unique: true
|
||||||
|
|
||||||
|
create_view "user_ips", sql_definition: <<-SQL
|
||||||
|
SELECT user_id,
|
||||||
|
ip,
|
||||||
|
max(used_at) AS used_at
|
||||||
|
FROM ( SELECT users.id AS user_id,
|
||||||
|
users.sign_up_ip AS ip,
|
||||||
|
users.created_at AS used_at
|
||||||
|
FROM users
|
||||||
|
WHERE (users.sign_up_ip IS NOT NULL)
|
||||||
|
UNION ALL
|
||||||
|
SELECT session_activations.user_id,
|
||||||
|
session_activations.ip,
|
||||||
|
session_activations.updated_at
|
||||||
|
FROM session_activations
|
||||||
|
UNION ALL
|
||||||
|
SELECT login_activities.user_id,
|
||||||
|
login_activities.ip,
|
||||||
|
login_activities.created_at
|
||||||
|
FROM login_activities
|
||||||
|
WHERE (login_activities.success = true)) t0
|
||||||
|
GROUP BY user_id, ip;
|
||||||
|
SQL
|
||||||
end
|
end
|
||||||
|
|
|
@ -214,4 +214,102 @@ RSpec.describe Fasp::Provider do
|
||||||
expect(subject.delivery_failure_tracker).to be_a(DeliveryFailureTracker)
|
expect(subject.delivery_failure_tracker).to be_a(DeliveryFailureTracker)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#available?' do
|
||||||
|
subject { Fabricate(:fasp_provider, delivery_last_failed_at:) }
|
||||||
|
|
||||||
|
let(:delivery_last_failed_at) { nil }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(subject.delivery_failure_tracker).to receive(:available?).and_return(available)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the delivery failure tracker reports it is available' do
|
||||||
|
let(:available) { true }
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(subject.available?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the delivery failure tracker reports it is unavailable' do
|
||||||
|
let(:available) { false }
|
||||||
|
|
||||||
|
context 'when the last failure was more than one hour ago' do
|
||||||
|
let(:delivery_last_failed_at) { 61.minutes.ago }
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(subject.available?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the last failure is very recent' do
|
||||||
|
let(:delivery_last_failed_at) { 5.minutes.ago }
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(subject.available?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#update_availability!' do
|
||||||
|
subject { Fabricate(:fasp_provider, delivery_last_failed_at:) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(subject.delivery_failure_tracker).to receive(:available?).and_return(available)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when `delivery_last_failed_at` is `nil`' do
|
||||||
|
let(:delivery_last_failed_at) { nil }
|
||||||
|
|
||||||
|
context 'when the delivery failure tracker reports it is available' do
|
||||||
|
let(:available) { true }
|
||||||
|
|
||||||
|
it 'does not update the provider' do
|
||||||
|
subject.update_availability!
|
||||||
|
|
||||||
|
expect(subject.saved_changes?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the delivery failure tracker reports it is unavailable' do
|
||||||
|
let(:available) { false }
|
||||||
|
|
||||||
|
it 'sets `delivery_last_failed_at` to the current time' do
|
||||||
|
freeze_time
|
||||||
|
|
||||||
|
subject.update_availability!
|
||||||
|
|
||||||
|
expect(subject.delivery_last_failed_at).to eq Time.zone.now
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when `delivery_last_failed_at` is present' do
|
||||||
|
context 'when the delivery failure tracker reports it is available' do
|
||||||
|
let(:available) { true }
|
||||||
|
let(:delivery_last_failed_at) { 5.minutes.ago }
|
||||||
|
|
||||||
|
it 'sets `delivery_last_failed_at` to `nil`' do
|
||||||
|
subject.update_availability!
|
||||||
|
|
||||||
|
expect(subject.delivery_last_failed_at).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the delivery failure tracker reports it is unavailable' do
|
||||||
|
let(:available) { false }
|
||||||
|
let(:delivery_last_failed_at) { 5.minutes.ago }
|
||||||
|
|
||||||
|
it 'updates `delivery_last_failed_at` to the current time' do
|
||||||
|
freeze_time
|
||||||
|
|
||||||
|
subject.update_availability!
|
||||||
|
|
||||||
|
expect(subject.delivery_last_failed_at).to eq Time.zone.now
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
57
spec/support/examples/workers/fasp/delivery_failure.rb
Normal file
57
spec/support/examples/workers/fasp/delivery_failure.rb
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.shared_examples 'worker handling fasp delivery failures' do
|
||||||
|
context 'when provider is not available' do
|
||||||
|
before do
|
||||||
|
provider.update(delivery_last_failed_at: 1.minute.ago)
|
||||||
|
domain = Addressable::URI.parse(provider.base_url).normalized_host
|
||||||
|
UnavailableDomain.create!(domain:)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not attempt connecting and does not fail the job' do
|
||||||
|
expect { subject }.to_not raise_error
|
||||||
|
expect(stubbed_request).to_not have_been_made
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when connection to provider fails' do
|
||||||
|
before do
|
||||||
|
base_stubbed_request
|
||||||
|
.to_raise(HTTP::ConnectionError)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when provider becomes unavailable' do
|
||||||
|
before do
|
||||||
|
travel_to 5.minutes.ago
|
||||||
|
4.times do
|
||||||
|
provider.delivery_failure_tracker.track_failure!
|
||||||
|
travel_to 1.minute.since
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the provider and does not fail the job, so it will not be retried' do
|
||||||
|
expect { subject }.to_not raise_error
|
||||||
|
expect(provider.reload.delivery_last_failed_at).to eq Time.current
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when provider is still marked as available' do
|
||||||
|
it 'fails the job so it can be retried' do
|
||||||
|
expect { subject }.to raise_error(HTTP::ConnectionError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when connection to a previously unavailable provider succeeds' do
|
||||||
|
before do
|
||||||
|
provider.update(delivery_last_failed_at: 2.hours.ago)
|
||||||
|
domain = Addressable::URI.parse(provider.base_url).normalized_host
|
||||||
|
UnavailableDomain.create!(domain:)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'marks the provider as being available again' do
|
||||||
|
expect { subject }.to_not raise_error
|
||||||
|
expect(provider).to be_available
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,12 +5,14 @@ require 'rails_helper'
|
||||||
RSpec.describe Fasp::AccountSearchWorker, feature: :fasp do
|
RSpec.describe Fasp::AccountSearchWorker, feature: :fasp do
|
||||||
include ProviderRequestHelper
|
include ProviderRequestHelper
|
||||||
|
|
||||||
|
subject { described_class.new.perform('cats') }
|
||||||
|
|
||||||
let(:provider) { Fabricate(:account_search_fasp) }
|
let(:provider) { Fabricate(:account_search_fasp) }
|
||||||
let(:account) { Fabricate(:account) }
|
let(:account) { Fabricate(:account) }
|
||||||
let(:fetch_service) { instance_double(ActivityPub::FetchRemoteActorService, call: true) }
|
let(:fetch_service) { instance_double(ActivityPub::FetchRemoteActorService, call: true) }
|
||||||
|
let(:path) { '/account_search/v0/search?term=cats&limit=10' }
|
||||||
|
|
||||||
let!(:stubbed_request) do
|
let!(:stubbed_request) do
|
||||||
path = '/account_search/v0/search?term=cats&limit=10'
|
|
||||||
stub_provider_request(provider,
|
stub_provider_request(provider,
|
||||||
method: :get,
|
method: :get,
|
||||||
path:,
|
path:,
|
||||||
|
@ -25,7 +27,7 @@ RSpec.describe Fasp::AccountSearchWorker, feature: :fasp do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'requests search results and fetches received account uris' do
|
it 'requests search results and fetches received account uris' do
|
||||||
described_class.new.perform('cats')
|
subject
|
||||||
|
|
||||||
expect(stubbed_request).to have_been_made
|
expect(stubbed_request).to have_been_made
|
||||||
expect(fetch_service).to have_received(:call).with('https://fedi.example.com/accounts/2')
|
expect(fetch_service).to have_received(:call).with('https://fedi.example.com/accounts/2')
|
||||||
|
@ -35,7 +37,7 @@ RSpec.describe Fasp::AccountSearchWorker, feature: :fasp do
|
||||||
it 'marks a running async refresh as finished' do
|
it 'marks a running async refresh as finished' do
|
||||||
async_refresh = AsyncRefresh.create("fasp:account_search:#{Digest::MD5.base64digest('cats')}", count_results: true)
|
async_refresh = AsyncRefresh.create("fasp:account_search:#{Digest::MD5.base64digest('cats')}", count_results: true)
|
||||||
|
|
||||||
described_class.new.perform('cats')
|
subject
|
||||||
|
|
||||||
expect(async_refresh.reload).to be_finished
|
expect(async_refresh.reload).to be_finished
|
||||||
end
|
end
|
||||||
|
@ -43,8 +45,16 @@ RSpec.describe Fasp::AccountSearchWorker, feature: :fasp do
|
||||||
it 'tracks the number of fetched accounts in the async refresh' do
|
it 'tracks the number of fetched accounts in the async refresh' do
|
||||||
async_refresh = AsyncRefresh.create("fasp:account_search:#{Digest::MD5.base64digest('cats')}", count_results: true)
|
async_refresh = AsyncRefresh.create("fasp:account_search:#{Digest::MD5.base64digest('cats')}", count_results: true)
|
||||||
|
|
||||||
described_class.new.perform('cats')
|
subject
|
||||||
|
|
||||||
expect(async_refresh.reload.result_count).to eq 2
|
expect(async_refresh.reload.result_count).to eq 2
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'provider delivery failure handling' do
|
||||||
|
let(:base_stubbed_request) do
|
||||||
|
stub_request(:get, provider.url(path))
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like('worker handling fasp delivery failures')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,15 +5,19 @@ require 'rails_helper'
|
||||||
RSpec.describe Fasp::AnnounceAccountLifecycleEventWorker do
|
RSpec.describe Fasp::AnnounceAccountLifecycleEventWorker do
|
||||||
include ProviderRequestHelper
|
include ProviderRequestHelper
|
||||||
|
|
||||||
|
subject { described_class.new.perform(account_uri, 'new') }
|
||||||
|
|
||||||
let(:account_uri) { 'https://masto.example.com/accounts/1' }
|
let(:account_uri) { 'https://masto.example.com/accounts/1' }
|
||||||
let(:subscription) do
|
let(:subscription) do
|
||||||
Fabricate(:fasp_subscription, category: 'account')
|
Fabricate(:fasp_subscription, category: 'account')
|
||||||
end
|
end
|
||||||
let(:provider) { subscription.fasp_provider }
|
let(:provider) { subscription.fasp_provider }
|
||||||
|
let(:path) { '/data_sharing/v0/announcements' }
|
||||||
|
|
||||||
let!(:stubbed_request) do
|
let!(:stubbed_request) do
|
||||||
stub_provider_request(provider,
|
stub_provider_request(provider,
|
||||||
method: :post,
|
method: :post,
|
||||||
path: '/data_sharing/v0/announcements',
|
path:,
|
||||||
response_body: {
|
response_body: {
|
||||||
source: {
|
source: {
|
||||||
subscription: {
|
subscription: {
|
||||||
|
@ -27,8 +31,16 @@ RSpec.describe Fasp::AnnounceAccountLifecycleEventWorker do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sends the account uri to subscribed providers' do
|
it 'sends the account uri to subscribed providers' do
|
||||||
described_class.new.perform(account_uri, 'new')
|
subject
|
||||||
|
|
||||||
expect(stubbed_request).to have_been_made
|
expect(stubbed_request).to have_been_made
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'provider delivery failure handling' do
|
||||||
|
let(:base_stubbed_request) do
|
||||||
|
stub_request(:post, provider.url(path))
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like('worker handling fasp delivery failures')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,15 +5,19 @@ require 'rails_helper'
|
||||||
RSpec.describe Fasp::AnnounceContentLifecycleEventWorker do
|
RSpec.describe Fasp::AnnounceContentLifecycleEventWorker do
|
||||||
include ProviderRequestHelper
|
include ProviderRequestHelper
|
||||||
|
|
||||||
|
subject { described_class.new.perform(status_uri, 'new') }
|
||||||
|
|
||||||
let(:status_uri) { 'https://masto.example.com/status/1' }
|
let(:status_uri) { 'https://masto.example.com/status/1' }
|
||||||
let(:subscription) do
|
let(:subscription) do
|
||||||
Fabricate(:fasp_subscription)
|
Fabricate(:fasp_subscription)
|
||||||
end
|
end
|
||||||
let(:provider) { subscription.fasp_provider }
|
let(:provider) { subscription.fasp_provider }
|
||||||
|
let(:path) { '/data_sharing/v0/announcements' }
|
||||||
|
|
||||||
let!(:stubbed_request) do
|
let!(:stubbed_request) do
|
||||||
stub_provider_request(provider,
|
stub_provider_request(provider,
|
||||||
method: :post,
|
method: :post,
|
||||||
path: '/data_sharing/v0/announcements',
|
path:,
|
||||||
response_body: {
|
response_body: {
|
||||||
source: {
|
source: {
|
||||||
subscription: {
|
subscription: {
|
||||||
|
@ -27,8 +31,16 @@ RSpec.describe Fasp::AnnounceContentLifecycleEventWorker do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sends the status uri to subscribed providers' do
|
it 'sends the status uri to subscribed providers' do
|
||||||
described_class.new.perform(status_uri, 'new')
|
subject
|
||||||
|
|
||||||
expect(stubbed_request).to have_been_made
|
expect(stubbed_request).to have_been_made
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'provider delivery failure handling' do
|
||||||
|
let(:base_stubbed_request) do
|
||||||
|
stub_request(:post, provider.url(path))
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like('worker handling fasp delivery failures')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,6 +5,8 @@ require 'rails_helper'
|
||||||
RSpec.describe Fasp::AnnounceTrendWorker do
|
RSpec.describe Fasp::AnnounceTrendWorker do
|
||||||
include ProviderRequestHelper
|
include ProviderRequestHelper
|
||||||
|
|
||||||
|
subject { described_class.new.perform(status.id, 'favourite') }
|
||||||
|
|
||||||
let(:status) { Fabricate(:status) }
|
let(:status) { Fabricate(:status) }
|
||||||
let(:subscription) do
|
let(:subscription) do
|
||||||
Fabricate(:fasp_subscription,
|
Fabricate(:fasp_subscription,
|
||||||
|
@ -14,10 +16,12 @@ RSpec.describe Fasp::AnnounceTrendWorker do
|
||||||
threshold_likes: 2)
|
threshold_likes: 2)
|
||||||
end
|
end
|
||||||
let(:provider) { subscription.fasp_provider }
|
let(:provider) { subscription.fasp_provider }
|
||||||
|
let(:path) { '/data_sharing/v0/announcements' }
|
||||||
|
|
||||||
let!(:stubbed_request) do
|
let!(:stubbed_request) do
|
||||||
stub_provider_request(provider,
|
stub_provider_request(provider,
|
||||||
method: :post,
|
method: :post,
|
||||||
path: '/data_sharing/v0/announcements',
|
path:,
|
||||||
response_body: {
|
response_body: {
|
||||||
source: {
|
source: {
|
||||||
subscription: {
|
subscription: {
|
||||||
|
@ -36,15 +40,23 @@ RSpec.describe Fasp::AnnounceTrendWorker do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sends the account uri to subscribed providers' do
|
it 'sends the account uri to subscribed providers' do
|
||||||
described_class.new.perform(status.id, 'favourite')
|
subject
|
||||||
|
|
||||||
expect(stubbed_request).to have_been_made
|
expect(stubbed_request).to have_been_made
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'provider delivery failure handling' do
|
||||||
|
let(:base_stubbed_request) do
|
||||||
|
stub_request(:post, provider.url(path))
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like('worker handling fasp delivery failures')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the configured threshold is not met' do
|
context 'when the configured threshold is not met' do
|
||||||
it 'does not notify any provider' do
|
it 'does not notify any provider' do
|
||||||
described_class.new.perform(status.id, 'favourite')
|
subject
|
||||||
|
|
||||||
expect(stubbed_request).to_not have_been_made
|
expect(stubbed_request).to_not have_been_made
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,13 +5,17 @@ require 'rails_helper'
|
||||||
RSpec.describe Fasp::BackfillWorker do
|
RSpec.describe Fasp::BackfillWorker do
|
||||||
include ProviderRequestHelper
|
include ProviderRequestHelper
|
||||||
|
|
||||||
|
subject { described_class.new.perform(backfill_request.id) }
|
||||||
|
|
||||||
let(:backfill_request) { Fabricate(:fasp_backfill_request) }
|
let(:backfill_request) { Fabricate(:fasp_backfill_request) }
|
||||||
let(:provider) { backfill_request.fasp_provider }
|
let(:provider) { backfill_request.fasp_provider }
|
||||||
let(:status) { Fabricate(:status) }
|
let(:status) { Fabricate(:status) }
|
||||||
|
let(:path) { '/data_sharing/v0/announcements' }
|
||||||
|
|
||||||
let!(:stubbed_request) do
|
let!(:stubbed_request) do
|
||||||
stub_provider_request(provider,
|
stub_provider_request(provider,
|
||||||
method: :post,
|
method: :post,
|
||||||
path: '/data_sharing/v0/announcements',
|
path:,
|
||||||
response_body: {
|
response_body: {
|
||||||
source: {
|
source: {
|
||||||
backfillRequest: {
|
backfillRequest: {
|
||||||
|
@ -25,8 +29,16 @@ RSpec.describe Fasp::BackfillWorker do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sends status uri to provider that requested backfill' do
|
it 'sends status uri to provider that requested backfill' do
|
||||||
described_class.new.perform(backfill_request.id)
|
subject
|
||||||
|
|
||||||
expect(stubbed_request).to have_been_made
|
expect(stubbed_request).to have_been_made
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'provider delivery failure handling' do
|
||||||
|
let(:base_stubbed_request) do
|
||||||
|
stub_request(:post, provider.url(path))
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like('worker handling fasp delivery failures')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,13 +5,15 @@ require 'rails_helper'
|
||||||
RSpec.describe Fasp::FollowRecommendationWorker, feature: :fasp do
|
RSpec.describe Fasp::FollowRecommendationWorker, feature: :fasp do
|
||||||
include ProviderRequestHelper
|
include ProviderRequestHelper
|
||||||
|
|
||||||
|
subject { described_class.new.perform(account.id) }
|
||||||
|
|
||||||
let(:provider) { Fabricate(:follow_recommendation_fasp) }
|
let(:provider) { Fabricate(:follow_recommendation_fasp) }
|
||||||
let(:account) { Fabricate(:account) }
|
let(:account) { Fabricate(:account) }
|
||||||
|
let(:account_uri) { ActivityPub::TagManager.instance.uri_for(account) }
|
||||||
let(:fetch_service) { instance_double(ActivityPub::FetchRemoteActorService) }
|
let(:fetch_service) { instance_double(ActivityPub::FetchRemoteActorService) }
|
||||||
|
let(:path) { "/follow_recommendation/v0/accounts?accountUri=#{URI.encode_uri_component(account_uri)}" }
|
||||||
|
|
||||||
let!(:stubbed_request) do
|
let!(:stubbed_request) do
|
||||||
account_uri = ActivityPub::TagManager.instance.uri_for(account)
|
|
||||||
path = "/follow_recommendation/v0/accounts?accountUri=#{URI.encode_uri_component(account_uri)}"
|
|
||||||
stub_provider_request(provider,
|
stub_provider_request(provider,
|
||||||
method: :get,
|
method: :get,
|
||||||
path:,
|
path:,
|
||||||
|
@ -28,7 +30,7 @@ RSpec.describe Fasp::FollowRecommendationWorker, feature: :fasp do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "sends the requesting account's uri to provider and fetches received account uris" do
|
it "sends the requesting account's uri to provider and fetches received account uris" do
|
||||||
described_class.new.perform(account.id)
|
subject
|
||||||
|
|
||||||
expect(stubbed_request).to have_been_made
|
expect(stubbed_request).to have_been_made
|
||||||
expect(fetch_service).to have_received(:call).with('https://fedi.example.com/accounts/1')
|
expect(fetch_service).to have_received(:call).with('https://fedi.example.com/accounts/1')
|
||||||
|
@ -38,7 +40,7 @@ RSpec.describe Fasp::FollowRecommendationWorker, feature: :fasp do
|
||||||
it 'marks a running async refresh as finished' do
|
it 'marks a running async refresh as finished' do
|
||||||
async_refresh = AsyncRefresh.create("fasp:follow_recommendation:#{account.id}", count_results: true)
|
async_refresh = AsyncRefresh.create("fasp:follow_recommendation:#{account.id}", count_results: true)
|
||||||
|
|
||||||
described_class.new.perform(account.id)
|
subject
|
||||||
|
|
||||||
expect(async_refresh.reload).to be_finished
|
expect(async_refresh.reload).to be_finished
|
||||||
end
|
end
|
||||||
|
@ -46,14 +48,22 @@ RSpec.describe Fasp::FollowRecommendationWorker, feature: :fasp do
|
||||||
it 'tracks the number of fetched accounts in the async refresh' do
|
it 'tracks the number of fetched accounts in the async refresh' do
|
||||||
async_refresh = AsyncRefresh.create("fasp:follow_recommendation:#{account.id}", count_results: true)
|
async_refresh = AsyncRefresh.create("fasp:follow_recommendation:#{account.id}", count_results: true)
|
||||||
|
|
||||||
described_class.new.perform(account.id)
|
subject
|
||||||
|
|
||||||
expect(async_refresh.reload.result_count).to eq 2
|
expect(async_refresh.reload.result_count).to eq 2
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'persists the results' do
|
it 'persists the results' do
|
||||||
expect do
|
expect do
|
||||||
described_class.new.perform(account.id)
|
subject
|
||||||
end.to change(Fasp::FollowRecommendation, :count).by(2)
|
end.to change(Fasp::FollowRecommendation, :count).by(2)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'provider delivery failure handling' do
|
||||||
|
let(:base_stubbed_request) do
|
||||||
|
stub_request(:get, provider.url(path))
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like('worker handling fasp delivery failures')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue
Block a user