go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/clustering/shards/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 shards provides methods to access the ReclusteringShards 16 // Spanner table. The table is used by reclustering shards to report progress. 17 package shards 18 19 import ( 20 "context" 21 "time" 22 23 "cloud.google.com/go/spanner" 24 25 "go.chromium.org/luci/common/errors" 26 "go.chromium.org/luci/server/span" 27 28 spanutil "go.chromium.org/luci/analysis/internal/span" 29 "go.chromium.org/luci/analysis/pbutil" 30 ) 31 32 // ReclusteringShard is used to for shards to report progress re-clustering 33 // test results. 34 type ReclusteringShard struct { 35 // A unique number assigned to shard. Shards are numbered sequentially, 36 // starting from one. 37 ShardNumber int64 38 // The attempt. This is the time the orchestrator run ends. 39 AttemptTimestamp time.Time 40 // The LUCI Project the shard is doing reclustering for. 41 Project string 42 // The progress. This is a value between 0 and 1000. If this is NULL, 43 // it means progress has not yet been reported by the shard. 44 Progress spanner.NullInt64 45 } 46 47 // ReclusteringProgress is the result of reading the progress of a project's 48 // shards for one reclustering attempt. 49 type ReclusteringProgress struct { 50 // The LUCI Project. 51 Project string 52 // The attempt. This is the time the orchestrator run ends. 53 AttemptTimestamp time.Time 54 // The number of shards running for the project. 55 ShardCount int64 56 // The number of shards which have reported progress. 57 ShardsReported int64 58 // The total progress reported for the project. This is a value 59 // between 0 and 1000*ShardCount. 60 Progress int64 61 } 62 63 // StartingEpoch is the earliest valid run attempt time. 64 var StartingEpoch = time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC) 65 66 // MaxProgress is the maximum progress value for a shard, corresponding to 67 // 100% complete reclustering. 68 const MaxProgress = 1000 69 70 // ReadProgress reads all the progress of reclustering the given 71 // LUCI Project, for the given attempt timestamp. 72 func ReadProgress(ctx context.Context, project string, attemptTimestamp time.Time) (ReclusteringProgress, error) { 73 whereClause := "Project = @project AND AttemptTimestamp = @attemptTimestamp" 74 params := map[string]any{ 75 "project": project, 76 "attemptTimestamp": attemptTimestamp, 77 } 78 progress, err := readProgressWhere(ctx, whereClause, params) 79 if err != nil { 80 return ReclusteringProgress{}, err 81 } 82 if len(progress) == 0 { 83 // No progress available. 84 return ReclusteringProgress{ 85 Project: project, 86 AttemptTimestamp: attemptTimestamp, 87 }, nil 88 } 89 return progress[0], nil 90 } 91 92 // ReadAllProgresses reads reclustering progress for ALL 93 // projects with shards, for the given attempt timestamp. 94 func ReadAllProgresses(ctx context.Context, attemptTimestamp time.Time) ([]ReclusteringProgress, error) { 95 whereClause := "AttemptTimestamp = @attemptTimestamp" 96 params := map[string]any{ 97 "attemptTimestamp": attemptTimestamp, 98 } 99 return readProgressWhere(ctx, whereClause, params) 100 } 101 102 // readProgressWhere reads reclustering progress satisfying the given 103 // where clause. 104 func readProgressWhere(ctx context.Context, whereClause string, params map[string]any) ([]ReclusteringProgress, error) { 105 stmt := spanner.NewStatement(` 106 SELECT 107 AttemptTimestamp, 108 Project, 109 SUM(Progress) as Progress, 110 COUNT(1) as ShardCount, 111 COUNTIF(Progress IS NOT NULL) as ShardsReported, 112 FROM ReclusteringShards 113 WHERE (` + whereClause + `) 114 GROUP BY AttemptTimestamp, Project 115 ORDER BY AttemptTimestamp, Project 116 `) 117 for k, v := range params { 118 stmt.Params[k] = v 119 } 120 121 it := span.Query(ctx, stmt) 122 results := []ReclusteringProgress{} 123 err := it.Do(func(r *spanner.Row) error { 124 var attemptTimestamp time.Time 125 var project string 126 var progress spanner.NullInt64 127 var shardCount, shardsReported int64 128 err := r.Columns( 129 &attemptTimestamp, &project, &progress, &shardCount, &shardsReported, 130 ) 131 if err != nil { 132 return errors.Annotate(err, "read shard row").Err() 133 } 134 135 result := ReclusteringProgress{ 136 AttemptTimestamp: attemptTimestamp, 137 Project: project, 138 Progress: progress.Int64, 139 ShardCount: shardCount, 140 ShardsReported: shardsReported, 141 } 142 results = append(results, result) 143 return nil 144 }) 145 if err != nil { 146 return nil, err 147 } 148 return results, nil 149 } 150 151 // ReadAll reads all reclustering shards. 152 // For testing use only. 153 func ReadAll(ctx context.Context) ([]ReclusteringShard, error) { 154 stmt := spanner.NewStatement(` 155 SELECT 156 ShardNumber, AttemptTimestamp, Project, Progress 157 FROM ReclusteringShards 158 ORDER BY ShardNumber 159 `) 160 161 it := span.Query(ctx, stmt) 162 results := []ReclusteringShard{} 163 err := it.Do(func(r *spanner.Row) error { 164 var shardNumber int64 165 var attemptTimestamp time.Time 166 var project string 167 var progress spanner.NullInt64 168 err := r.Columns( 169 &shardNumber, &attemptTimestamp, &project, &progress, 170 ) 171 if err != nil { 172 return errors.Annotate(err, "read shard row").Err() 173 } 174 175 shard := ReclusteringShard{ 176 ShardNumber: shardNumber, 177 AttemptTimestamp: attemptTimestamp, 178 Project: project, 179 Progress: progress, 180 } 181 results = append(results, shard) 182 return nil 183 }) 184 if err != nil { 185 return nil, err 186 } 187 return results, nil 188 } 189 190 // Create inserts a new reclustering shard without progress. 191 func Create(ctx context.Context, r ReclusteringShard) error { 192 if err := validateShard(r); err != nil { 193 return err 194 } 195 ms := spanutil.InsertMap("ReclusteringShards", map[string]any{ 196 "ShardNumber": r.ShardNumber, 197 "Project": r.Project, 198 "AttemptTimestamp": r.AttemptTimestamp, 199 }) 200 span.BufferWrite(ctx, ms) 201 return nil 202 } 203 204 func validateShard(r ReclusteringShard) error { 205 if err := pbutil.ValidateProject(r.Project); err != nil { 206 return errors.Annotate(err, "project").Err() 207 } 208 switch { 209 case r.ShardNumber < 1: 210 return errors.New("shard number must be a positive integer") 211 case r.AttemptTimestamp.Before(StartingEpoch): 212 return errors.New("attempt timestamp must be valid") 213 } 214 return nil 215 } 216 217 // UpdateProgress updates the progress on a particular shard. 218 // Clients should be mindful of the fact that if they are late 219 // to update the progress, the entry for a shard may no longer exist. 220 func UpdateProgress(ctx context.Context, shardNumber int64, attemptTimestamp time.Time, progress int) error { 221 if progress < 0 || progress > MaxProgress { 222 return errors.Reason("progress, if set, must be a value between 0 and %v", MaxProgress).Err() 223 } 224 ms := spanutil.UpdateMap("ReclusteringShards", map[string]any{ 225 "ShardNumber": shardNumber, 226 "AttemptTimestamp": attemptTimestamp, 227 "Progress": progress, 228 }) 229 span.BufferWrite(ctx, ms) 230 return nil 231 } 232 233 // DeleteAll deletes all reclustering shards. 234 func DeleteAll(ctx context.Context) { 235 m := spanner.Delete("ReclusteringShards", spanner.AllKeys()) 236 span.BufferWrite(ctx, m) 237 }