go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/services/bugupdater/tasks.go (about) 1 // Copyright 2023 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 bugupdater defines a task for updating a project's LUCI 16 // Analysis bugs in response to the latest cluster metrics. Bugs 17 // are auto-opened, auto-verified and have their priorities adjusted 18 // according to the LUCI project's bug management policies. 19 package bugupdater 20 21 import ( 22 "context" 23 "fmt" 24 "time" 25 26 "google.golang.org/protobuf/proto" 27 28 "go.chromium.org/luci/common/clock" 29 "go.chromium.org/luci/common/errors" 30 "go.chromium.org/luci/common/logging" 31 "go.chromium.org/luci/server" 32 "go.chromium.org/luci/server/tq" 33 34 "go.chromium.org/luci/analysis/internal/analysis" 35 "go.chromium.org/luci/analysis/internal/bugs/buganizer" 36 "go.chromium.org/luci/analysis/internal/bugs/monorail" 37 "go.chromium.org/luci/analysis/internal/bugs/updater" 38 "go.chromium.org/luci/analysis/internal/clustering/runs" 39 "go.chromium.org/luci/analysis/internal/config" 40 "go.chromium.org/luci/analysis/internal/tasks/taskspb" 41 "go.chromium.org/luci/analysis/pbutil" 42 ) 43 44 const ( 45 taskClassID = "update-bugs" 46 queueName = "update-bugs" 47 ) 48 49 var bugUpdater = tq.RegisterTaskClass(tq.TaskClass{ 50 ID: taskClassID, 51 Prototype: &taskspb.UpdateBugs{}, 52 Queue: queueName, 53 Kind: tq.NonTransactional, 54 }) 55 56 // RegisterTaskHandler registers the handler for bug update tasks. 57 func RegisterTaskHandler(srv *server.Server) error { 58 h := &Handler{ 59 GCPProject: srv.Options.CloudProject, 60 Simulate: false, 61 } 62 63 handler := func(ctx context.Context, payload proto.Message) error { 64 task := payload.(*taskspb.UpdateBugs) 65 err := h.UpdateBugs(ctx, task) 66 if err != nil { 67 // Tasks should never be retried as it may cause 68 // us to exceed our bug-filing quota. 69 // 70 // No retries should be configured at the task queue level 71 // as this is the only solution that will ensure no retries 72 // even in the case of unexpected GAE instance shutdown. 73 // 74 // Nonetheless, as a precaution we also return a fatal error here. 75 return tq.Fatal.Apply(err) 76 } 77 return nil 78 } 79 bugUpdater.AttachHandler(handler) 80 return nil 81 } 82 83 // Schedule enqueues a task to update bugs for the given LUCI Project. 84 func Schedule(ctx context.Context, task *taskspb.UpdateBugs) error { 85 if err := validateTask(task); err != nil { 86 return errors.Annotate(err, "validate task").Err() 87 } 88 89 title := fmt.Sprintf("update-bugs-%s-%v", task.Project, task.ReclusteringAttemptMinute.AsTime().Format("20060102-150405")) 90 91 taskProto := &tq.Task{ 92 Title: title, 93 // Copy the task to avoid the caller retaining an alias to 94 // the task proto passed to tq.AddTask. 95 Payload: proto.Clone(task).(*taskspb.UpdateBugs), 96 } 97 98 return tq.AddTask(ctx, taskProto) 99 } 100 101 // Handler provide a method to handle bug update tasks. 102 type Handler struct { 103 // GCPProject is the GCP Cloud Project Name, e.g. luci-analysis. 104 GCPProject string 105 // Whether bug updates should be simulated. Only used in local 106 // development when UpdateBug(...) is called directly from the 107 // cron handler. 108 Simulate bool 109 } 110 111 // UpdateBugs handles the given bug update task. 112 func (h Handler) UpdateBugs(ctx context.Context, task *taskspb.UpdateBugs) (retErr error) { 113 if err := validateTask(task); err != nil { 114 return errors.Annotate(err, "validate task").Err() 115 } 116 117 defer func() { 118 status := "failure" 119 if retErr == nil { 120 status = "success" 121 } 122 statusGauge.Set(ctx, status, task.Project) 123 runsCounter.Add(ctx, 1, task.Project, status) 124 }() 125 126 ctx, cancel := context.WithDeadline(ctx, task.Deadline.AsTime()) 127 defer cancel() 128 if err := ctx.Err(); err != nil { 129 return errors.Annotate(err, "check deadline before start").Err() 130 } 131 132 cfg, err := config.Get(ctx) 133 if err != nil { 134 return errors.Annotate(err, "get config").Err() 135 } 136 137 monorailClient, err := monorail.NewClient(ctx, cfg.MonorailHostname) 138 if err != nil { 139 return err 140 } 141 142 buganizerClient, err := createBuganizerClient(ctx) 143 if err != nil { 144 return errors.Annotate(err, "creating a buganizer client").Err() 145 } 146 if buganizerClient != nil { 147 defer buganizerClient.Close() 148 } 149 150 analysisClient, err := analysis.NewClient(ctx, h.GCPProject) 151 if err != nil { 152 return err 153 } 154 defer func() { 155 if err := analysisClient.Close(); err != nil && retErr == nil { 156 retErr = errors.Annotate(err, "closing analysis client").Err() 157 } 158 }() 159 160 progress, err := runs.ReadReclusteringProgressUpTo(ctx, task.Project, task.ReclusteringAttemptMinute.AsTime()) 161 if err != nil { 162 return errors.Annotate(err, "read re-clustering progress").Err() 163 } 164 165 uiBaseURL := fmt.Sprintf("https://%s.appspot.com", h.GCPProject) 166 runTimestamp := clock.Now(ctx).Truncate(time.Minute) 167 168 opts := updater.UpdateOptions{ 169 UIBaseURL: uiBaseURL, 170 Project: task.Project, 171 AnalysisClient: analysisClient, 172 MonorailClient: monorailClient, 173 BuganizerClient: buganizerClient, 174 SimulateBugUpdates: h.Simulate, 175 MaxBugsFiledPerRun: 1, 176 UpdateRuleBatchSize: 1000, 177 ReclusteringProgress: progress, 178 RunTimestamp: runTimestamp, 179 } 180 181 start := time.Now() 182 err = updater.UpdateBugsForProject(ctx, opts) 183 if err != nil { 184 err = errors.Annotate(err, "in project %v", task.Project).Err() 185 logging.Errorf(ctx, "Updating analysis and bugs: %s", err) 186 } 187 188 elapsed := time.Since(start) 189 durationGauge.Set(ctx, elapsed.Seconds(), task.Project) 190 191 return err 192 } 193 194 func validateTask(task *taskspb.UpdateBugs) error { 195 if err := pbutil.ValidateProject(task.Project); err != nil { 196 return errors.Annotate(err, "project").Err() 197 } 198 if err := task.ReclusteringAttemptMinute.CheckValid(); err != nil { 199 return errors.New("reclustering_attempt_minute: missing or invalid timestamp") 200 } 201 if err := task.Deadline.CheckValid(); err != nil { 202 return errors.New("deadline: missing or invalid timestamp") 203 } 204 attemptMinute := task.ReclusteringAttemptMinute.AsTime() 205 if attemptMinute.Truncate(time.Minute) != attemptMinute { 206 return errors.New("reclustering_attempt_minute: must be aligned to the start of a minute") 207 } 208 return nil 209 } 210 211 func createBuganizerClient(ctx context.Context) (buganizer.Client, error) { 212 buganizerClientMode := ctx.Value(&buganizer.BuganizerClientModeKey) 213 var buganizerClient buganizer.Client 214 var err error 215 if buganizerClientMode != nil { 216 switch buganizerClientMode { 217 case buganizer.ModeProvided: 218 // TODO (b/263906102) 219 buganizerClient, err = buganizer.NewRPCClient(ctx) 220 if err != nil { 221 return nil, errors.Annotate(err, "create new buganizer client").Err() 222 } 223 case buganizer.ModeDisable: 224 break 225 default: 226 return nil, errors.New("Unrecognized buganizer-mode value used.") 227 } 228 } 229 230 return buganizerClient, err 231 }