| package ignore |
| |
| import ( |
| "fmt" |
| "net/url" |
| "sync" |
| "time" |
| |
| "go.skia.org/infra/go/database" |
| "go.skia.org/infra/go/sklog" |
| "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 count the untriaged digests per rule. |
| // tileStream - continuously 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 = createdId |
| m.inc() |
| return nil |
| } |
| |
| // Update, see IgnoreStore interface. |
| func (m *SQLIgnoreStore) Update(id int64, 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 { |
| m.lastTilePair, err = addIgnoreCounts(result, m, m.lastTilePair, m.expStore, m.tileStream) |
| if err != nil { |
| sklog.Errorf("Unable to add counts to ignore list result: %s", err) |
| } |
| } |
| |
| return result, nil |
| } |
| |
| // Delete, see IgnoreStore interface. |
| func (m *SQLIgnoreStore) Delete(id int64) (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 |
| } |
| |
| // Revision, 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) |
| } |
| |
| // TODO(stephana): move buildRuleMatcher into a separate file since it's |
| // used by multiple implementations of IgnoreStore |
| |
| 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 |
| } |
| |
| // TODO(stephana): Add unit tests to addIgnoreCounts once we have a framework ready to |
| // easily test against live (vs synthetic) data. |
| |
| // addIgnoreCounts adds counts for the current tile to the given list of rules. |
| func addIgnoreCounts(rules []*IgnoreRule, ignoreStore IgnoreStore, lastTilePair *types.TilePair, expStore expstorage.ExpectationsStore, tileStream <-chan *types.TilePair) (*types.TilePair, error) { |
| if (expStore == nil) || (tileStream == nil) { |
| return nil, fmt.Errorf("Either expStore or tileStream is nil. Cannot count ignores.") |
| } |
| |
| exp, err := expStore.Get() |
| if err != nil { |
| return nil, err |
| } |
| |
| ignoreMatcher, err := ignoreStore.BuildRuleMatcher() |
| if err != nil { |
| return nil, err |
| } |
| |
| // Get the next tile. |
| var tilePair *types.TilePair = nil |
| select { |
| case tilePair = <-tileStream: |
| default: |
| tilePair = lastTilePair |
| } |
| if tilePair == nil { |
| return nil, fmt.Errorf("No tile available to count ignores") |
| } |
| |
| // Count the untriaged digests in HEAD. |
| // matchingDigests[rule.ID]map[digest]bool |
| matchingDigests := make(map[int64]map[string]bool, len(rules)) |
| rulesByDigest := map[string]map[int64]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[int64]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 tilePair, nil |
| } |