blob: 9146e34a85ab344c70aa198905050ef474ea15da [file] [log] [blame]
package regression
import (
"database/sql"
"encoding/json"
"fmt"
"go.skia.org/infra/go/util"
"go.skia.org/infra/perf/go/cid"
"go.skia.org/infra/perf/go/clustering2"
"go.skia.org/infra/perf/go/dataframe"
"go.skia.org/infra/perf/go/db"
)
// Subset is the subset of regressions we are querying for.
type Subset string
const (
ALL_SUBSET Subset = "all" // Include all regressions in a range.
REGRESSIONS_SUBSET Subset = "regressions" // Only include regressions in a range that are alerting.
UNTRIAGED_SUBSET Subset = "untriaged" // All untriaged alerting regressions regardless of range.
)
// Store persists Regressions to/from an SQL database.
type Store struct {
}
// NewStore returns a new Store.
func NewStore() *Store {
return &Store{}
}
// load Regressions stored for the given commit.
func (s *Store) load(tx *sql.Tx, cid *cid.CommitDetail) (*Regressions, error) {
row := tx.QueryRow("SELECT cid, body FROM regression WHERE cid=?", cid.ID())
if row == nil {
return nil, fmt.Errorf("Failed to query database for %q.", cid.ID())
}
var id string
var body string
if err := row.Scan(&id, &body); err != nil {
return nil, fmt.Errorf("Failed to read from database: %s", err)
}
ret := New()
if err := json.Unmarshal([]byte(body), ret); err != nil {
return nil, fmt.Errorf("Failed to decode JSON body: %s", err)
}
return ret, nil
}
// store Regressions for the given commit.
func (s *Store) store(tx *sql.Tx, cid *cid.CommitDetail, r *Regressions) error {
body, err := r.JSON()
// MEDIUMTEXT is only 16MB, and will silently be truncated.
if len(body) > 16777215 {
return fmt.Errorf("Regressions is too large, >16 MB.")
}
if err != nil {
return fmt.Errorf("Failed to encode Regressions to JSON: %s", err)
}
_, err = tx.Exec("INSERT INTO regression (cid, timestamp, triaged, body) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE cid=?, timestamp=?, triaged=?, body=?",
cid.ID(), cid.Timestamp, r.Triaged(), body,
cid.ID(), cid.Timestamp, r.Triaged(), body)
if err != nil {
return fmt.Errorf("Failed to write to database: %s", err)
}
return nil
}
// Untriaged returns the number of untriaged regressions.
func (s *Store) Untriaged() (int, error) {
row := db.DB.QueryRow("SELECT count(*) FROM regression WHERE triaged=false")
if row == nil {
return -1, fmt.Errorf("Failed to query database for count of untriaged regressions.")
}
var count int
if err := row.Scan(&count); err != nil {
return -1, fmt.Errorf("Failed to read from database: %s", err)
}
return count, nil
}
// Range returns a map from cid.ID()'s to *Regressions that exist in the given time range.
func (s *Store) Range(begin, end int64, subset Subset) (map[string]*Regressions, error) {
ret := map[string]*Regressions{}
rows, err := db.DB.Query("SELECT cid, timestamp, body FROM regression WHERE timestamp >= ? AND timestamp < ? ORDER BY timestamp", begin, end)
if subset == UNTRIAGED_SUBSET {
rows, err = db.DB.Query("SELECT cid, timestamp, body FROM regression WHERE triaged=false ORDER BY timestamp")
}
if err != nil {
return nil, fmt.Errorf("Failed to query from database: %s", err)
}
defer util.Close(rows)
for rows.Next() {
var id string
var timestamp int64
var body string
if err := rows.Scan(&id, &timestamp, &body); err != nil {
return nil, fmt.Errorf("Failed to read from database: %s", err)
}
reg := New()
if err := json.Unmarshal([]byte(body), reg); err != nil {
return nil, fmt.Errorf("Failed to decode JSON body: %s", err)
}
ret[id] = reg
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("Error while iterating rows: %s", err)
}
return ret, nil
}
// intx runs f within a database transaction.
//
func intx(f func(tx *sql.Tx) error) (err error) {
tx, err := db.DB.Begin()
if err != nil {
return fmt.Errorf("Failed to start transaction: %s", err)
}
defer func() {
if err != nil {
_ = tx.Rollback()
return
}
err = tx.Commit()
}()
err = f(tx)
return err
}
// SetHigh sets the cluster for a high regression at the given commit and query.
func (s *Store) SetHigh(cid *cid.CommitDetail, query string, df *dataframe.FrameResponse, high *clustering2.ClusterSummary) error {
return intx(func(tx *sql.Tx) error {
r, err := s.load(tx, cid)
if err != nil {
r = New()
}
r.SetHigh(query, df, high)
return s.store(tx, cid, r)
})
}
// SetLow sets the cluster for a low regression at the given commit and query.
func (s *Store) SetLow(cid *cid.CommitDetail, query string, df *dataframe.FrameResponse, low *clustering2.ClusterSummary) error {
return intx(func(tx *sql.Tx) error {
r, err := s.load(tx, cid)
if err != nil {
r = New()
}
r.SetLow(query, df, low)
return s.store(tx, cid, r)
})
}
// TriageLow sets the triage status for the low cluster at the given commit and query.
func (s *Store) TriageLow(cid *cid.CommitDetail, query string, tr TriageStatus) error {
return intx(func(tx *sql.Tx) error {
r, err := s.load(tx, cid)
if err != nil {
return fmt.Errorf("Failed to load Regressions: %s", err)
}
if err = r.TriageLow(query, tr); err != nil {
return fmt.Errorf("Failed to update Regressions: %s", err)
}
return s.store(tx, cid, r)
})
}
// TriageHigh sets the triage status for the high cluster at the given commit and query.
func (s *Store) TriageHigh(cid *cid.CommitDetail, query string, tr TriageStatus) error {
return intx(func(tx *sql.Tx) error {
r, err := s.load(tx, cid)
if err != nil {
return fmt.Errorf("Failed to load Regressions: %s", err)
}
if err := r.TriageHigh(query, tr); err != nil {
return fmt.Errorf("Failed to update Regressions: %s", err)
}
return s.store(tx, cid, r)
})
}