FIX: Handle timeout errors when sending push notifications (#13312)

Decreases the timeout from 60 to 5 seconds and counts timeouts as errors. It also refactors existing specs to reduce duplicate code.
This commit is contained in:
Gerhard Schlager 2021-06-07 20:46:07 +02:00 committed by GitHub
parent b29132ebdc
commit 7fcfebe772
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 87 additions and 93 deletions

View File

@ -2,6 +2,7 @@
class PushNotificationPusher class PushNotificationPusher
TOKEN_VALID_FOR_SECONDS ||= 5 * 60 TOKEN_VALID_FOR_SECONDS ||= 5 * 60
CONNECTION_TIMEOUT_SECONDS = 5
def self.push(user, payload) def self.push(user, payload)
I18n.with_locale(user.effective_locale) do I18n.with_locale(user.effective_locale) do
@ -76,7 +77,7 @@ class PushNotificationPusher
MAX_ERRORS ||= 3 MAX_ERRORS ||= 3
MIN_ERROR_DURATION ||= 86400 # 1 day MIN_ERROR_DURATION ||= 86400 # 1 day
def self.handle_generic_error(subscription) def self.handle_generic_error(subscription, error, user, endpoint, message)
subscription.error_count += 1 subscription.error_count += 1
subscription.first_error_at ||= Time.zone.now subscription.first_error_at ||= Time.zone.now
@ -86,6 +87,16 @@ class PushNotificationPusher
else else
subscription.save! subscription.save!
end end
Discourse.warn_exception(
error,
message: "Failed to send push notification",
env: {
user_id: user.id,
endpoint: endpoint,
message: message.to_json
}
)
end end
def self.send_notification(user, subscription, message) def self.send_notification(user, subscription, message)
@ -111,7 +122,10 @@ class PushNotificationPusher
public_key: SiteSetting.vapid_public_key, public_key: SiteSetting.vapid_public_key,
private_key: SiteSetting.vapid_private_key, private_key: SiteSetting.vapid_private_key,
expiration: TOKEN_VALID_FOR_SECONDS expiration: TOKEN_VALID_FOR_SECONDS
} },
open_timeout: CONNECTION_TIMEOUT_SECONDS,
read_timeout: CONNECTION_TIMEOUT_SECONDS,
ssl_timeout: CONNECTION_TIMEOUT_SECONDS
) )
if subscription.first_error_at || subscription.error_count != 0 if subscription.first_error_at || subscription.error_count != 0
@ -123,17 +137,10 @@ class PushNotificationPusher
if e.response.message == "MismatchSenderId" if e.response.message == "MismatchSenderId"
subscription.destroy! subscription.destroy!
else else
handle_generic_error(subscription) handle_generic_error(subscription, e, user, endpoint, message)
Discourse.warn_exception(
e,
message: "Failed to send push notification",
env: {
user_id: user.id,
endpoint: endpoint,
message: message.to_json
}
)
end end
rescue Timeout::Error => e
handle_generic_error(subscription, e, user, endpoint, message)
end end
end end

View File

