blob: f118e11a29efd0f3570a2cd612db16d930b09559 [file] [log] [blame]
// Copyright 2022 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package runs
import (
"context"
"time"
"go.chromium.org/luci/server/span"
"go.chromium.org/luci/analysis/internal/clustering/shards"
)
// ReclusteringTarget captures the rules and algorithms a re-clustering run
// is re-clustering to.
type ReclusteringTarget struct {
// RulesVersion is the rules version the re-clustering run is attempting
// to achieve.
RulesVersion time.Time `json:"rulesVersion"`
// ConfigVersion is the config version the re-clustering run is attempting
// to achieve.
ConfigVersion time.Time `json:"configVersion"`
// AlgorithmsVersion is the algorithms version the re-clustering run is
// attempting to achieve.
AlgorithmsVersion int64 `json:"algorithmsVersion"`
}
// ReclusteringProgress captures the progress re-clustering a
// given LUCI project's test results using specific rules
// versions or algorithms versions.
type ReclusteringProgress struct {
// ProgressPerMille is the progress of the current re-clustering run,
// measured in thousandths (per mille).
ProgressPerMille int `json:"progressPerMille"`
// Next is the goal of the current re-clustering run. (For which
// ProgressPerMille is specified.)
Next ReclusteringTarget `json:"next"`
// Last is the goal of the last completed re-clustering run.
Last ReclusteringTarget `json:"last"`
}
// ReadReclusteringProgress reads the re-clustering progress for
// the given LUCI project.
func ReadReclusteringProgress(ctx context.Context, project string) (*ReclusteringProgress, error) {
return ReadReclusteringProgressUpTo(ctx, project, MaxAttemptTimestamp)
}
// ReadReclusteringProgressUpTo reads the re-clustering progress for
// the given LUCI project up to the given reclustering attempt
// timestamp.
//
// For the latest re-clustering progess, pass MaxAttemptTimestamp.
// For re-clustering as it was at a given attempt minute in the
// past, pass the timestamp for that minute. Reading past reclustering
// progress is useful for understanding the versions of rules and
// algorithms reflected in outputs of BigQuery jobs which ran
// using data some minutes old.
//
// When reading historic progress, data for reclustering progress
// as it was part way through a minute is not available; this is
// also why the passed timestamp is called 'upToAttemptTimestamp'
// not an asAtTime.
func ReadReclusteringProgressUpTo(ctx context.Context, project string, upToAttemptTimestamp time.Time) (*ReclusteringProgress, error) {
// Reading reclustering progress as at a time in the past can also
// be achieved with Spanner stale reads, but this does not have
// good testability. Implement reading past reclustering state
// using the history we are already storing in the table.
txn, cancel := span.ReadOnlyTransaction(ctx)
defer cancel()
lastCompleted, err := ReadLastCompleteUpTo(txn, project, upToAttemptTimestamp)
if err != nil {
return nil, err
}
last, err := ReadLastUpTo(txn, project, upToAttemptTimestamp)
if err != nil {
return nil, err
}
runProgress := 0
// For the most recent run, try to read live progress
// from the reclustering shards table.
liveProgress, err := shards.ReadProgress(txn, project, last.AttemptTimestamp)
if err != nil {
return nil, err
}
// Use live progress if it is available.
if liveProgress.ShardCount > 0 && liveProgress.ShardCount == liveProgress.ShardsReported {
// Scale run progress to being from 0 to 1000.
runProgress = int(liveProgress.Progress / liveProgress.ShardCount)
if runProgress == shards.MaxProgress {
// If the run is complete.
lastCompleted = last
}
} else {
// Otherwise, try to use the progress that was on the last
// run with progress.
lastWithProgress, err := ReadLastWithProgressUpTo(txn, project, upToAttemptTimestamp)
if err != nil {
return nil, err
}
// If the last reclustering run row with progress has the same
// reclustering objective as the current run, use its progress.
if last.RulesVersion.Equal(lastWithProgress.RulesVersion) &&
last.AlgorithmsVersion == lastWithProgress.AlgorithmsVersion &&
last.ConfigVersion.Equal(lastWithProgress.ConfigVersion) {
// Scale run progress to being from 0 to 1000.
runProgress = int(lastWithProgress.Progress / lastWithProgress.ShardCount)
}
}
return &ReclusteringProgress{
ProgressPerMille: runProgress,
Next: ReclusteringTarget{
RulesVersion: last.RulesVersion,
ConfigVersion: last.ConfigVersion,
AlgorithmsVersion: last.AlgorithmsVersion,
},
Last: ReclusteringTarget{
RulesVersion: lastCompleted.RulesVersion,
ConfigVersion: lastCompleted.ConfigVersion,
AlgorithmsVersion: lastCompleted.AlgorithmsVersion,
},
}, nil
}
// IsReclusteringToNewAlgorithms returns whether LUCI Analysis's
// clustering output is being updated to use a newer standard of
// algorithms and is not yet stable. The algorithms version LUCI Analysis
// is re-clustering to is accessible via LatestAlgorithmsVersion.
func (p *ReclusteringProgress) IsReclusteringToNewAlgorithms() bool {
return p.Last.AlgorithmsVersion < p.Next.AlgorithmsVersion
}
// IsReclusteringToNewConfig returns whether LUCI Analysis's
// clustering output is in the process of being updated to a later
// configuration standard and is not yet stable.
// The configuration version LUCI Analysis is re-clustering to is
// accessible via LatestConfigVersion.
// Clients using re-clustering output should verify they are using
// the configuration version defined by LatestConfigVersion when
// interpreting the output.
func (p *ReclusteringProgress) IsReclusteringToNewConfig() bool {
return p.Last.ConfigVersion.Before(p.Next.ConfigVersion)
}
// IncorporatesRulesVersion returns returns whether LUCI Analysis
// clustering output incorporates all rule changes up to
// the given predicate last updated time. Later changes
// may also be included, in full or in part.
func (p *ReclusteringProgress) IncorporatesRulesVersion(rulesVersion time.Time) bool {
return !rulesVersion.After(p.Last.RulesVersion)
}