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  }