go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/clustering/runs/progress.go (about) 1 // Copyright 2022 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package runs 16 17 import ( 18 "context" 19 "time" 20 21 "go.chromium.org/luci/server/span" 22 23 "go.chromium.org/luci/analysis/internal/clustering/shards" 24 ) 25 26 // ReclusteringTarget captures the rules and algorithms a re-clustering run 27 // is re-clustering to. 28 type ReclusteringTarget struct { 29 // RulesVersion is the rules version the re-clustering run is attempting 30 // to achieve. 31 RulesVersion time.Time `json:"rulesVersion"` 32 // ConfigVersion is the config version the re-clustering run is attempting 33 // to achieve. 34 ConfigVersion time.Time `json:"configVersion"` 35 // AlgorithmsVersion is the algorithms version the re-clustering run is 36 // attempting to achieve. 37 AlgorithmsVersion int64 `json:"algorithmsVersion"` 38 } 39 40 // ReclusteringProgress captures the progress re-clustering a 41 // given LUCI project's test results using specific rules 42 // versions or algorithms versions. 43 type ReclusteringProgress struct { 44 // ProgressPerMille is the progress of the current re-clustering run, 45 // measured in thousandths (per mille). 46 ProgressPerMille int `json:"progressPerMille"` 47 // Next is the goal of the current re-clustering run. (For which 48 // ProgressPerMille is specified.) 49 Next ReclusteringTarget `json:"next"` 50 // Last is the goal of the last completed re-clustering run. 51 Last ReclusteringTarget `json:"last"` 52 } 53 54 // ReadReclusteringProgress reads the re-clustering progress for 55 // the given LUCI project. 56 func ReadReclusteringProgress(ctx context.Context, project string) (*ReclusteringProgress, error) { 57 return ReadReclusteringProgressUpTo(ctx, project, MaxAttemptTimestamp) 58 } 59 60 // ReadReclusteringProgressUpTo reads the re-clustering progress for 61 // the given LUCI project up to the given reclustering attempt 62 // timestamp. 63 // 64 // For the latest re-clustering progess, pass MaxAttemptTimestamp. 65 // For re-clustering as it was at a given attempt minute in the 66 // past, pass the timestamp for that minute. Reading past reclustering 67 // progress is useful for understanding the versions of rules and 68 // algorithms reflected in outputs of BigQuery jobs which ran 69 // using data some minutes old. 70 // 71 // When reading historic progress, data for reclustering progress 72 // as it was part way through a minute is not available; this is 73 // also why the passed timestamp is called 'upToAttemptTimestamp' 74 // not an asAtTime. 75 func ReadReclusteringProgressUpTo(ctx context.Context, project string, upToAttemptTimestamp time.Time) (*ReclusteringProgress, error) { 76 77 // Reading reclustering progress as at a time in the past can also 78 // be achieved with Spanner stale reads, but this does not have 79 // good testability. Implement reading past reclustering state 80 // using the history we are already storing in the table. 81 txn, cancel := span.ReadOnlyTransaction(ctx) 82 defer cancel() 83 84 lastCompleted, err := ReadLastCompleteUpTo(txn, project, upToAttemptTimestamp) 85 if err != nil { 86 return nil, err 87 } 88 89 last, err := ReadLastUpTo(txn, project, upToAttemptTimestamp) 90 if err != nil { 91 return nil, err 92 } 93 94 runProgress := 0 95 96 // For the most recent run, try to read live progress 97 // from the reclustering shards table. 98 liveProgress, err := shards.ReadProgress(txn, project, last.AttemptTimestamp) 99 if err != nil { 100 return nil, err 101 } 102 // Use live progress if it is available. 103 if liveProgress.ShardCount > 0 && liveProgress.ShardCount == liveProgress.ShardsReported { 104 // Scale run progress to being from 0 to 1000. 105 runProgress = int(liveProgress.Progress / liveProgress.ShardCount) 106 if runProgress == shards.MaxProgress { 107 // If the run is complete. 108 lastCompleted = last 109 } 110 } else { 111 // Otherwise, try to use the progress that was on the last 112 // run with progress. 113 lastWithProgress, err := ReadLastWithProgressUpTo(txn, project, upToAttemptTimestamp) 114 if err != nil { 115 return nil, err 116 } 117 118 // If the last reclustering run row with progress has the same 119 // reclustering objective as the current run, use its progress. 120 if last.RulesVersion.Equal(lastWithProgress.RulesVersion) && 121 last.AlgorithmsVersion == lastWithProgress.AlgorithmsVersion && 122 last.ConfigVersion.Equal(lastWithProgress.ConfigVersion) { 123 // Scale run progress to being from 0 to 1000. 124 runProgress = int(lastWithProgress.Progress / lastWithProgress.ShardCount) 125 } 126 } 127 128 return &ReclusteringProgress{ 129 ProgressPerMille: runProgress, 130 Next: ReclusteringTarget{ 131 RulesVersion: last.RulesVersion, 132 ConfigVersion: last.ConfigVersion, 133 AlgorithmsVersion: last.AlgorithmsVersion, 134 }, 135 Last: ReclusteringTarget{ 136 RulesVersion: lastCompleted.RulesVersion, 137 ConfigVersion: lastCompleted.ConfigVersion, 138 AlgorithmsVersion: lastCompleted.AlgorithmsVersion, 139 }, 140 }, nil 141 } 142 143 // IsReclusteringToNewAlgorithms returns whether LUCI Analysis's 144 // clustering output is being updated to use a newer standard of 145 // algorithms and is not yet stable. The algorithms version LUCI Analysis 146 // is re-clustering to is accessible via LatestAlgorithmsVersion. 147 func (p *ReclusteringProgress) IsReclusteringToNewAlgorithms() bool { 148 return p.Last.AlgorithmsVersion < p.Next.AlgorithmsVersion 149 } 150 151 // IsReclusteringToNewConfig returns whether LUCI Analysis's 152 // clustering output is in the process of being updated to a later 153 // configuration standard and is not yet stable. 154 // The configuration version LUCI Analysis is re-clustering to is 155 // accessible via LatestConfigVersion. 156 // Clients using re-clustering output should verify they are using 157 // the configuration version defined by LatestConfigVersion when 158 // interpreting the output. 159 func (p *ReclusteringProgress) IsReclusteringToNewConfig() bool { 160 return p.Last.ConfigVersion.Before(p.Next.ConfigVersion) 161 } 162 163 // IncorporatesRulesVersion returns returns whether LUCI Analysis 164 // clustering output incorporates all rule changes up to 165 // the given predicate last updated time. Later changes 166 // may also be included, in full or in part. 167 func (p *ReclusteringProgress) IncorporatesRulesVersion(rulesVersion time.Time) bool { 168 return !rulesVersion.After(p.Last.RulesVersion) 169 }