blob: 2891033f5c7b2d28e4e5841ef45778d83da59152 [file] [log] [blame]
package issuestore
import (
"path"
"github.com/boltdb/bolt"
"go.skia.org/infra/go/boltutil"
"go.skia.org/infra/go/fileutil"
"go.skia.org/infra/go/util"
)
// IssueStore captures the functions necessary to persist the connection between
// Monorail issues and digests, traces, tests and ignores.
type IssueStore interface {
// ByDigest returns the ids of all issue associated with the given digest.
ByDigest(digest string) ([]string, error) // list of issues
// ByDigest returns the ids of all issue associated with the given digest.
ByIgnore(ignoreID string) ([]string, error) // list of issues
// ByDigest returns the ids of all issue associated with the given digest.
ByTrace(traceID string) ([]string, error) // list of issues
// ByDigest returns the ids of all issue associated with the given digest.
ByTest(testName string) ([]string, error) // list of issues
// Add allows to create an issue annotation or add to an existing annotation.
// If the issue identified by delta.IssueID exists, delta will be merged into
// the existing annotation.
Add(delta *Annotation) error
// Subtract removes from an existing issue annotation. The values in
// delta are subtracted from an existing annotation.
Subtract(delta *Annotation) error
// Get returns the annotations for the given list of issue ids.
Get(issueIDs []string) ([]*Annotation, error)
// List returns a list of all issues that are currently annotated with
// support of paging. The first 'offset' annotations will be skipped and
// the returned array has at most 'size'. If 'size' <= 0 there is no limit
// on the number of annotations returned.
List(offset int, size int) ([]*Annotation, int, error)
// Delete the given issue annotations.
Delete(issueIDs []string) error
}
var annotationIndices = []string{DIGEST_INDEX, TRACE_INDEX, IGNORE_INDEX, TEST_INDEX}
// Annotation captures annotations for the issue identified by IssueID.
type Annotation struct {
IssueID string // id of the issue in Monorail
Digests []string // Image digests connected to this issue
Traces []string // Trace ids connected to this issues.
Ignores []string // Ignore ids connected to this issue.
TestNames []string // Test names connected to this issue.
}
// Key see boltutil.Record interface.
func (a *Annotation) Key() string {
return a.IssueID
}
// IndexValues see boltutil.Record interface.
func (a *Annotation) IndexValues() map[string][]string {
ret := make(map[string][]string, len(annotationIndices))
for _, idx := range annotationIndices {
switch idx {
case DIGEST_INDEX:
ret[idx] = append(ret[idx], a.Digests...)
case TRACE_INDEX:
ret[idx] = append(ret[idx], a.Traces...)
case IGNORE_INDEX:
ret[idx] = append(ret[idx], a.Ignores...)
case TEST_INDEX:
ret[idx] = append(ret[idx], a.TestNames...)
}
}
return ret
}
// Adds the digests, traces, ignores and tests in delta to the current annotation.
// and deduplicates in the process.
func (r *Annotation) Add(deltaRec *Annotation) bool {
updated := mergeStrings(&r.Digests, deltaRec.Digests)
updated = mergeStrings(&r.Traces, deltaRec.Traces) || updated
updated = mergeStrings(&r.Ignores, deltaRec.Ignores) || updated
return mergeStrings(&r.TestNames, deltaRec.TestNames) || updated
}
// Subtract removes the digests, traces, ignores and tests in delta from the current annotation.
func (r *Annotation) Subtract(deltaRec *Annotation) bool {
updated := removeStrings(&r.Digests, deltaRec.Digests)
updated = removeStrings(&r.Traces, deltaRec.Traces) || updated
updated = removeStrings(&r.Ignores, deltaRec.Ignores) || updated
return removeStrings(&r.TestNames, deltaRec.TestNames) || updated
}
// IsEmpty returns true if all all annotations are empty.
func (r *Annotation) IsEmpty() bool {
return (len(r.Digests) + len(r.Traces) + len(r.Ignores) + len(r.TestNames)) == 0
}
const (
// Bucket names in boltdb. 'INDEX' in the name indicates an index.
ISSUES_DB = "issues"
DIGEST_INDEX = "digest"
TRACE_INDEX = "trace"
IGNORE_INDEX = "ignore"
TEST_INDEX = "test"
)
// Separator used to separate child and parent id in indices.
const IDX_SEPARATOR = "|"
// boltIssueStore implements the IssueStore interface.
type boltIssueStore struct {
store *boltutil.IndexedBucket
}
// New returns a new instance of IssueStore that is stored in the given directory.
func New(baseDir string) (IssueStore, error) {
baseDir, err := fileutil.EnsureDirExists(baseDir)
if err != nil {
return nil, err
}
db, err := bolt.Open(path.Join(baseDir, "issuestore.db"), 0600, nil)
if err != nil {
return nil, err
}
config := &boltutil.Config{
DB: db,
Name: "issues",
Indices: annotationIndices,
Codec: util.JSONCodec(&Annotation{}),
}
store, err := boltutil.NewIndexedBucket(config)
if err != nil {
return nil, err
}
return &boltIssueStore{
store: store,
}, nil
}
// ByDigest, see IgnoreStore interface.
func (b *boltIssueStore) ByDigest(digest string) ([]string, error) {
return b.readFromIndex(DIGEST_INDEX, digest)
}
// ByIgnore, see IgnoreStore interface.
func (b *boltIssueStore) ByIgnore(ignoreID string) ([]string, error) {
return b.readFromIndex(IGNORE_INDEX, ignoreID)
}
// ByTrace, see IgnoreStore interface.
func (b *boltIssueStore) ByTrace(traceID string) ([]string, error) {
return b.readFromIndex(TRACE_INDEX, traceID)
}
// ByTest, see IgnoreStore interface.
func (b *boltIssueStore) ByTest(testName string) ([]string, error) {
return b.readFromIndex(TEST_INDEX, testName)
}
// Get, see IgnoreStore interface.
func (b *boltIssueStore) Get(issueIDs []string) ([]*Annotation, error) {
if len(issueIDs) == 0 {
return []*Annotation{}, nil
}
result, err := b.store.Read(issueIDs)
if err != nil {
return nil, err
}
ret := make([]*Annotation, len(result))
for i, val := range result {
ret[i] = val.(*Annotation)
}
return ret, nil
}
// Add, see IgnoreStore interface.
func (b *boltIssueStore) Add(delta *Annotation) error {
if delta.IsEmpty() {
return nil
}
writeFn := func(tx *bolt.Tx, result []boltutil.Record) error {
if result[0] != nil {
// If there a no change then don't write any records.
if !result[0].(*Annotation).Add(delta) {
result[0] = nil
}
} else {
result[0] = delta
}
return nil
}
return b.store.Update([]boltutil.Record{delta}, writeFn)
}
// List, see IgnoreStore interface.
func (b *boltIssueStore) List(offset int, size int) ([]*Annotation, int, error) {
result, total, err := b.store.List(offset, size)
if err != nil {
return nil, 0, err
}
ret := make([]*Annotation, len(result))
for i, rec := range result {
ret[i] = rec.(*Annotation)
}
return ret, total, nil
}
// Subtract, see IgnoreStore interface.
func (b *boltIssueStore) Subtract(delta *Annotation) error {
writeFn := func(tx *bolt.Tx, result []boltutil.Record) error {
found := result[0]
if found != nil {
rec := found.(*Annotation)
// Subtract the delta and only take action if there was a change.
if rec.Subtract(delta) {
// If the resulting record is not empty, then we write it to disk.
if !rec.IsEmpty() {
return nil
}
// Delete the empty record.
if err := b.store.DeleteTx(tx, []string{rec.IssueID}); err != nil {
return err
}
}
}
result[0] = nil
return nil
}
return b.store.Update([]boltutil.Record{delta}, writeFn)
}
// Delete, see IgnoreStore interface.
func (b *boltIssueStore) Delete(issueIDs []string) error {
return b.store.Delete(issueIDs)
}
// readFromIndex does a lookup in the given index for the given value and
// returns all primary keys that match.
func (b *boltIssueStore) readFromIndex(index, value string) ([]string, error) {
ret, err := b.store.ReadIndex(index, []string{value})
if err != nil {
return nil, err
}
return ret[value], nil
}
// mergeStrings merges the strings of src into tgt. true is returned if the
// strings in tgt changed as a result of the merge.
func mergeStrings(tgt *[]string, src []string) bool {
if t := util.NewStringSet(*tgt, src); len(t) != len(*tgt) {
*tgt = t.Keys()
return true
}
return false
}
// removeStrings removes all strings from tgt that also appear in src. true is returned
// if tgt changed as part of the removal.
func removeStrings(tgt *[]string, src []string) bool {
if t := util.NewStringSet(*tgt).Complement(util.NewStringSet(src)); len(t) != len(*tgt) {
*tgt = t.Keys()
return true
}
return false
}