blob: 0bd488efd2304b2e93a142b023ce27d94941cc7c [file] [log] [blame]
// Copyright 2024 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package alerts manages alert values in the database.
package alerts
import (
"context"
"encoding/binary"
"fmt"
"hash/fnv"
"time"
"cloud.google.com/go/spanner"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/server/span"
)
const (
// StatusIDExpression is a partial regular expression that validates status identifiers.
// For now this is quite loose to handle Sheriff-o-matic keys which contain builder name
// strings, so could be any characters at all.
// Example SOM key: chromium$!chrome$!ci$!linux-chromeos-chrome$!browser_tests on Ubuntu-22.04$!8754809345790718177
// TODO: Once we switch away from sheriff-o-matic we should tighten this up.
AlertKeyExpression = `[^/]+`
)
// Alert mirrors the structure of alert values in the database.
type Alert struct {
// The key of the alert.
// For now, this matches the key of the alert in Sheriff-o-Matic.
// This may be revised in the future.
AlertKey string
// The bug number in Buganizer/IssueTracker.
// 0 if the alert is not linked to a bug.
Bug int64
// GerritCL is the CL number associated with this alert.
// 0 means the alert is not associated with any GerritCL.
GerritCL int64
// The build number to consider this alert silenced until.
// 0 if the alert is not silenced.
SilenceUntil int64
// The time the alert was last modified.
// Used to control TTL of alert values.
ModifyTime time.Time
}
// Validate validates an alert value.
// It ignores the ModifyTime fields.
// Reported field names are as they appear in the RPC documentation rather than the Go struct.
func Validate(alert *Alert) error {
if err := validateBug(alert.Bug); err != nil {
return errors.Annotate(err, "bug").Err()
}
if err := validateGerritCL(alert.GerritCL); err != nil {
return errors.Annotate(err, "gerrit_cl").Err()
}
if err := validateSilenceUntil(alert.SilenceUntil); err != nil {
return errors.Annotate(err, "silence_until").Err()
}
return nil
}
func validateBug(bug int64) error {
if bug < 0 {
return errors.Reason("must be zero or positive").Err()
}
return nil
}
func validateGerritCL(gerritCL int64) error {
if gerritCL < 0 {
return errors.Reason("must be zero or positive").Err()
}
return nil
}
func validateSilenceUntil(silenceUntil int64) error {
if silenceUntil < 0 {
return errors.Reason("must be zero or positive").Err()
}
return nil
}
// Put sets the data for the alert in the Spanner Database.
// Note that this implies removing the entry from the database
// if there is no non-default information to keep a small table size.
// ModifyTime in the passed in alert will be ignored
// in favour of the commit time.
func Put(alert *Alert) (*spanner.Mutation, error) {
if err := Validate(alert); err != nil {
return nil, err
}
if alert.Bug == 0 && alert.GerritCL == 0 && alert.SilenceUntil == 0 {
return spanner.Delete("Alerts", spanner.Key{alert.AlertKey}), nil
}
row := map[string]any{
"AlertKey": alert.AlertKey,
"Bug": alert.Bug,
"GerritCL": alert.GerritCL,
"SilenceUntil": alert.SilenceUntil,
"ModifyTime": spanner.CommitTimestamp,
}
return spanner.InsertOrUpdateMap("Alerts", row), nil
}
// ReadBatch retrieves a batch of alerts from the database. If no record exists for an
// alert in the database an alert struct with all fields other than the key set to zero values will be returned.
// Returned alerts are in the same order as the keys requested.
// At most 100 keys can be requested in a batch.
func ReadBatch(ctx context.Context, keys []string) ([]*Alert, error) {
if len(keys) > 100 {
return nil, errors.Reason("requested a batch of %d keys, the maximum size is 100", len(keys)).Err()
}
if len(keys) == 0 {
// Nothing to do.
return []*Alert{}, nil
}
stmt := spanner.NewStatement(`
SELECT
AlertKey,
Bug,
GerritCL,
SilenceUntil,
ModifyTime
FROM Alerts
WHERE
AlertKey IN UNNEST(@keys)`)
stmt.Params["keys"] = keys
iter := span.Query(ctx, stmt)
alertMap := map[string]*Alert{}
if err := iter.Do(func(row *spanner.Row) error {
alert, err := fromRow(row)
if err != nil {
return err
}
alertMap[alert.AlertKey] = alert
return nil
}); err != nil {
return nil, errors.Annotate(err, "read batch of alerts").Err()
}
// Sort return value according to input value, and insert blank entries.
alerts := []*Alert{}
for _, key := range keys {
alert, ok := alertMap[key]
if !ok {
alert = &Alert{
AlertKey: key,
ModifyTime: time.Time{},
}
}
alerts = append(alerts, alert)
}
return alerts, nil
}
func fromRow(row *spanner.Row) (*Alert, error) {
alert := &Alert{}
if err := row.Column(0, &alert.AlertKey); err != nil {
return nil, errors.Annotate(err, "reading AlertKey column").Err()
}
var nullable spanner.NullInt64
if err := row.Column(1, &nullable); err != nil {
return nil, errors.Annotate(err, "reading Bug column").Err()
}
alert.Bug = nullable.Int64
if err := row.Column(2, &nullable); err != nil {
return nil, errors.Annotate(err, "reading GerritCL column").Err()
}
alert.GerritCL = nullable.Int64
if err := row.Column(3, &nullable); err != nil {
return nil, errors.Annotate(err, "reading SilenceUntil column").Err()
}
alert.SilenceUntil = nullable.Int64
if err := row.Column(4, &alert.ModifyTime); err != nil {
return nil, errors.Annotate(err, "reading ModifyTime column").Err()
}
return alert, nil
}
func (a *Alert) Etag() string {
h := fnv.New32a()
// Ignore errors here.
_ = binary.Write(h, binary.LittleEndian, a.Bug)
_ = binary.Write(h, binary.LittleEndian, a.GerritCL)
_ = binary.Write(h, binary.LittleEndian, a.SilenceUntil)
return fmt.Sprintf("W/\"%x\"", h.Sum32())
}