FIX: Make problem check tracker unique constraint work on PG13 (#29272)

In #29169 we added a NULLS NOT DISTINCT option to the unique index on problem_check_trackers. This is to enforce uniqueness even when the target is NULL. (Postgres considers all NULLs to be distinct by default.)

However, this only works in PG15. In PG13 it does nothing.

This commit adds a default dummy string value __NULL__ to target. Since it's a string, PG13 will be able to correctly identify duplicate records.
This commit is contained in:
Ted Johansson 2024-10-21 10:45:09 +08:00 committed by GitHub
parent fc2093fc7e
commit 93625ef07c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 32 additions and 11 deletions

View File

@ -90,6 +90,10 @@ class ProblemCheck
ProblemCheck::WatchedWords, ProblemCheck::WatchedWords,
].freeze ].freeze
# To enforce the unique constraint in Postgres <15 we need a dummy
# value, since the index considers NULLs to be distinct.
NO_TARGET = "__NULL__"
def self.[](key) def self.[](key)
key = key.to_sym key = key.to_sym
@ -176,12 +180,12 @@ class ProblemCheck
private private
def tracker(target = nil) def tracker(target = NO_TARGET)
ProblemCheckTracker[identifier, target] ProblemCheckTracker[identifier, target]
end end
def targets def targets
[nil] [NO_TARGET]
end end
def problem(override_key: nil, override_data: {}) def problem(override_key: nil, override_data: {})

View File

@ -7,7 +7,7 @@ class ProblemCheckTracker < ActiveRecord::Base
scope :failing, -> { where("last_problem_at = last_run_at") } scope :failing, -> { where("last_problem_at = last_run_at") }
scope :passing, -> { where("last_success_at = last_run_at") } scope :passing, -> { where("last_success_at = last_run_at") }
def self.[](identifier, target = nil) def self.[](identifier, target = ProblemCheck::NO_TARGET)
find_or_create_by(identifier:, target:) find_or_create_by(identifier:, target:)
end end
@ -71,11 +71,7 @@ class ProblemCheckTracker < ActiveRecord::Base
end end
def admin_notice def admin_notice
if target.present? AdminNotice.problem.where("details->>'target' = ?", target || ProblemCheck::NO_TARGET)
AdminNotice.problem.where("details->>'target' = ?", target)
else
AdminNotice.problem.where("(details->>'target') IS NULL")
end
end end
end end
@ -91,7 +87,7 @@ end
# last_success_at :datetime # last_success_at :datetime
# last_problem_at :datetime # last_problem_at :datetime
# details :json # details :json
# target :string # target :string default("__NULL__")
# #
# Indexes # Indexes
# #

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class AddDefaultValueToProblemCheckTrackersTarget < ActiveRecord::Migration[7.1]
def up
change_column_default :problem_check_trackers, :target, "__NULL__"
execute(<<~SQL)
UPDATE problem_check_trackers
SET target='__NULL__'
WHERE target IS NULL
SQL
end
def down
change_column_default :problem_check_trackers, :target, nil
execute(<<~SQL)
UPDATE problem_check_trackers
SET target = NULL
WHERE target = '__NULL__'
SQL
end
end

View File

@ -187,7 +187,6 @@ RSpec.describe ProblemCheckTracker do
before { ProblemCheck::TwitterLogin.stubs(:max_blips).returns(1) } before { ProblemCheck::TwitterLogin.stubs(:max_blips).returns(1) }
it "does not sound the alarm" do it "does not sound the alarm" do
puts ProblemCheck::TwitterLogin.max_blips
expect { problem_tracker.problem!(next_run_at: 24.hours.from_now) }.not_to change { expect { problem_tracker.problem!(next_run_at: 24.hours.from_now) }.not_to change {
AdminNotice.problem.count AdminNotice.problem.count
} }
@ -221,7 +220,7 @@ RSpec.describe ProblemCheckTracker do
end end
context "when there's an alarm sounding" do context "when there's an alarm sounding" do
before { Fabricate(:admin_notice, subject: "problem", identifier: "twitter_login") } before { problem_tracker.problem! }
it "silences the alarm" do it "silences the alarm" do
expect { problem_tracker.no_problem!(next_run_at: 24.hours.from_now) }.to change { expect { problem_tracker.no_problem!(next_run_at: 24.hours.from_now) }.to change {