Documentation
¶
Overview ¶
Package reviewalert pushes an operator alert when a review queue crosses its staleness threshold (#803, extended to managed scripts by #1287).
#764 made knowledge review debt visible to anyone who looks: bulk_review, platform_info, and the portal all report the pending count and its age. This package supplies the signal for everyone who does not look. It reads a lightweight rollup on a timer and, when the queue crosses the operator's threshold, enqueues a digest through the notification substrate (pkg/notification) rather than sending mail of its own.
Two queues are watched by one mechanism rather than by two copies of it: the knowledge insight queue and the managed-script review queue. What differs between them is named by a Target (which settings section holds the configuration, which state row records the last alert, what the email says and where it points) and read by a Source (what is pending, and how old the oldest is). Everything else — the threshold model, the recipient list, the cooldown, and the single-winner claim — is one implementation.
Three pieces, none of which the substrate already owned:
Settings the operator's threshold, cooldown, and recipient list,
stored in the platform_settings section the admin API writes
StateStore the last-alert marker, claimed with one conditional UPDATE so
a persistently stale queue alerts once per cooldown and only
one replica's check wins a given window
Checker the timer that joins them
It must not import pkg/platform: the HTTP composition root supplies the queue sources, the enqueuer, and the portal base URL it holds already.
Index ¶
- Constants
- Variables
- type Checker
- type Config
- type InsightSource
- type PostgresStore
- func (s *PostgresStore) ClaimAlert(ctx context.Context, cooldown time.Duration, now time.Time) (bool, error)
- func (s *PostgresStore) Clear(ctx context.Context) error
- func (s *PostgresStore) Get(ctx context.Context) (*Settings, error)
- func (s *PostgresStore) Set(ctx context.Context, in Settings, author string) error
- type Settings
- type SettingsInput
- type SettingsStore
- type SettingsView
- type Source
- type StateStore
- type Target
Constants ¶
const ( // NoRecipientsWarning is reported when the check is on with an empty // recipient list. NoRecipientsWarning = "no recipients are configured, so no alert will be delivered; add at least one address" // NoThresholdWarning is reported when the check is on with both // thresholds cleared. NoThresholdWarning = "both thresholds are 0, so nothing can cross; set a pending count, an age in days, or both" )
Warnings reported by SettingsView for a configuration that saves cleanly but delivers nothing. They are warnings rather than validation errors so an operator can enable the check and fill in the rest, in either order.
const ( // DefaultCooldownHours is the default minimum gap between two alerts // about a queue that stays over threshold. DefaultCooldownHours = 24 // DefaultKnowledgeOldestDays is the knowledge queue's default age // threshold, in days. It matches the platform's existing definition of // stale review debt (#764) rather than inventing a second number, so the // alert fires on exactly the rows the portal already badges. DefaultKnowledgeOldestDays = knowledgekit.PendingStalenessThresholdDays )
Default threshold values.
const DefaultInterval = time.Hour
DefaultInterval is how often a queue is evaluated. The thresholds are measured in days, so an hourly check is already far finer than the signal they watch; the cooldown, not the interval, decides how often mail goes out.
const ( // MaxRecipients caps the alert's distribution list. This is an operator // alert about a review queue, not an announcement channel. MaxRecipients = 20 )
Input bounds. They exist to keep a typo from turning the alert into a mail loop or an unreadable configuration, not to express policy.
Variables ¶
var ErrNotFound = errors.New("reviewalert: settings not found")
ErrNotFound is returned by the settings store when the alert has never been configured. Callers apply the target's defaults instead.
Functions ¶
This section is empty.
Types ¶
type Checker ¶
type Checker struct {
// contains filtered or unexported fields
}
Checker evaluates one pending review queue on a timer and alerts the configured recipients when it crosses the threshold.
func New ¶
New builds a Checker, or nil when a dependency is absent (no database, or notifications disabled). A nil Checker's methods are no-ops, so the caller brackets Start/Stop unconditionally.
func (*Checker) Check ¶
Check evaluates the queue once and enqueues the alert when it has crossed the threshold and the cooldown allows. It is the whole behavior of the package; Start only supplies the clock.
func (*Checker) Start ¶
Start runs the check loop until ctx is canceled or Stop is called. The first check runs one interval in rather than at startup, keeping it clear of boot: the signal it watches is measured in days, so nothing is lost by waiting. What bounds repeat mail is the cooldown claim, which is in the database and so survives a restart. Nil-safe.
type Config ¶
type Config struct {
// Target names the queue being watched.
Target Target
// Settings holds the operator's threshold and recipients.
Settings SettingsStore
// State holds the re-alert marker.
State StateStore
// Source reports what is pending.
Source Source
// Enqueuer is the notification substrate's trigger-side entry point.
Enqueuer *notification.Enqueuer
// BaseURL is the portal's public base URL, for the alert's deep link.
BaseURL string
// Interval overrides DefaultInterval. Testing hook.
Interval time.Duration
// Now overrides time.Now. Testing hook.
Now func() time.Time
}
Config carries the checker's dependencies. All are required; New returns nil when any is missing, which is how the composition root expresses "this deployment has no database" without a second flag.
type InsightSource ¶ added in v1.121.0
type InsightSource struct {
Insights knowledgekit.InsightStore
}
InsightSource reports the knowledge insight review queue through the same fast-path rollup the portal and platform_info read (#764), so the alert fires on exactly the rows those surfaces already badge.
func (InsightSource) Pending ¶ added in v1.121.0
func (s InsightSource) Pending(ctx context.Context, now time.Time) (notification.ReviewQueue, error)
Pending returns the insight queue's rollup.
type PostgresStore ¶
type PostgresStore struct {
// contains filtered or unexported fields
}
PostgresStore is one queue's PostgreSQL persistence: the operator's configuration (SettingsStore, here) and the re-alert marker (StateStore, in state.go). One store because they are one queue's state, always built together over the same pool; two interfaces because the admin API and the checker each need only their half.
It is bound to a Target rather than to a hardcoded section and row, which is what lets a second review queue reuse this implementation instead of copying it.
func NewPostgresStore ¶
func NewPostgresStore(db *sql.DB, target Target) *PostgresStore
NewPostgresStore creates the PostgreSQL-backed alert store for one queue.
func (*PostgresStore) ClaimAlert ¶
func (s *PostgresStore) ClaimAlert(ctx context.Context, cooldown time.Duration, now time.Time) (bool, error)
ClaimAlert stamps a new alert when the cooldown allows, reporting whether this caller won the claim.
func (*PostgresStore) Clear ¶
func (s *PostgresStore) Clear(ctx context.Context) error
Clear drops the over-threshold marker, keeping last_alert_at as the record of when the last alert went out.
type Settings ¶
type Settings struct {
// Enabled turns the scheduled check on. A check with no recipients or no
// threshold delivers nothing regardless; SettingsView reports both as
// warnings rather than refusing the save.
Enabled bool `json:"enabled"`
// PendingThreshold alerts when the pending count reaches it. Zero
// disables the count condition.
PendingThreshold int `json:"pending_threshold"`
// OldestPendingDays alerts when the oldest pending item reaches this age
// in days. Zero disables the age condition.
OldestPendingDays int `json:"oldest_pending_days"`
// CooldownHours is the minimum gap between two alerts while the queue
// stays over threshold. A queue that drops back under and crosses again
// alerts immediately: the cooldown suppresses repetition, not news.
CooldownHours int `json:"cooldown_hours"`
// Recipients are the addresses the alert is delivered to, in
// notification.NormalizeAddress form.
Recipients []string `json:"recipients"`
// UpdatedBy and UpdatedAt describe the last admin write. They live in the
// platform_settings audit columns, which are authoritative, so they are
// excluded from the section value rather than written into it twice.
UpdatedBy string `json:"-"`
UpdatedAt time.Time `json:"-"`
}
Settings is the operator's alert configuration for one queue. It is that queue's section of the platform_settings table.
func SettingsOf ¶
SettingsOf returns the stored configuration, or the target's defaults when none has been written. Every caller wants this rather than the raw ErrNotFound: an operator who has never opened the settings page still gets the platform's default threshold, and the recipient list is what actually gates delivery.
func (Settings) Cooldown ¶
Cooldown returns the configured re-alert gap, falling back to the default so a zero (or negative) stored value cannot turn every check into a send.
func (Settings) Crossed ¶
Crossed reports whether a queue of pending items whose oldest is oldestAgeDays old has crossed the configured threshold. Either condition alone is enough; a zero threshold disables its condition, and an empty queue never crosses.
func (Settings) Deliverable ¶
Deliverable reports whether a crossing could actually reach anyone: the check is on, at least one threshold can fire, and someone is listed.
func (Settings) View ¶
func (s Settings) View() SettingsView
View maps stored settings to the read shape.
type SettingsInput ¶
type SettingsInput struct {
Enabled bool `json:"enabled" example:"true"`
PendingThreshold int `json:"pending_threshold" example:"25"`
OldestPendingDays int `json:"oldest_pending_days" example:"30"`
CooldownHours int `json:"cooldown_hours" example:"24"`
Recipients []string `json:"recipients" example:"data-admin@example.com"`
}
SettingsInput is the write shape for the admin alert configuration.
func (*SettingsInput) Settings ¶
func (in *SettingsInput) Settings() Settings
Settings maps the validated input to stored settings.
func (*SettingsInput) Validate ¶
func (in *SettingsInput) Validate() string
Validate normalizes the input in place and returns a non-empty message when it is invalid. Recipients are reduced to their storage form and de-duplicated, so the checker never mails one person twice because their address was listed in two shapes.
type SettingsStore ¶
type SettingsStore interface {
// Get returns the stored configuration, or ErrNotFound when the alert has
// never been configured.
Get(ctx context.Context) (*Settings, error)
// Set upserts the configuration.
Set(ctx context.Context, s Settings, author string) error
}
SettingsStore is the configuration half of one queue's alert persistence: the operator's threshold, cooldown, and recipients, held as that queue's section of the platform_settings table. (The SMTP section has its own contract over the same table rather than one widened store serving both.)
It is a contract of its own because the admin API needs nothing else: the settings surface can write the configuration without being handed the checker's claim.
type SettingsView ¶
type SettingsView struct {
Enabled bool `json:"enabled" example:"true"`
PendingThreshold int `json:"pending_threshold" example:"25"`
OldestPendingDays int `json:"oldest_pending_days" example:"30"`
CooldownHours int `json:"cooldown_hours" example:"24"`
Recipients []string `json:"recipients"`
UpdatedBy string `json:"updated_by,omitempty" example:"admin@example.com"`
UpdatedAt time.Time `json:"updated_at"`
// Warnings describes a configuration that saves cleanly but delivers
// nothing. They never block a save; they exist so the operator sees the
// gap at the surface where the setting was chosen.
Warnings []string `json:"warnings,omitempty"`
}
SettingsView is the read shape for the admin alert configuration.
type Source ¶ added in v1.121.0
type Source interface {
// Pending returns the queue's rollup as of now: how much is waiting and how
// old the oldest of it is.
Pending(ctx context.Context, now time.Time) (notification.ReviewQueue, error)
}
Source reports what one review queue is holding. It is the only part of a check that differs between queues beyond the strings in the Target.
type StateStore ¶
type StateStore interface {
// ClaimAlert stamps a new alert at now and reports whether this caller
// won it. It loses when an alert for the same continuously-over-threshold
// stretch was stamped less than cooldown ago, which is what keeps a
// stale queue from mailing on every check.
ClaimAlert(ctx context.Context, cooldown time.Duration, now time.Time) (bool, error)
// Clear drops the over-threshold marker. The next crossing then alerts
// immediately instead of serving out a cooldown that belongs to a queue
// which has since been worked.
Clear(ctx context.Context) error
}
StateStore is the re-alert marker half of one queue's persistence: whether that queue is currently over threshold and when it was last alerted about.
The claim is the whole de-duplication mechanism. It is one conditional write, so it answers both questions a scheduled alert has to answer -- "has this already been sent recently?" and "is another replica sending it right now?" -- without a second coordination primitive.
type Target ¶ added in v1.121.0
type Target struct {
// Queue is the stable key of this queue: the primary key of its state row
// and the identifier in logs. It is never derived from a display string,
// because renaming a queue in the UI must not orphan its cooldown.
Queue string
// SettingsSection is the platform_settings section holding the operator's
// configuration for this queue.
SettingsSection string
// Category and Kind are the notification category the alert is enqueued
// under and the payload kind its renderer dispatches on.
Category string
Kind string
// Title labels the queue in the email.
Title string
// Route is the portal path the alert links to, relative to the portal base
// URL.
Route string
// DefaultOldestDays is the age threshold applied before an operator has
// configured one.
DefaultOldestDays int
}
Target names one review queue: where its configuration and its last-alert marker live, and what the email it raises says.
It is a value rather than an interface because none of it is behavior. The behavior that does differ between queues — what counts as pending — is the Source.
func KnowledgeTarget ¶ added in v1.121.0
func KnowledgeTarget() Target
KnowledgeTarget describes the knowledge insight review queue (#803).
Its settings section keeps the name it was written under: the section is a stored key, and renaming it would silently strand every deployment's configured recipients behind a section nothing reads.
func (Target) DefaultSettings ¶ added in v1.121.0
DefaultSettings returns the configuration applied before an operator has written one for this queue: on, alerting on the queue's own age threshold, once a day at most, to nobody yet.