go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/clustering/runs/span.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 "cloud.google.com/go/spanner" 22 23 "go.chromium.org/luci/common/errors" 24 "go.chromium.org/luci/server/span" 25 26 "go.chromium.org/luci/analysis/internal/clustering/rules" 27 "go.chromium.org/luci/analysis/internal/clustering/shards" 28 "go.chromium.org/luci/analysis/internal/config" 29 spanutil "go.chromium.org/luci/analysis/internal/span" 30 "go.chromium.org/luci/analysis/pbutil" 31 ) 32 33 // ReclusteringRun contains the details of a runs used to re-cluster 34 // test results. 35 type ReclusteringRun struct { 36 // The LUCI Project for which this rule is defined. 37 Project string 38 // The attempt. This is the time the orchestrator run ends. 39 AttemptTimestamp time.Time 40 // The minimum algorithms version this reclustering run is trying 41 // to achieve. Chunks with an AlgorithmsVersion less than this 42 // value are eligible to be re-clustered. 43 AlgorithmsVersion int64 44 // The minimum config version the reclustering run is trying to achieve. 45 // Chunks with a ConfigVersion less than this value are eligible to be 46 // re-clustered. 47 ConfigVersion time.Time 48 // The minimum rules version the reclustering run is trying to achieve. 49 // Chunks with a RulesVersion less than this value are eligible to be 50 // re-clustered. 51 RulesVersion time.Time 52 // The number of shards created for this run (for this LUCI project). 53 ShardCount int64 54 // The number of shards that have reported progress (at least once). 55 // When this is equal to ShardCount, readers can have confidence Progress 56 // is a reasonable reflection of the progress made reclustering 57 // this project. Until then, it is a loose lower-bound. 58 ShardsReported int64 59 // The progress. This is a value between 0 and 1000*ShardCount. 60 Progress int64 61 } 62 63 // NotFound is the error returned by Read if the row could not be found. 64 var NotFound = errors.New("reclustering run row not found") 65 66 // StartingEpoch is the earliest valid run attempt time. 67 var StartingEpoch = shards.StartingEpoch 68 69 // MaxAttemptTimestamp can be passed to any Read....() method to 70 // return data up to the last attempt. 71 var MaxAttemptTimestamp = time.Date(9999, 12, 31, 23, 59, 0, 0, time.UTC) 72 73 // Read reads the run with the given attempt timestamp in the given LUCI 74 // project. If the row does not exist, the error NotFound is returned. 75 func Read(ctx context.Context, projectID string, attemptTimestamp time.Time) (*ReclusteringRun, error) { 76 whereClause := `AttemptTimestamp = @attemptTimestamp` 77 params := map[string]any{ 78 "attemptTimestamp": attemptTimestamp, 79 } 80 r, err := readLastWhere(ctx, projectID, whereClause, params) 81 if err != nil { 82 return nil, errors.Annotate(err, "query run").Err() 83 } 84 if r == nil { 85 return nil, NotFound 86 } 87 return r, nil 88 } 89 90 // ReadLastUpTo reads the last run in the given LUCI project up to 91 // the given attempt timestamp. If no row exists, 92 // a fake run is returned with the following details: 93 // - Project matching the requested Project ID. 94 // - AttemptTimestamp of StartingEpoch. 95 // - AlgorithmsVersion of 1. 96 // - ConfigVersion of clusteringcfg.StartingEpoch. 97 // - RulesVersion of rules.StartingEpoch. 98 // - ShardCount and ShardsReported of 1. 99 // - Progress of 1000. 100 func ReadLastUpTo(ctx context.Context, projectID string, upToAttemptTimestamp time.Time) (*ReclusteringRun, error) { 101 whereClause := `AttemptTimestamp <= @upToAttemptTimestamp` 102 params := map[string]any{ 103 "upToAttemptTimestamp": upToAttemptTimestamp, 104 } 105 r, err := readLastWhere(ctx, projectID, whereClause, params) 106 if err != nil { 107 return nil, errors.Annotate(err, "query last run").Err() 108 } 109 if r == nil { 110 r = fakeLastRow(projectID) 111 } 112 return r, nil 113 } 114 115 // ReadLastWithProgress reads the last run with progress in the given LUCI 116 // project up to the given attempt timestamp. 117 // 118 // If no row exists, a fake row is returned; see ReadLast for details. 119 func ReadLastWithProgressUpTo(ctx context.Context, projectID string, upToAttemptTimestamp time.Time) (*ReclusteringRun, error) { 120 whereClause := `ShardsReported = ShardCount AND AttemptTimestamp <= @upToAttemptTimestamp` 121 params := map[string]any{ 122 "upToAttemptTimestamp": upToAttemptTimestamp, 123 } 124 r, err := readLastWhere(ctx, projectID, whereClause, params) 125 if err != nil { 126 return nil, errors.Annotate(err, "query last run with progress up to").Err() 127 } 128 if r == nil { 129 r = fakeLastRow(projectID) 130 } 131 return r, nil 132 } 133 134 // ReadLastCompleteUpTo reads the last run that completed in the given LUCI 135 // project up to the given attempt timestamp. 136 // If no row exists, a fake row is returned; see ReadLast for details. 137 func ReadLastCompleteUpTo(ctx context.Context, projectID string, upToAttemptTimestamp time.Time) (*ReclusteringRun, error) { 138 whereClause := `Progress = (ShardCount * 1000) AND AttemptTimestamp <= @upToAttemptTimestamp` 139 params := map[string]any{ 140 "upToAttemptTimestamp": upToAttemptTimestamp, 141 } 142 r, err := readLastWhere(ctx, projectID, whereClause, params) 143 if err != nil { 144 return nil, errors.Annotate(err, "query last run up to").Err() 145 } 146 if r == nil { 147 r = fakeLastRow(projectID) 148 } 149 return r, nil 150 } 151 152 func fakeLastRow(projectID string) *ReclusteringRun { 153 return &ReclusteringRun{ 154 Project: projectID, 155 AttemptTimestamp: StartingEpoch, 156 AlgorithmsVersion: 1, 157 ConfigVersion: config.StartingEpoch, 158 RulesVersion: rules.StartingEpoch, 159 ShardCount: 1, 160 ShardsReported: 1, 161 Progress: 1000, 162 } 163 } 164 165 // readLastWhere reads the last run matching the given where clause, 166 // substituting params for any SQL parameters used in that clause. 167 func readLastWhere(ctx context.Context, projectID string, whereClause string, params map[string]any) (*ReclusteringRun, error) { 168 stmt := spanner.NewStatement(` 169 SELECT 170 AttemptTimestamp, ConfigVersion, RulesVersion, 171 AlgorithmsVersion, ShardCount, ShardsReported, Progress 172 FROM ReclusteringRuns 173 WHERE Project = @projectID AND (` + whereClause + `) 174 ORDER BY AttemptTimestamp DESC 175 LIMIT 1 176 `) 177 for k, v := range params { 178 stmt.Params[k] = v 179 } 180 stmt.Params["projectID"] = projectID 181 182 it := span.Query(ctx, stmt) 183 rs := []*ReclusteringRun{} 184 err := it.Do(func(r *spanner.Row) error { 185 var attemptTimestamp, rulesVersion, configVersion time.Time 186 var algorithmsVersion, shardCount, shardsReported, progress int64 187 err := r.Columns( 188 &attemptTimestamp, &configVersion, &rulesVersion, 189 &algorithmsVersion, &shardCount, &shardsReported, &progress, 190 ) 191 if err != nil { 192 return errors.Annotate(err, "read run row").Err() 193 } 194 195 run := &ReclusteringRun{ 196 Project: projectID, 197 AttemptTimestamp: attemptTimestamp, 198 AlgorithmsVersion: algorithmsVersion, 199 ConfigVersion: configVersion, 200 RulesVersion: rulesVersion, 201 ShardCount: shardCount, 202 ShardsReported: shardsReported, 203 Progress: progress, 204 } 205 rs = append(rs, run) 206 return nil 207 }) 208 if len(rs) > 0 { 209 return rs[0], err 210 } 211 return nil, err 212 } 213 214 // Create inserts a new reclustering run. 215 func Create(ctx context.Context, r *ReclusteringRun) error { 216 if err := validateRun(r); err != nil { 217 return err 218 } 219 ms := spanutil.InsertMap("ReclusteringRuns", map[string]any{ 220 "Project": r.Project, 221 "AttemptTimestamp": r.AttemptTimestamp, 222 "AlgorithmsVersion": r.AlgorithmsVersion, 223 "ConfigVersion": r.ConfigVersion, 224 "RulesVersion": r.RulesVersion, 225 "ShardCount": r.ShardCount, 226 "ShardsReported": r.ShardsReported, 227 "Progress": r.Progress, 228 }) 229 span.BufferWrite(ctx, ms) 230 return nil 231 } 232 233 func validateRun(r *ReclusteringRun) error { 234 if err := pbutil.ValidateProject(r.Project); err != nil { 235 return errors.Annotate(err, "project").Err() 236 } 237 switch { 238 case r.AttemptTimestamp.Before(StartingEpoch): 239 return errors.New("attempt timestamp must be valid") 240 case r.AlgorithmsVersion <= 0: 241 return errors.New("algorithms version must be valid") 242 case r.ConfigVersion.Before(config.StartingEpoch): 243 return errors.New("config version must be valid") 244 case r.RulesVersion.Before(rules.StartingEpoch): 245 return errors.New("rules version must be valid") 246 case r.ShardCount <= 0: 247 return errors.New("shard count must be valid") 248 case r.ShardsReported < 0 || r.ShardsReported > r.ShardCount: 249 return errors.New("shards reported must be valid") 250 case r.Progress < 0 || r.Progress > (r.ShardCount*1000): 251 return errors.New("progress must be valid") 252 } 253 return nil 254 } 255 256 // UpdateProgress sets the progress of a particular run. 257 func UpdateProgress(ctx context.Context, projectID string, attemptTimestamp time.Time, shardsReported, progress int64) error { 258 ms := spanutil.UpdateMap("ReclusteringRuns", map[string]any{ 259 "Project": projectID, 260 "AttemptTimestamp": attemptTimestamp, 261 "ShardsReported": shardsReported, 262 "Progress": progress, 263 }) 264 span.BufferWrite(ctx, ms) 265 return nil 266 }