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:
parent
b29132ebdc
commit
7fcfebe772
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue