blob: f6a2f429cdd4cb5e72b0acc74fd75aeb99e04e80 [file] [log] [blame]
package status
import (
"bytes"
"context"
"encoding/gob"
"sync"
"cloud.google.com/go/datastore"
"go.skia.org/infra/autoroll/go/revision"
"go.skia.org/infra/go/autoroll"
"go.skia.org/infra/go/ds"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
)
var (
// Insert the AutoRollMiniStatus for these internal rollers into the
// external datastore.
EXPORT_WHITELIST = []string{
"android-master-autoroll",
"google3-autoroll",
}
)
// AutoRollStatus is a struct which provides roll-up status information about
// the AutoRoll Bot.
type AutoRollStatus struct {
AutoRollMiniStatus
ChildHead string `json:"childHead"`
ChildName string `json:"childName"`
CurrentRoll *autoroll.AutoRollIssue `json:"currentRoll"`
Error string `json:"error"`
FullHistoryUrl string `json:"fullHistoryUrl"`
IssueUrlBase string `json:"issueUrlBase"`
LastRoll *autoroll.AutoRollIssue `json:"lastRoll"`
NotRolledRevisions []*revision.Revision `json:"notRolledRevs"`
ParentName string `json:"parentName"`
Recent []*autoroll.AutoRollIssue `json:"recent"`
Status string `json:"status"`
ThrottledUntil int64 `json:"throttledUntil"`
ValidModes []string `json:"validModes"`
ValidStrategies []string `json:"validStrategies"`
}
// AutoRollMiniStatus is a struct which provides a minimal amount of status
// information about the AutoRoll Bot.
// TODO(borenet): Some of this duplicates things in AutoRollStatus. Revisit and
// either don't include AutoRollMiniStatus in AutoRollStatus or de-dupe the
// fields after revamping the UI.
type AutoRollMiniStatus struct {
// Revision of the current roll, if any.
CurrentRollRev string `json:"currentRollRev"`
// Revision of the last successful roll.
LastRollRev string `json:"lastRollRev"`
// The current mode of the roller.
// Note: This duplicates what is stored in the modes DB but is more
// convenient for users like status.skia.org.
Mode string `json:"mode"`
// The number of failed rolls since the last successful roll.
NumFailedRolls int `json:"numFailed"`
// The number of commits which have not been rolled.
NumNotRolledCommits int `json:"numBehind"`
}
// Fake ancestor we supply for all AutoRollStatus, to force strong consistency.
// We lose some performance this way but it keeps our tests from flaking.
func fakeAncestor() *datastore.Key {
rv := ds.NewKey(ds.KIND_AUTOROLL_STATUS_ANCESTOR)
rv.ID = 13 // Bogus ID.
return rv
}
// DsStatusWrapper is a helper struct used for storing an AutoRollStatus in the
// datastore.
type DsStatusWrapper struct {
Data []byte `datastore:"data,noindex"`
Roller string `datastore:"roller"`
}
// Create a key for the given roller.
func key(rollerName string) *datastore.Key {
key := ds.NewKey(ds.KIND_AUTOROLL_STATUS)
key.Name = rollerName
key.Parent = fakeAncestor()
return key
}
// Set the AutoRollStatus for the given roller in the datastore. Should only
// be called by the roller itself.
func Set(ctx context.Context, rollerName string, st *AutoRollStatus) error {
buf := bytes.NewBuffer(nil)
if err := gob.NewEncoder(buf).Encode(st); err != nil {
return err
}
w := &DsStatusWrapper{
Data: buf.Bytes(),
Roller: rollerName,
}
_, err := ds.DS.RunInTransaction(ctx, func(tx *datastore.Transaction) error {
_, err := tx.Put(key(rollerName), w)
return err
})
if err != nil {
return err
}
// Optionally export the mini version of the internal roller's status
// to the external datastore.
if util.In(rollerName, EXPORT_WHITELIST) {
exportStatus := &AutoRollStatus{
AutoRollMiniStatus: st.AutoRollMiniStatus,
}
buf := bytes.NewBuffer(nil)
if err := gob.NewEncoder(buf).Encode(exportStatus); err != nil {
return err
}
w := &DsStatusWrapper{
Data: buf.Bytes(),
Roller: rollerName,
}
_, err := ds.DS.RunInTransaction(ctx, func(tx *datastore.Transaction) error {
k := key(rollerName)
k.Namespace = ds.AUTOROLL_NS
k.Parent.Namespace = ds.AUTOROLL_NS
_, err := tx.Put(k, w)
return err
})
if err != nil {
return err
}
}
return nil
}
// Get the AutoRollStatus for the given roller from the datastore. Most callers
// should use AutoRollStatusCache.
func Get(ctx context.Context, rollerName string) (*AutoRollStatus, error) {
var w DsStatusWrapper
if err := ds.DS.Get(ctx, key(rollerName), &w); err != nil {
return nil, err
}
rv := new(AutoRollStatus)
if err := gob.NewDecoder(bytes.NewReader(w.Data)).Decode(rv); err != nil {
return nil, err
}
return rv, nil
}
// AutoRollStatusCache is a struct used for caching roll-up status
// information about the AutoRoll Bot.
type AutoRollStatusCache struct {
mtx sync.RWMutex
roller string
status *AutoRollStatus
}
// NewCache returns an AutoRollStatusCache instance.
func NewCache(ctx context.Context, rollerName string) (*AutoRollStatusCache, error) {
c := &AutoRollStatusCache{
roller: rollerName,
}
if err := c.Update(ctx); err != nil {
return nil, err
}
return c, nil
}
// Return the AutoRollStatus as of the last call to Update().
func (c *AutoRollStatusCache) Get() *AutoRollStatus {
c.mtx.RLock()
defer c.mtx.RUnlock()
return c.status.Copy()
}
func (s *AutoRollStatus) Copy() *AutoRollStatus {
if s == nil {
sklog.Warningf("Copying nil AutoRollStatus.")
return nil
}
var recent []*autoroll.AutoRollIssue
if s.Recent != nil {
recent = make([]*autoroll.AutoRollIssue, 0, len(s.Recent))
for _, r := range s.Recent {
recent = append(recent, r.Copy())
}
}
var notRolledRevisions []*revision.Revision
if s.NotRolledRevisions != nil {
notRolledRevisions = make([]*revision.Revision, 0, len(s.NotRolledRevisions))
for _, r := range s.NotRolledRevisions {
notRolledRevisions = append(notRolledRevisions, r.Copy())
}
}
rv := &AutoRollStatus{
AutoRollMiniStatus: AutoRollMiniStatus{
CurrentRollRev: s.CurrentRollRev,
LastRollRev: s.LastRollRev,
NumFailedRolls: s.NumFailedRolls,
NumNotRolledCommits: s.NumNotRolledCommits,
},
ChildHead: s.ChildHead,
ChildName: s.ChildName,
Error: s.Error,
FullHistoryUrl: s.FullHistoryUrl,
IssueUrlBase: s.IssueUrlBase,
NotRolledRevisions: notRolledRevisions,
ParentName: s.ParentName,
Recent: recent,
Status: s.Status,
ThrottledUntil: s.ThrottledUntil,
ValidModes: util.CopyStringSlice(s.ValidModes),
ValidStrategies: util.CopyStringSlice(s.ValidStrategies),
}
if s.CurrentRoll != nil {
rv.CurrentRoll = s.CurrentRoll.Copy()
}
if s.LastRoll != nil {
rv.LastRoll = s.LastRoll.Copy()
}
return rv
}
// Return the AutoRollMiniStatus as of the last call to Update().
func (c *AutoRollStatusCache) GetMini() *AutoRollMiniStatus {
return &c.Get().AutoRollMiniStatus
}
// Update updates the current status information.
func (c *AutoRollStatusCache) Update(ctx context.Context) error {
status, err := Get(ctx, c.roller)
if err == datastore.ErrNoSuchEntity || status == nil {
// This will occur the first time the roller starts,
// before it sets the status for the first time. Ignore.
sklog.Warningf("Unable to find AutoRollStatus for %s. Is this the first startup for this roller?", c.roller)
status = &AutoRollStatus{}
} else if err != nil {
return err
}
c.mtx.Lock()
defer c.mtx.Unlock()
c.status = status
return nil
}