blob: aac544349e27174132361386eb5398311549a905 [file] [log] [blame]
package expectations
import (
"context"
"math"
"sync"
"time"
"go.skia.org/infra/golden/go/types"
)
// Store defines the interface for persisting expectations.
type Store interface {
// Get a snapshot of the current classifications for image digests.
Get(ctx context.Context) (ReadOnly, error)
// GetCopy a copy of the current classifications, safe for mutating.
GetCopy(ctx context.Context) (*Expectations, error)
// AddChange writes the given classified digests to the database and records the user that
// made the change. If two users are modifying changes at the same time, last one in wins.
AddChange(ctx context.Context, changes []Delta, userId string) error
// QueryLog returns a list of n entries starting at the given offset.
// If it is computationally cheap to do so, the second return value can be
// a count of the total number of CLs, or CountMany otherwise.
QueryLog(ctx context.Context, offset, n int, details bool) ([]TriageLogEntry, int, error)
// UndoChange reverts a change by setting all testname/digest pairs of the
// original change to the label they had before the change was applied.
// A new entry is added to the log with a reference to the change that was
// undone.
UndoChange(ctx context.Context, changeID, userID string) error
// ForChangelist returns a new Store that will deal with the Expectations for a Changelist
// with the given id (aka a CLExpectations). Any Expectations added to the returned Store
// will be kept separate from the master branch. Any Expectations returned should be
// treated as the delta between the MasterBranch and the given issue. The parameter crs is
// the CodeReviewSystem (e.g. "gerrit", "github") and id is the id of the CL in that CRS.p
// (This allows us to avoid a collision between two CLs with the same id in the event that
// we transition from one CRS to another).
ForChangelist(id, crs string) Store
// GetTriageHistory returns a slice of TriageHistory structs that indicate the last edits
// for a given grouping/digest. If not nil, the return value should be sorted such that
// the most recent entry is first.
GetTriageHistory(ctx context.Context, grouping types.TestName, digest types.Digest) ([]TriageHistory, error)
}
// GarbageCollector encapsulates methods that can be used to clean up expectations not used in a
// configurable amount of time.
type GarbageCollector interface {
// UpdateLastUsed will bulk update the given IDs as last used on the given time. This will
// not impact the "modified" timestamp for an entry. This bulk operation will not show up in
// the triage log.
UpdateLastUsed(context.Context, []ID, time.Time) error
// MarkUnusedEntriesForGC marks entries matching the given label for garbage collecting, provided
// they have a modified ts and a last used ts before the given ts. It returns the number of
// affected entries or an error if there were issues. This bulk operation need not appear in the
// triage log. It does not affect CL expectations.
MarkUnusedEntriesForGC(context.Context, Label, time.Time) (int, error)
// GarbageCollect removes all entries that have previously been marked for GC. It returns the
// number of affected entries or an error if there were issues. This bulk operation will not
// appear in the triage log.
GarbageCollect(context.Context) (int, error)
}
// Delta represents one changed digest and the label that was
// assigned as part of the triage operation.
type Delta struct {
Grouping types.TestName
Digest types.Digest
Label Label
}
// ID returns the ID for the Delta, as a method of convenience.
func (d Delta) ID() ID {
return ID{
Grouping: d.Grouping,
Digest: d.Digest,
}
}
// DeltaWithRange is unused. It represents the likely change that would be required to add support
// for expectation ranges.
type DeltaWithRange struct {
Delta
FirstIndex int
LastIndex int
}
// AsDelta converts an Expectations object into a slice of Deltas.
func AsDelta(e ReadOnly) []Delta {
var delta []Delta
_ = e.ForAll(func(tn types.TestName, d types.Digest, l Label) error {
delta = append(delta, Delta{Grouping: tn, Digest: d, Label: l})
return nil
})
return delta
}
// TriageHistory represents the changes that happened to a given expectation entry.
type TriageHistory struct {
User string
TS time.Time
}
// ID represents a unique identifier for an entry in the Store.
type ID struct {
Grouping types.TestName
Digest types.Digest
}
// TriageLogEntry represents a set of changes by a single person.
type TriageLogEntry struct {
ID string
User string
TS time.Time
ChangeCount int
Details []Delta
}
// CountMany indicates it is computationally expensive to determine exactly how many
// items there are.
var CountMany = math.MaxInt32
// ChangeNotifier represents a type that will be called when the master branch expectations change.
type ChangeNotifier interface {
NotifyChange(ID)
}
// ChangeEventRegisterer allows for the registration of callbacks that are called when the
// expectations on the master branch change.
type ChangeEventRegisterer interface {
ListenForChange(func(ID))
}
// ChangeEventDispatcher implements the ChangeEventRegisterer and ChangeNotifier interfaces. Calls to NotifyChange
// will be piped to any registered "ListenForChange" callbacks.
type ChangeEventDispatcher struct {
isSyncForTesting bool
callbacks []func(ID)
callbackMutex sync.Mutex
}
// NewEventDispatcher returns an empty event dispatcher to be used by production/
func NewEventDispatcher() *ChangeEventDispatcher {
return &ChangeEventDispatcher{
isSyncForTesting: false,
}
}
// NewEventDispatcherForTesting returns an empty event dispatcher to be used for testing. Calls
// to NotifyChange will synchronously call all registered callbacks and then return.
func NewEventDispatcherForTesting() *ChangeEventDispatcher {
return &ChangeEventDispatcher{
isSyncForTesting: true,
}
}
// NotifyChange implements the ChangeNotifier interface.
func (e *ChangeEventDispatcher) NotifyChange(d ID) {
e.callbackMutex.Lock()
defer e.callbackMutex.Unlock()
for _, fn := range e.callbacks {
if e.isSyncForTesting {
fn(d)
} else {
go fn(d)
}
}
}
// ListenForChange implements the ChangeEventRegisterer interface.
func (e *ChangeEventDispatcher) ListenForChange(fn func(ID)) {
e.callbackMutex.Lock()
defer e.callbackMutex.Unlock()
e.callbacks = append(e.callbacks, fn)
}
// Make sure ChangeEventDispatcher implements the ChangeEventRegisterer interface.
var _ ChangeEventRegisterer = (*ChangeEventDispatcher)(nil)
// Make sure ChangeEventDispatcher implements the ChangeNotifier interface.
var _ ChangeNotifier = (*ChangeEventDispatcher)(nil)