@ -16,10 +16,11 @@ RSpec.describe PushNotificationPusher do
.to eq(UrlHelper.absolute(upload.url)) .to eq(UrlHelper.absolute(upload.url))
end end
it "sends notification in user's locale" do context "with user" do
SiteSetting.allow_user_locale = true fab!(:user) { Fabricate(:user) }
user = Fabricate(:user, locale: 'pt_BR')
data = <<~JSON def create_subscription
data = <<~JSON
{ {
"endpoint": "endpoint", "endpoint": "endpoint",
"keys": { "keys": {
@ -27,47 +28,11 @@ RSpec.describe PushNotificationPusher do
"auth": "auth" "auth": "auth"
} }
} }
JSON JSON
PushSubscription.create!(user_id: user.id, data: data) PushSubscription.create!(user_id: user.id, data: data)
end
Webpush.expects(:payload_send).with do |*args| def execute_push
args.to_s.include?("system mencionou")
end.once
PushNotificationPusher.push(user, {
topic_title: 'Topic',
username: 'system',
excerpt: 'description',
topic_id: 1,
post_url: "https://example.com/t/1/2",
notification_type: 1
})
end
it "deletes subscriptions which are erroring regularly" do
start = freeze_time
user = Fabricate(:user)
data = <<~JSON
{
"endpoint": "endpoint",
"keys": {
"p256dh": "p256dh",
"auth": "auth"
}
}
JSON
sub = PushSubscription.create!(user_id: user.id, data: data)
response = Struct.new(:body, :inspect, :message).new("test", "test", "failed")
error = Webpush::ResponseError.new(response, "localhost")
Webpush.expects(:payload_send).raises(error).times(4)
# 3 failures in more than 24 hours
3.times do
PushNotificationPusher.push(user, { PushNotificationPusher.push(user, {
topic_title: 'Topic', topic_title: 'Topic',
username: 'system', username: 'system',
@ -76,55 +41,77 @@ RSpec.describe PushNotificationPusher do
post_url: "https://example.com/t/1/2", post_url: "https://example.com/t/1/2",
notification_type: 1 notification_type: 1
}) })
freeze_time 1.minute.from_now
end end
sub.reload it "sends notification in user's locale" do
expect(sub.error_count).to eq(3) SiteSetting.allow_user_locale = true
expect(sub.first_error_at).to eq_time(start) user.update!(locale: 'pt_BR')
freeze_time(2.days.from_now) Webpush.expects(:payload_send).with do |*args|
args.to_s.include?("system mencionou")
end.once
PushNotificationPusher.push(user, { create_subscription
topic_title: 'Topic', execute_push
username: 'system', end
excerpt: 'description',
topic_id: 1,
post_url: "https://example.com/t/1/2",
notification_type: 1
})
expect(PushSubscription.where(id: sub.id).exists?).to eq(false) it "deletes subscriptions which are erroring regularly" do
end start = freeze_time
it "deletes invalid subscriptions during send" do sub = create_subscription
user = Fabricate(:walter_white)
missing_endpoint = PushSubscription.create!(user_id: user.id, data: response = Struct.new(:body, :inspect, :message).new("test", "test", "failed")
{ p256dh: "public ECDH key", keys: { auth: "private ECDH key" } }.to_json) error = Webpush::ResponseError.new(response, "localhost")
missing_p256dh = PushSubscription.create!(user_id: user.id, data: Webpush.expects(:payload_send).raises(error).times(4)
{ endpoint: "endpoint 1", keys: { auth: "private ECDH key" } }.to_json)
missing_auth = PushSubscription.create!(user_id: user.id, data: # 3 failures in more than 24 hours
{ endpoint: "endpoint 2", keys: { p256dh: "public ECDH key" } }.to_json) 3.times do
execute_push
valid_subscription = PushSubscription.create!(user_id: user.id, data: freeze_time 1.minute.from_now
{ endpoint: "endpoint 3", keys: { p256dh: "public ECDH key", auth: "private ECDH key" } }.to_json) end
expect(PushSubscription.where(user_id: user.id)).to contain_exactly(missing_endpoint, missing_p256dh, missing_auth, valid_subscription) sub.reload
Webpush.expects(:payload_send).with(has_entries(endpoint: "endpoint 3", p256dh: "public ECDH key", auth: "private ECDH key")).once expect(sub.error_count).to eq(3)
expect(sub.first_error_at).to eq_time(start)
PushNotificationPusher.push(user, { freeze_time(2.days.from_now)
topic_title: 'Topic',
username: 'system',
excerpt: 'description',
topic_id: 1,
post_url: "https://example.com/t/1/2",
notification_type: 1
})
expect(PushSubscription.where(user_id: user.id)).to contain_exactly(valid_subscription) execute_push
expect(PushSubscription.where(id: sub.id).exists?).to eq(false)
end
it "deletes invalid subscriptions during send" do
missing_endpoint = PushSubscription.create!(user_id: user.id, data:
{ p256dh: "public ECDH key", keys: { auth: "private ECDH key" } }.to_json)
missing_p256dh = PushSubscription.create!(user_id: user.id, data:
{ endpoint: "endpoint 1", keys: { auth: "private ECDH key" } }.to_json)
missing_auth = PushSubscription.create!(user_id: user.id, data:
{ endpoint: "endpoint 2", keys: { p256dh: "public ECDH key" } }.to_json)
valid_subscription = PushSubscription.create!(user_id: user.id, data:
{ endpoint: "endpoint 3", keys: { p256dh: "public ECDH key", auth: "private ECDH key" } }.to_json)
expect(PushSubscription.where(user_id: user.id)).to contain_exactly(missing_endpoint, missing_p256dh, missing_auth, valid_subscription)
Webpush.expects(:payload_send).with(has_entries(endpoint: "endpoint 3", p256dh: "public ECDH key", auth: "private ECDH key")).once
execute_push
expect(PushSubscription.where(user_id: user.id)).to contain_exactly(valid_subscription)
end
it "handles timeouts" do
Webpush.expects(:payload_send).raises(Net::ReadTimeout.new)
subscription = create_subscription
expect { execute_push }.to_not raise_exception
subscription.reload
expect(subscription.error_count).to eq(1)
end
end end
end end