2
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-03-03 23:54:20 +08:00
discourse/app/models/problem_check_tracker.rb
Régis Hanol ae58a5a1be
FIX: Dismissing admin notices fails with 422 when tracker has NULL target (#36878)
When dismissing an admin notice, the service would look up the
associated `ProblemCheckTracker` by identifier only. This could find a
tracker with a `NULL` target, which then fails validation on update
since target is now required (as of 8ca5fb706a).

The root issue is that `problem_check_trackers.target` was added with
`null: true` and while we later added a default value of `"__NULL__"`
and a Ruby validation, we never enforced `NOT NULL` at the database
level. This allowed records with `NULL` targets to persist or be
recreated through code paths that explicitly passed nil.

This commit:

- Updates the dismiss service to look up trackers by both identifier AND
target (extracted from the admin notice's details)
- Removes any remaining records with `NULL` targets
- Adds a `NOT NULL` constraint to prevent this from recurring

Ref - https://meta.discourse.org/t/392248
2025-12-29 16:32:12 +01:00

104 lines
2.4 KiB
Ruby

# frozen_string_literal: true
class ProblemCheckTracker < ActiveRecord::Base
validates :identifier, presence: true, uniqueness: { scope: :target }
validates :target, presence: true
validates :blips, presence: true, numericality: { greater_than_or_equal_to: 0 }
scope :failing, -> { where("last_problem_at = last_run_at") }
scope :passing, -> { where("last_success_at = last_run_at") }
before_destroy :silence_the_alarm
def self.[](identifier, target = ProblemCheck::NO_TARGET)
find_or_create_by(identifier:, target:)
end
def ready_to_run?
next_run_at.blank? || next_run_at.past?
end
def failing?
last_problem_at == last_run_at
end
def passing?
last_success_at == last_run_at
end
def problem!(next_run_at: nil, details: {})
now = Time.current
update!(blips: blips + 1, details:, last_run_at: now, last_problem_at: now, next_run_at:)
update_notice_details(details)
sound_the_alarm if sound_the_alarm?
end
def no_problem!(next_run_at: nil)
reset(next_run_at:)
silence_the_alarm
end
def reset(next_run_at: nil)
now = Time.current
update!(blips: 0, last_run_at: now, last_success_at: now, next_run_at:)
end
def check
check = ProblemCheck[identifier]
return check if check.present?
silence_the_alarm
destroy
nil
end
private
def update_notice_details(details)
admin_notice.where(identifier:).update_all(details: details.merge(target:))
end
def sound_the_alarm?
failing? && blips > check.max_blips
end
def sound_the_alarm
admin_notice.create_with(
priority: check.priority,
details: details.merge(target:),
).find_or_create_by(identifier:)
end
def silence_the_alarm
admin_notice.where(identifier:).delete_all
end
def admin_notice
AdminNotice.problem.where("details->>'target' = ?", target)
end
end
# == Schema Information
#
# Table name: problem_check_trackers
#
# id :bigint not null, primary key
# blips :integer default(0), not null
# details :json
# identifier :string not null
# last_problem_at :datetime
# last_run_at :datetime
# last_success_at :datetime
# next_run_at :datetime
# target :string default("__NULL__"), not null
#
# Indexes
#
# index_problem_check_trackers_on_identifier_and_target (identifier,target) UNIQUE
#