blob: 813035ffc104285486c01cd2a72ade00f68dc2e7 [file] [log] [blame]
package ds_ignorestore
import (
"context"
"sort"
"strconv"
"time"
"cloud.google.com/go/datastore"
ttlcache "github.com/patrickmn/go-cache"
"golang.org/x/sync/errgroup"
"go.skia.org/infra/go/ds"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/golden/go/dsutil"
"go.skia.org/infra/golden/go/ignore"
)
// DSIgnoreStore implements the IgnoreStore interface
type DSIgnoreStore struct {
client *datastore.Client
recentKeysList *dsutil.RecentKeysList
ignoreCache *ttlcache.Cache
}
const (
// We could probably cache this longer, since we only expect there to be one
// reader and writer (and we dump the cache on create/update/delete)
ignoreCacheFreshness = 5 * time.Minute
listCacheKey = "listCacheKey"
)
// dsRule represents how rules are stored in DataStore. This may be distinct to how
// they are represented by the frontend.
type dsRule struct {
ID int64
Name string
UpdatedBy string
Expires time.Time
Query string
Note string
}
func fromRule(r ignore.Rule) (*dsRule, error) {
var id int64
if r.ID != "" {
var err error
id, err = strconv.ParseInt(r.ID, 10, 64)
if err != nil {
return nil, skerr.Wrapf(err, "id must be int64: %q", id)
}
}
return &dsRule{
ID: id,
Name: r.Name,
UpdatedBy: r.UpdatedBy,
Expires: r.Expires,
Query: r.Query,
Note: r.Note,
}, nil
}
func (r *dsRule) toRule() ignore.Rule {
return ignore.Rule{
ID: strconv.FormatInt(r.ID, 10),
Name: r.Name,
UpdatedBy: r.UpdatedBy,
Expires: r.Expires,
Query: r.Query,
Note: r.Note,
}
}
// New returns an IgnoreStore instance that is backed by Cloud Datastore.
func New(client *datastore.Client) (*DSIgnoreStore, error) {
if client == nil {
return nil, skerr.Fmt("Received nil for datastore client.")
}
containerKey := ds.NewKey(ds.HELPER_RECENT_KEYS)
containerKey.Name = "ignore:recent-keys"
store := &DSIgnoreStore{
client: client,
recentKeysList: dsutil.NewRecentKeysList(client, containerKey, dsutil.DefaultConsistencyDelta),
ignoreCache: ttlcache.New(ignoreCacheFreshness, ignoreCacheFreshness),
}
return store, nil
}
// Create implements the IgnoreStore interface.
func (c *DSIgnoreStore) Create(ctx context.Context, ir ignore.Rule) error {
c.ignoreCache.Delete(listCacheKey)
r, err := fromRule(ir)
if err != nil {
return skerr.Wrap(err)
}
createFn := func(tx *datastore.Transaction) error {
key := dsutil.TimeSortableKey(ds.IGNORE_RULE, 0)
r.ID = key.ID
// Add the new rule and put its key with the recently added keys.
if _, err := tx.Put(key, r); err != nil {
return err
}
ir.ID = strconv.FormatInt(r.ID, 10)
return c.recentKeysList.Add(tx, key)
}
// Run the relevant updates in a transaction.
_, err = c.client.RunInTransaction(ctx, createFn)
return skerr.Wrap(err)
}
// List implements the IgnoreStore interface.
func (c *DSIgnoreStore) List(ctx context.Context) ([]ignore.Rule, error) {
if rules, ok := c.ignoreCache.Get(listCacheKey); ok {
rv, ok := rules.([]ignore.Rule)
if ok {
return rv, nil
}
sklog.Warningf("corrupt data in cache, refetching")
c.ignoreCache.Delete(listCacheKey)
}
var egroup errgroup.Group
var queriedKeys []*datastore.Key
egroup.Go(func() error {
// Query all entities.
query := ds.NewQuery(ds.IGNORE_RULE).KeysOnly()
var err error
queriedKeys, err = c.client.GetAll(ctx, query, nil)
return skerr.Wrap(err)
})
var recently *dsutil.Recently
egroup.Go(func() error {
var err error
recently, err = c.recentKeysList.GetRecent()
return skerr.Wrap(err)
})
if err := egroup.Wait(); err != nil {
return nil, skerr.Wrapf(err, "getting keys of ignore rules")
}
// Merge the keys to get all of the current keys.
allKeys := recently.Combine(queriedKeys)
if len(allKeys) == 0 {
return []ignore.Rule{}, nil
}
rules := make([]*dsRule, len(allKeys))
if err := c.client.GetMulti(ctx, allKeys, rules); err != nil {
return nil, skerr.Wrap(err)
}
ret := make([]ignore.Rule, 0, len(rules))
for _, r := range rules {
ret = append(ret, r.toRule())
}
sort.Slice(ret, func(i, j int) bool { return ret[i].Expires.Before(ret[j].Expires) })
c.ignoreCache.SetDefault(listCacheKey, ret)
return ret, nil
}
// Update implements the IgnoreStore interface.
func (c *DSIgnoreStore) Update(ctx context.Context, rule ignore.Rule) error {
if rule.ID == "" {
return skerr.Fmt("Updated an empty rule")
}
r, err := fromRule(rule)
if err != nil {
return skerr.Wrap(err)
}
key := ds.NewKey(ds.IGNORE_RULE)
key.ID = r.ID
c.ignoreCache.Delete(listCacheKey)
_, err = c.client.Mutate(ctx, datastore.NewUpdate(key, r))
return skerr.Wrap(err)
}
// Delete implements the IgnoreStore interface.
func (c *DSIgnoreStore) Delete(ctx context.Context, idStr string) (int, error) {
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
return 0, skerr.Wrapf(err, "id must be int64: %q", idStr)
}
if id <= 0 {
return 0, skerr.Fmt("Given id does not exist: %d", id)
}
deleteFn := func(tx *datastore.Transaction) error {
key := ds.NewKey(ds.IGNORE_RULE)
key.ID = id
ir := &dsRule{}
if err := tx.Get(key, ir); err != nil {
return skerr.Wrap(err)
}
if err := tx.Delete(key); err != nil {
return skerr.Wrap(err)
}
return c.recentKeysList.Delete(tx, key)
}
c.ignoreCache.Delete(listCacheKey)
// Run the relevant updates in a transaction.
_, err = c.client.RunInTransaction(ctx, deleteFn)
if err != nil {
// Don't report an error if the item did not exist.
if skerr.Unwrap(err) == datastore.ErrNoSuchEntity {
sklog.Warningf("Could not delete ignore with id %d because it did not exist", id)
return 0, nil
}
return 0, skerr.Wrap(err)
}
return 1, nil
}