blob: 28bc2256143b3d1ffe2dc6fcd24a5db9c2b2942a [file] [log] [blame]
package ignore
import (
"fmt"
"net/url"
"sync"
"time"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/database"
"go.skia.org/infra/go/util"
"go.skia.org/infra/golden/go/expstorage"
"go.skia.org/infra/golden/go/types"
)
type SQLIgnoreStore struct {
vdb *database.VersionedDB
mutex sync.Mutex
revision int64
tileStream <-chan *types.TilePair
lastTilePair *types.TilePair
expStore expstorage.ExpectationsStore
}
// NewSQLIgnoreStore creates a new SQL based IgnoreStore.
// vdb - database to connect to.
// expStore - expectations store needed to cound the untriaged digests per rule.
// tileStream - continously provides an updated copy of the current tile.
func NewSQLIgnoreStore(vdb *database.VersionedDB, expStore expstorage.ExpectationsStore, tileStream <-chan *types.TilePair) IgnoreStore {
ret := &SQLIgnoreStore{
vdb: vdb,
tileStream: tileStream,
expStore: expStore,
}
return ret
}
func (m *SQLIgnoreStore) inc() {
m.mutex.Lock()
defer m.mutex.Unlock()
m.revision += 1
}
// Create, see IgnoreStore interface.
func (m *SQLIgnoreStore) Create(rule *IgnoreRule) error {
stmt := `INSERT INTO ignorerule (userid, updated_by, expires, query, note)
VALUES(?,?,?,?,?)`
ret, err := m.vdb.DB.Exec(stmt, rule.Name, rule.Name, rule.Expires.Unix(), rule.Query, rule.Note)
if err != nil {
return err
}
createdId, err := ret.LastInsertId()
if err != nil {
return err
}
rule.ID = int(createdId)
m.inc()
return nil
}
// Update, see IgnoreStore interface.
func (m *SQLIgnoreStore) Update(id int, rule *IgnoreRule) error {
stmt := `UPDATE ignorerule SET updated_by=?, expires=?, query=?, note=? WHERE id=?`
res, err := m.vdb.DB.Exec(stmt, rule.UpdatedBy, rule.Expires.Unix(), rule.Query, rule.Note, rule.ID)
if err != nil {
return err
}
n, err := res.RowsAffected()
if err == nil && n == 0 {
return fmt.Errorf("Did not find an IgnoreRule with id: %d", id)
}
m.inc()
return nil
}
// List, see IgnoreStore interface.
func (m *SQLIgnoreStore) List(addCounts bool) ([]*IgnoreRule, error) {
stmt := `SELECT id, userid, updated_by, expires, query, note
FROM ignorerule
ORDER BY expires ASC`
rows, err := m.vdb.DB.Query(stmt)
if err != nil {
return nil, err
}
defer util.Close(rows)
result := []*IgnoreRule{}
for rows.Next() {
target := &IgnoreRule{}
var expiresTS int64
err := rows.Scan(&target.ID, &target.Name, &target.UpdatedBy, &expiresTS, &target.Query, &target.Note)
if err != nil {
return nil, err
}
target.Expires = time.Unix(expiresTS, 0)
result = append(result, target)
}
if addCounts {
if err := m.addIgnoreCounts(result); err != nil {
sklog.Errorf("Unable to add counts to ignore list result: %s", err)
}
}
return result, nil
}
// TODO(stephana): Add unit tests to addIgnoreCounts once we have a framework ready to
// easily test against live (vs synthetic) data.
// addIgnoreCounts counts the number of traces in the current tile that match the given
// ignore rules. It sets the corresponding field in each instance of IgnoreRule.
func (m *SQLIgnoreStore) addIgnoreCounts(rules []*IgnoreRule) error {
if (m.expStore == nil) || (m.tileStream == nil) {
return fmt.Errorf("Either expStore or tileStream is nil. Cannot count ignores.")
}
exp, err := m.expStore.Get()
if err != nil {
return err
}
ignoreMatcher, err := m.BuildRuleMatcher()
if err != nil {
return err
}
// Get the next tile.
var tilePair *types.TilePair = nil
select {
case tilePair = <-m.tileStream:
default:
tilePair = m.lastTilePair
}
if tilePair == nil {
return fmt.Errorf("No tile available to count ignores")
}
m.lastTilePair = tilePair
// Count the untriaged digests in HEAD.
// matchingDigests[rule.ID]map[digest]bool
matchingDigests := make(map[int]map[string]bool, len(rules))
rulesByDigest := map[string]map[int]bool{}
for _, trace := range tilePair.TileWithIgnores.Traces {
gTrace := trace.(*types.GoldenTrace)
if matchRules, ok := ignoreMatcher(gTrace.Params_); ok {
testName := gTrace.Params_[types.PRIMARY_KEY_FIELD]
if digest := gTrace.LastDigest(); digest != types.MISSING_DIGEST && (exp.Classification(testName, digest) == types.UNTRIAGED) {
k := testName + ":" + digest
for _, r := range matchRules {
// Add the digest to all matching rules.
if t, ok := matchingDigests[r.ID]; ok {
t[k] = true
} else {
matchingDigests[r.ID] = map[string]bool{k: true}
}
// Add the rule to the test-digest.
if t, ok := rulesByDigest[k]; ok {
t[r.ID] = true
} else {
rulesByDigest[k] = map[int]bool{r.ID: true}
}
}
}
}
}
for _, r := range rules {
r.Count = len(matchingDigests[r.ID])
r.ExclusiveCount = 0
for testDigestKey := range matchingDigests[r.ID] {
// If exactly this one rule matches then account for it.
if len(rulesByDigest[testDigestKey]) == 1 {
r.ExclusiveCount++
}
}
}
return nil
}
// Delete, see IgnoreStore interface.
func (m *SQLIgnoreStore) Delete(id int, userId string) (int, error) {
stmt := "DELETE FROM ignorerule WHERE id=?"
ret, err := m.vdb.DB.Exec(stmt, id)
if err != nil {
return 0, err
}
rowsAffected, err := ret.RowsAffected()
if err != nil {
return 0, err
}
if rowsAffected > 0 {
m.inc()
}
return int(rowsAffected), nil
}
// Revisison, see IngoreStore interface.
func (m *SQLIgnoreStore) Revision() int64 {
m.mutex.Lock()
defer m.mutex.Unlock()
return m.revision
}
// BuildRuleMatcher, see IgnoreStore interface.
func (m *SQLIgnoreStore) BuildRuleMatcher() (RuleMatcher, error) {
return buildRuleMatcher(m)
}
func buildRuleMatcher(store IgnoreStore) (RuleMatcher, error) {
rulesList, err := store.List(false)
if err != nil {
return noopRuleMatcher, err
}
ignoreRules := make([]QueryRule, len(rulesList))
for idx, rawRule := range rulesList {
parsedQuery, err := url.ParseQuery(rawRule.Query)
if err != nil {
return noopRuleMatcher, err
}
ignoreRules[idx] = NewQueryRule(parsedQuery)
}
return func(params map[string]string) ([]*IgnoreRule, bool) {
result := []*IgnoreRule{}
for ruleIdx, rule := range ignoreRules {
if rule.IsMatch(params) {
result = append(result, rulesList[ruleIdx])
}
}
return result, len(result) > 0
}, nil
}