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  }