go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/tasks/tasks.go (about) 1 // Copyright 2020 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 tasks contains task queue implementations. 16 package tasks 17 18 import ( 19 "context" 20 "encoding/json" 21 "fmt" 22 "time" 23 24 "google.golang.org/protobuf/encoding/protojson" 25 "google.golang.org/protobuf/proto" 26 27 "go.chromium.org/luci/common/errors" 28 "go.chromium.org/luci/common/logging" 29 "go.chromium.org/luci/server/tq" 30 31 "go.chromium.org/luci/buildbucket/appengine/internal/resultdb" 32 taskdefs "go.chromium.org/luci/buildbucket/appengine/tasks/defs" 33 pb "go.chromium.org/luci/buildbucket/proto" 34 35 // Enable datastore transactional tasks support. 36 _ "go.chromium.org/luci/server/tq/txn/datastore" 37 ) 38 39 // rejectionHandler returns a tq.Handler which rejects the given task. 40 // Used by tasks which are handled in Python. 41 // TODO(crbug/1042991): Remove once all handlers are implemented in Go. 42 func rejectionHandler(tq string) tq.Handler { 43 return func(ctx context.Context, payload proto.Message) error { 44 logging.Errorf(ctx, "tried to handle %s: %q", tq, payload) 45 return errors.Reason("handler called").Err() 46 } 47 } 48 49 func init() { 50 tq.RegisterTaskClass(tq.TaskClass{ 51 ID: "cancel-backend-task", 52 Kind: tq.FollowsContext, 53 Prototype: (*taskdefs.CancelBackendTask)(nil), 54 Queue: "backend-go-default", 55 Handler: func(ctx context.Context, payload proto.Message) error { 56 t := payload.(*taskdefs.CancelBackendTask) 57 return HandleCancelBackendTask(ctx, t.Project, t.Target, t.TaskId) 58 }, 59 }) 60 61 tq.RegisterTaskClass(tq.TaskClass{ 62 ID: "cancel-swarming-task", 63 Custom: func(ctx context.Context, m proto.Message) (*tq.CustomPayload, error) { 64 task := m.(*taskdefs.CancelSwarmingTask) 65 body, err := json.Marshal(map[string]any{ 66 "hostname": task.Hostname, 67 "task_id": task.TaskId, 68 "realm": task.Realm, 69 }) 70 if err != nil { 71 return nil, errors.Annotate(err, "error marshaling payload").Err() 72 } 73 return &tq.CustomPayload{ 74 Body: body, 75 Method: "POST", 76 RelativeURI: fmt.Sprintf("/internal/task/buildbucket/cancel_swarming_task/%s/%s", task.Hostname, task.TaskId), 77 }, nil 78 }, 79 Handler: rejectionHandler("cancel-swarming-task"), 80 Kind: tq.FollowsContext, 81 Prototype: (*taskdefs.CancelSwarmingTask)(nil), 82 Queue: "backend-default", 83 }) 84 85 tq.RegisterTaskClass(tq.TaskClass{ 86 ID: "cancel-swarming-task-go", 87 Kind: tq.FollowsContext, 88 Prototype: (*taskdefs.CancelSwarmingTaskGo)(nil), 89 Queue: "backend-go-default", 90 Handler: func(ctx context.Context, payload proto.Message) error { 91 t := payload.(*taskdefs.CancelSwarmingTaskGo) 92 return HandleCancelSwarmingTask(ctx, t.Hostname, t.TaskId, t.Realm) 93 }, 94 }) 95 96 // TODO(crbug.com/1328646): Delete it after swarming-build-create migration is done. 97 tq.RegisterTaskClass(tq.TaskClass{ 98 ID: "create-swarming-task", 99 Custom: func(ctx context.Context, m proto.Message) (*tq.CustomPayload, error) { 100 task := m.(*taskdefs.CreateSwarmingTask) 101 body, err := json.Marshal(map[string]any{ 102 "generation": 0, 103 "id": task.BuildId, 104 }) 105 if err != nil { 106 return nil, errors.Annotate(err, "error marshaling payload").Err() 107 } 108 return &tq.CustomPayload{ 109 Body: body, 110 Method: "POST", 111 RelativeURI: fmt.Sprintf("/internal/task/swarming/sync-build/%d", task.BuildId), 112 }, nil 113 }, 114 Handler: rejectionHandler("create-swarming-task"), 115 Kind: tq.Transactional, 116 Prototype: (*taskdefs.CreateSwarmingTask)(nil), 117 Queue: "swarming-build-create", 118 }) 119 120 // TODO(crbug.com/1356766): remove it after bq-exporter runs in Go. 121 tq.RegisterTaskClass(tq.TaskClass{ 122 ID: "export-bigquery", 123 Custom: func(ctx context.Context, m proto.Message) (*tq.CustomPayload, error) { 124 task := m.(*taskdefs.ExportBigQuery) 125 body, err := json.Marshal(map[string]any{ 126 "id": task.BuildId, 127 }) 128 if err != nil { 129 return nil, errors.Annotate(err, "error marshaling payload").Err() 130 } 131 return &tq.CustomPayload{ 132 Body: body, 133 Method: "POST", 134 RelativeURI: fmt.Sprintf("/internal/task/bq/export/%d", task.BuildId), 135 }, nil 136 }, 137 Handler: rejectionHandler("export-bigquery"), 138 Kind: tq.Transactional, 139 Prototype: (*taskdefs.ExportBigQuery)(nil), 140 Queue: "backend-default", 141 }) 142 143 tq.RegisterTaskClass(tq.TaskClass{ 144 ID: "finalize-resultdb", 145 Custom: func(ctx context.Context, m proto.Message) (*tq.CustomPayload, error) { 146 task := m.(*taskdefs.FinalizeResultDB) 147 body, err := json.Marshal(map[string]any{ 148 "id": task.BuildId, 149 }) 150 if err != nil { 151 return nil, errors.Annotate(err, "error marshaling payload").Err() 152 } 153 return &tq.CustomPayload{ 154 Body: body, 155 Method: "POST", 156 RelativeURI: fmt.Sprintf("/internal/task/resultdb/finalize/%d", task.BuildId), 157 }, nil 158 }, 159 Handler: rejectionHandler("finalize-resultdb"), 160 Kind: tq.Transactional, 161 Prototype: (*taskdefs.FinalizeResultDB)(nil), 162 Queue: "backend-default", 163 }) 164 165 tq.RegisterTaskClass(tq.TaskClass{ 166 ID: "finalize-resultdb-go", 167 Kind: tq.Transactional, 168 Prototype: (*taskdefs.FinalizeResultDBGo)(nil), 169 Queue: "backend-go-default", 170 Handler: func(ctx context.Context, payload proto.Message) error { 171 t := payload.(*taskdefs.FinalizeResultDBGo) 172 return resultdb.FinalizeInvocation(ctx, t.BuildId) 173 }, 174 }) 175 176 tq.RegisterTaskClass(tq.TaskClass{ 177 ID: "notify-pubsub", 178 Custom: func(ctx context.Context, m proto.Message) (*tq.CustomPayload, error) { 179 task := m.(*taskdefs.NotifyPubSub) 180 mode := "global" 181 if task.Callback { 182 mode = "callback" 183 } 184 body, err := json.Marshal(map[string]any{ 185 "id": task.BuildId, 186 "mode": mode, 187 }) 188 if err != nil { 189 return nil, errors.Annotate(err, "error marshaling payload").Err() 190 } 191 return &tq.CustomPayload{ 192 Body: body, 193 Method: "POST", 194 RelativeURI: fmt.Sprintf("/internal/task/buildbucket/notify/%d", task.BuildId), 195 }, nil 196 }, 197 Handler: rejectionHandler("notify-pubsub"), 198 Kind: tq.Transactional, 199 Prototype: (*taskdefs.NotifyPubSub)(nil), 200 Queue: "backend-default", 201 }) 202 203 tq.RegisterTaskClass(tq.TaskClass{ 204 ID: "notify-pubsub-go", 205 Kind: tq.FollowsContext, 206 Prototype: (*taskdefs.NotifyPubSubGo)(nil), 207 Queue: "backend-go-default", 208 Handler: func(ctx context.Context, payload proto.Message) error { 209 t := payload.(*taskdefs.NotifyPubSubGo) 210 return PublishBuildsV2Notification(ctx, t.BuildId, t.Topic, t.Callback) 211 }, 212 }) 213 214 tq.RegisterTaskClass(tq.TaskClass{ 215 ID: "notify-pubsub-go-proxy", 216 Kind: tq.Transactional, 217 Prototype: (*taskdefs.NotifyPubSubGoProxy)(nil), 218 Queue: "backend-go-default", 219 Handler: func(ctx context.Context, payload proto.Message) error { 220 t := payload.(*taskdefs.NotifyPubSubGoProxy) 221 return EnqueueNotifyPubSubGo(ctx, t.BuildId, t.Project) 222 }, 223 }) 224 225 tq.RegisterTaskClass(tq.TaskClass{ 226 ID: "builds_v2", 227 Kind: tq.NonTransactional, 228 Prototype: (*pb.BuildsV2PubSub)(nil), 229 Topic: "builds_v2", 230 Custom: func(ctx context.Context, m proto.Message) (*tq.CustomPayload, error) { 231 t := m.(*pb.BuildsV2PubSub) 232 blob, err := (protojson.MarshalOptions{Indent: "\t"}).Marshal(m) 233 if err != nil { 234 logging.Errorf(ctx, "failed to marshal builds_v2 pubsub message body - %s", err) 235 return nil, err 236 } 237 return &tq.CustomPayload{ 238 Body: blob, 239 Meta: generateBuildsV2Attributes(t.Build), 240 }, nil 241 }, 242 }) 243 244 tq.RegisterTaskClass(tq.TaskClass{ 245 ID: "cancel-build", 246 Kind: tq.Transactional, 247 Prototype: (*taskdefs.CancelBuildTask)(nil), 248 Queue: "backend-go-default", 249 Handler: func(ctx context.Context, payload proto.Message) error { 250 t := payload.(*taskdefs.CancelBuildTask) 251 _, err := Cancel(ctx, t.BuildId) 252 return err 253 }, 254 }) 255 256 tq.RegisterTaskClass(tq.TaskClass{ 257 ID: "create-swarming-task-go", 258 Kind: tq.Transactional, 259 Prototype: (*taskdefs.CreateSwarmingBuildTask)(nil), 260 Queue: "swarming-build-create-go", 261 Handler: func(ctx context.Context, payload proto.Message) error { 262 t := payload.(*taskdefs.CreateSwarmingBuildTask) 263 return SyncBuild(ctx, t.GetBuildId(), 0) 264 }, 265 }) 266 267 tq.RegisterTaskClass(tq.TaskClass{ 268 ID: "create-backend-task-go", 269 Kind: tq.Transactional, 270 Prototype: (*taskdefs.CreateBackendBuildTask)(nil), 271 Queue: "backend-go-default", 272 Handler: func(ctx context.Context, payload proto.Message) error { 273 t := payload.(*taskdefs.CreateBackendBuildTask) 274 return CreateBackendTask(ctx, t.GetBuildId(), t.GetRequestId()) 275 }, 276 }) 277 278 tq.RegisterTaskClass(tq.TaskClass{ 279 ID: "sync-swarming-task-go", 280 Kind: tq.NonTransactional, 281 Prototype: (*taskdefs.SyncSwarmingBuildTask)(nil), 282 Queue: "swarming-build-sync-go", 283 Handler: func(ctx context.Context, payload proto.Message) error { 284 t := payload.(*taskdefs.SyncSwarmingBuildTask) 285 return SyncBuild(ctx, t.GetBuildId(), t.GetGeneration()) 286 }, 287 }) 288 289 tq.RegisterTaskClass(tq.TaskClass{ 290 ID: "export-bigquery-go", 291 Kind: tq.Transactional, 292 Prototype: (*taskdefs.ExportBigQueryGo)(nil), 293 Queue: "backend-go-default", 294 Handler: func(ctx context.Context, payload proto.Message) error { 295 t := payload.(*taskdefs.ExportBigQueryGo) 296 return ExportBuild(ctx, t.BuildId) 297 }, 298 }) 299 300 tq.RegisterTaskClass(tq.TaskClass{ 301 ID: "sync-builds-with-backend-tasks", 302 Kind: tq.NonTransactional, 303 Prototype: (*taskdefs.SyncBuildsWithBackendTasks)(nil), 304 Queue: "backend-go-default", 305 Handler: func(ctx context.Context, payload proto.Message) error { 306 t := payload.(*taskdefs.SyncBuildsWithBackendTasks) 307 return SyncBuildsWithBackendTasks(ctx, t.Backend, t.Project) 308 }, 309 }) 310 311 tq.RegisterTaskClass(tq.TaskClass{ 312 ID: "check-build-liveness", 313 Kind: tq.FollowsContext, 314 Prototype: (*taskdefs.CheckBuildLiveness)(nil), 315 Queue: "backend-go-default", 316 Handler: func(ctx context.Context, payload proto.Message) error { 317 t := payload.(*taskdefs.CheckBuildLiveness) 318 return CheckLiveness(ctx, t.BuildId, t.HeartbeatTimeout) 319 }, 320 }) 321 } 322 323 // CancelBackendTask enqueues a task queue task to cancel the given Backend 324 // task. 325 func CancelBackendTask(ctx context.Context, task *taskdefs.CancelBackendTask) error { 326 switch { 327 case task.Project == "": 328 return errors.Reason("project is required").Err() 329 case task.TaskId == "": 330 return errors.Reason("task_id is required").Err() 331 case task.Target == "": 332 return errors.Reason("task target is required").Err() 333 } 334 return tq.AddTask(ctx, &tq.Task{ 335 Payload: task, 336 }) 337 } 338 339 // CancelSwarmingTask enqueues a task queue task to cancel the given Swarming 340 // task. 341 func CancelSwarmingTask(ctx context.Context, task *taskdefs.CancelSwarmingTaskGo) error { 342 switch { 343 case task.GetHostname() == "": 344 return errors.Reason("hostname is required").Err() 345 case task.TaskId == "": 346 return errors.Reason("task_id is required").Err() 347 } 348 return tq.AddTask(ctx, &tq.Task{ 349 Payload: task, 350 }) 351 } 352 353 // CreateSwarmingTask enqueues a task queue task to create a Swarming task from 354 // the given build. 355 func CreateSwarmingTask(ctx context.Context, task *taskdefs.CreateSwarmingTask) error { 356 if task.GetBuildId() == 0 { 357 return errors.Reason("build_id is required").Err() 358 } 359 return tq.AddTask(ctx, &tq.Task{ 360 Payload: task, 361 }) 362 } 363 364 // CreateSwarmingBuildTask enqueues a Cloud Tasks task to create a Swarming task 365 // from the given build. 366 func CreateSwarmingBuildTask(ctx context.Context, task *taskdefs.CreateSwarmingBuildTask) error { 367 if task.GetBuildId() == 0 { 368 return errors.Reason("build_id is required").Err() 369 } 370 return tq.AddTask(ctx, &tq.Task{ 371 Title: fmt.Sprintf("create-swarming-task-%d", task.BuildId), 372 Payload: task, 373 }) 374 } 375 376 // CreateBackendBuildTask enqueues a Cloud Tasks task to create a backend task 377 // from the given build. 378 func CreateBackendBuildTask(ctx context.Context, task *taskdefs.CreateBackendBuildTask) error { 379 if task.GetBuildId() == 0 { 380 return errors.Reason("build_id is required").Err() 381 } 382 return tq.AddTask(ctx, &tq.Task{ 383 Title: fmt.Sprintf("create-backend-task-%d", task.BuildId), 384 Payload: task, 385 }) 386 } 387 388 // SyncSwarmingBuildTask enqueues a Cloud Tasks task to sync the Swarming task 389 // with the given build. 390 func SyncSwarmingBuildTask(ctx context.Context, task *taskdefs.SyncSwarmingBuildTask, delay time.Duration) error { 391 switch { 392 case task.GetBuildId() == 0: 393 return errors.Reason("build_id is required").Err() 394 case task.GetGeneration() == 0: 395 return errors.Reason("generation should be larger than 0").Err() 396 } 397 return tq.AddTask(ctx, &tq.Task{ 398 Title: fmt.Sprintf("sync-swarming-task-%d", task.BuildId), 399 Payload: task, 400 Delay: delay, 401 DeduplicationKey: fmt.Sprintf("%d-%d", task.BuildId, task.Generation), 402 }) 403 } 404 405 // ExportBigQuery enqueues a task queue task to export the given build to Bq. 406 // TODO(crbug.com/1356766): routeToGo will be removed once migration is done. 407 func ExportBigQuery(ctx context.Context, buildID int64, routeToGo bool) error { 408 if buildID <= 0 { 409 return errors.Reason("build_id is invalid").Err() 410 } 411 if routeToGo { 412 return tq.AddTask(ctx, &tq.Task{ 413 Payload: &taskdefs.ExportBigQueryGo{BuildId: buildID}, 414 }) 415 } 416 return tq.AddTask(ctx, &tq.Task{ 417 Payload: &taskdefs.ExportBigQuery{BuildId: buildID}, 418 }) 419 } 420 421 // FinalizeResultDB enqueues a task queue task to finalize the invocation for 422 // the given build in ResultDB. 423 func FinalizeResultDB(ctx context.Context, task *taskdefs.FinalizeResultDBGo) error { 424 if task.GetBuildId() == 0 { 425 return errors.Reason("build_id is required").Err() 426 } 427 return tq.AddTask(ctx, &tq.Task{ 428 Payload: task, 429 }) 430 } 431 432 // SyncWithBackend enqueues a task queue task to sync builds in one project 433 // running on a backend. 434 func SyncWithBackend(ctx context.Context, backend, project string) error { 435 switch { 436 case backend == "": 437 return errors.Reason("backend is required").Err() 438 case project == "": 439 return errors.Reason("project is required").Err() 440 } 441 return tq.AddTask(ctx, &tq.Task{ 442 Payload: &taskdefs.SyncBuildsWithBackendTasks{ 443 Backend: backend, 444 Project: project, 445 }, 446 }) 447 } 448 449 // CheckBuildLiveness enqueues a task queue task to check a build's liveness. 450 func CheckBuildLiveness(ctx context.Context, buildID int64, heartbeatTimeout uint32, delay time.Duration) error { 451 if buildID <= 0 { 452 return errors.Reason("build_id is invalid").Err() 453 } 454 return tq.AddTask(ctx, &tq.Task{ 455 Payload: &taskdefs.CheckBuildLiveness{ 456 BuildId: buildID, 457 HeartbeatTimeout: heartbeatTimeout, 458 }, 459 Delay: delay, 460 }) 461 }