FIX: automatically expire bad push channels (#13156)
Previously we would retry push notifications indefinitely for all errors except for ExpiredSubscription Under certain conditions other persistent errors may arise such as a persistent rate limit. If we track more than 3 errors in a period of time longer than a day we will delete the subscription Also performs a bit of internal cleanup to ensure protected methods really are private.
This commit is contained in:
parent
b9053c5e77
commit
d45682716b
|
@ -2,15 +2,21 @@
|
||||||
|
|
||||||
class PushSubscription < ActiveRecord::Base
|
class PushSubscription < ActiveRecord::Base
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
|
||||||
|
def parsed_data
|
||||||
|
JSON.parse(data)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# == Schema Information
|
# == Schema Information
|
||||||
#
|
#
|
||||||
# Table name: push_subscriptions
|
# Table name: push_subscriptions
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# user_id :integer not null
|
# user_id :integer not null
|
||||||
# data :string not null
|
# data :string not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
|
# error_count :integer default(0), not null
|
||||||
|
# first_error_at :datetime
|
||||||
#
|
#
|
||||||
|
|
|
@ -22,7 +22,6 @@ class PushNotificationPusher
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriptions(user).each do |subscription|
|
subscriptions(user).each do |subscription|
|
||||||
subscription = JSON.parse(subscription.data)
|
|
||||||
send_notification(user, subscription, message)
|
send_notification(user, subscription, message)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -66,8 +65,6 @@ class PushNotificationPusher
|
||||||
PushSubscription.find_by(user: user, data: subscription.to_json)&.destroy!
|
PushSubscription.find_by(user: user, data: subscription.to_json)&.destroy!
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
def self.get_badge
|
def self.get_badge
|
||||||
if (url = SiteSetting.site_push_notifications_icon_url).present?
|
if (url = SiteSetting.site_push_notifications_icon_url).present?
|
||||||
url
|
url
|
||||||
|
@ -76,13 +73,30 @@ class PushNotificationPusher
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
MAX_ERRORS ||= 3
|
||||||
|
MIN_ERROR_DURATION ||= 86400 # 1 day
|
||||||
|
|
||||||
|
def self.handle_generic_error(subscription)
|
||||||
|
subscription.error_count += 1
|
||||||
|
subscription.first_error_at ||= Time.zone.now
|
||||||
|
|
||||||
|
delta = Time.zone.now - subscription.first_error_at
|
||||||
|
if subscription.error_count >= MAX_ERRORS && delta > MIN_ERROR_DURATION
|
||||||
|
subscription.destroy!
|
||||||
|
else
|
||||||
|
subscription.save!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def self.send_notification(user, subscription, message)
|
def self.send_notification(user, subscription, message)
|
||||||
endpoint = subscription["endpoint"]
|
parsed_data = subscription.parsed_data
|
||||||
p256dh = subscription.dig("keys", "p256dh")
|
|
||||||
auth = subscription.dig("keys", "auth")
|
endpoint = parsed_data["endpoint"]
|
||||||
|
p256dh = parsed_data.dig("keys", "p256dh")
|
||||||
|
auth = parsed_data.dig("keys", "auth")
|
||||||
|
|
||||||
if (endpoint.blank? || p256dh.blank? || auth.blank?)
|
if (endpoint.blank? || p256dh.blank? || auth.blank?)
|
||||||
unsubscribe(user, subscription)
|
subscription.destroy!
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -99,22 +113,31 @@ class PushNotificationPusher
|
||||||
expiration: TOKEN_VALID_FOR_SECONDS
|
expiration: TOKEN_VALID_FOR_SECONDS
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if subscription.first_error_at || subscription.error_count != 0
|
||||||
|
subscription.update_columns(error_count: 0, first_error_at: nil)
|
||||||
|
end
|
||||||
rescue Webpush::ExpiredSubscription
|
rescue Webpush::ExpiredSubscription
|
||||||
unsubscribe(user, subscription)
|
subscription.destroy!
|
||||||
rescue Webpush::ResponseError => e
|
rescue Webpush::ResponseError => e
|
||||||
if e.response.message == "MismatchSenderId"
|
if e.response.message == "MismatchSenderId"
|
||||||
unsubscribe(user, subscription)
|
subscription.destroy!
|
||||||
else
|
else
|
||||||
|
handle_generic_error(subscription)
|
||||||
Discourse.warn_exception(
|
Discourse.warn_exception(
|
||||||
e,
|
e,
|
||||||
message: "Failed to send push notification",
|
message: "Failed to send push notification",
|
||||||
env: {
|
env: {
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
endpoint: subscription["endpoint"],
|
endpoint: endpoint,
|
||||||
message: message.to_json
|
message: message.to_json
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private_class_method :send_notification
|
||||||
|
private_class_method :handle_generic_error
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddErrorCountToPushSubscriptions < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
add_column :push_subscriptions, :error_count, :integer, null: false, default: 0
|
||||||
|
add_column :push_subscriptions, :first_error_at, :datetime
|
||||||
|
end
|
||||||
|
end
|
|
@ -19,18 +19,20 @@ RSpec.describe PushNotificationPusher do
|
||||||
it "sends notification in user's locale" do
|
it "sends notification in user's locale" do
|
||||||
SiteSetting.allow_user_locale = true
|
SiteSetting.allow_user_locale = true
|
||||||
user = Fabricate(:user, locale: 'pt_BR')
|
user = Fabricate(:user, locale: 'pt_BR')
|
||||||
PushSubscription.create!(user_id: user.id, data: "{\"endpoint\": \"endpoint\"}")
|
data = <<~JSON
|
||||||
|
{
|
||||||
|
"endpoint": "endpoint",
|
||||||
|
"keys": {
|
||||||
|
"p256dh": "p256dh",
|
||||||
|
"auth": "auth"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
PushSubscription.create!(user_id: user.id, data: data)
|
||||||
|
|
||||||
PushNotificationPusher.expects(:send_notification).with(user, { "endpoint" => "endpoint" }, {
|
Webpush.expects(:payload_send).with do |*args|
|
||||||
title: "system mencionou você em \"Topic\" - Discourse",
|
args.to_s.include?("system mencionou")
|
||||||
body: "description",
|
end.once
|
||||||
badge: "/assets/push-notifications/discourse.png",
|
|
||||||
icon: "/assets/push-notifications/mentioned.png",
|
|
||||||
tag: "test.localhost-1",
|
|
||||||
base_url: "http://test.localhost",
|
|
||||||
url: "https://example.com/t/1/2",
|
|
||||||
hide_when_active: true
|
|
||||||
}).once
|
|
||||||
|
|
||||||
PushNotificationPusher.push(user, {
|
PushNotificationPusher.push(user, {
|
||||||
topic_title: 'Topic',
|
topic_title: 'Topic',
|
||||||
|
@ -42,6 +44,60 @@ RSpec.describe PushNotificationPusher do
|
||||||
})
|
})
|
||||||
end
|
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, {
|
||||||
|
topic_title: 'Topic',
|
||||||
|
username: 'system',
|
||||||
|
excerpt: 'description',
|
||||||
|
topic_id: 1,
|
||||||
|
post_url: "https://example.com/t/1/2",
|
||||||
|
notification_type: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
freeze_time 1.minute.from_now
|
||||||
|
end
|
||||||
|
|
||||||
|
sub.reload
|
||||||
|
expect(sub.error_count).to eq(3)
|
||||||
|
expect(sub.first_error_at).to eq_time(start)
|
||||||
|
|
||||||
|
freeze_time(2.days.from_now)
|
||||||
|
|
||||||
|
PushNotificationPusher.push(user, {
|
||||||
|
topic_title: 'Topic',
|
||||||
|
username: 'system',
|
||||||
|
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)
|
||||||
|
end
|
||||||
|
|
||||||
it "deletes invalid subscriptions during send" do
|
it "deletes invalid subscriptions during send" do
|
||||||
user = Fabricate(:walter_white)
|
user = Fabricate(:walter_white)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue