go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/prjpb/tq.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 prjpb
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"time"
    21  
    22  	"google.golang.org/protobuf/types/known/timestamppb"
    23  
    24  	"go.chromium.org/luci/common/clock"
    25  	"go.chromium.org/luci/gae/service/datastore"
    26  	"go.chromium.org/luci/server/tq"
    27  )
    28  
    29  const (
    30  	// PMTaskInterval is target frequency of executions of ManageProjectTask.
    31  	//
    32  	// See Dispatch() for details.
    33  	PMTaskInterval = time.Second
    34  
    35  	// MaxAcceptableDelay prevents TQ tasks which arrive too late from invoking PM.
    36  	//
    37  	// MaxAcceptableDelay / PMTaskInterval effectively limits # concurrent
    38  	// invocations of PM on the same project that may happen due to task retries,
    39  	// delays, and queue throttling.
    40  	//
    41  	// Do not set too low, as this may prevent actual PM invoking from happening at
    42  	// all if the TQ is overloaded.
    43  	MaxAcceptableDelay = 60 * time.Second
    44  
    45  	ManageProjectTaskClass        = "manage-project"
    46  	KickManageProjectTaskClass    = "kick-" + ManageProjectTaskClass
    47  	PurgeProjectCLTaskClass       = "purge-project-cl"
    48  	TriggerProjectCLDepsTaskClass = "trigger-project-cl-deps"
    49  )
    50  
    51  // TasksBinding binds Project Manager tasks to a TQ Dispatcher.
    52  //
    53  // This struct exists to separate task creation and handling,
    54  // which in turns avoids circular dependency.
    55  type TasksBinding struct {
    56  	ManageProject        tq.TaskClassRef
    57  	KickManageProject    tq.TaskClassRef
    58  	PurgeProjectCL       tq.TaskClassRef
    59  	TriggerProjectCLs    tq.TaskClassRef
    60  	TriggerProjectCLDeps tq.TaskClassRef
    61  	TQDispatcher         *tq.Dispatcher
    62  }
    63  
    64  func Register(tqd *tq.Dispatcher) TasksBinding {
    65  	return TasksBinding{
    66  		TQDispatcher: tqd,
    67  
    68  		ManageProject: tqd.RegisterTaskClass(tq.TaskClass{
    69  			ID:           ManageProjectTaskClass,
    70  			Prototype:    &ManageProjectTask{},
    71  			Queue:        "manage-project",
    72  			Kind:         tq.NonTransactional,
    73  			Quiet:        true,
    74  			QuietOnError: true,
    75  		}),
    76  		KickManageProject: tqd.RegisterTaskClass(tq.TaskClass{
    77  			ID:           KickManageProjectTaskClass,
    78  			Prototype:    &KickManageProjectTask{},
    79  			Queue:        "kick-manage-project",
    80  			Kind:         tq.Transactional,
    81  			Quiet:        true,
    82  			QuietOnError: true,
    83  		}),
    84  		PurgeProjectCL: tqd.RegisterTaskClass(tq.TaskClass{
    85  			ID:           PurgeProjectCLTaskClass,
    86  			Prototype:    &PurgeCLTask{},
    87  			Queue:        "purge-project-cl",
    88  			Kind:         tq.Transactional,
    89  			Quiet:        false, // these tasks are rare enough that verbosity only helps.
    90  			QuietOnError: true,
    91  		}),
    92  		TriggerProjectCLDeps: tqd.RegisterTaskClass(tq.TaskClass{
    93  			ID:           TriggerProjectCLDepsTaskClass,
    94  			Prototype:    &TriggeringCLDepsTask{},
    95  			Queue:        "trigger-project-cl-deps",
    96  			Kind:         tq.Transactional,
    97  			Quiet:        true,
    98  			QuietOnError: true,
    99  		}),
   100  	}
   101  }
   102  
   103  // Dispatch ensures invocation of ProjectManager via ManageProjectTask.
   104  //
   105  // ProjectManager will be invoked at approximately no earlier than both:
   106  // * eta time
   107  // * next possible.
   108  //
   109  // To avoid actually dispatching TQ tasks in tests, use pmtest.MockDispatch().
   110  func (tr TasksBinding) Dispatch(ctx context.Context, luciProject string, eta time.Time) error {
   111  	mock, mocked := ctx.Value(&mockDispatcherContextKey).(func(string, time.Time))
   112  
   113  	if datastore.CurrentTransaction(ctx) != nil {
   114  		// TODO(tandrii): use txndefer to immediately trigger a ManageProjectTask after
   115  		// transaction completes to reduce latency in *most* circumstances.
   116  		// The KickManageProjectTask is still required for correctness.
   117  		payload := &KickManageProjectTask{LuciProject: luciProject}
   118  		if !eta.IsZero() {
   119  			payload.Eta = timestamppb.New(eta)
   120  		}
   121  
   122  		if mocked {
   123  			mock(luciProject, eta)
   124  			return nil
   125  		}
   126  		return tr.TQDispatcher.AddTask(ctx, &tq.Task{
   127  			Title:            luciProject,
   128  			DeduplicationKey: "", // not allowed in a transaction
   129  			Payload:          payload,
   130  		})
   131  	}
   132  
   133  	// If actual local clock is more than `clockDrift` behind, the "next" computed
   134  	// ManageProjectTask moment might be already executing, meaning task dedup
   135  	// will ensure no new task will be scheduled AND the already executing run
   136  	// might not have read the Event that was just written.
   137  	// Thus, this should be large for safety. However, large value leads to higher
   138  	// latency of event processing of non-busy ProjectManagers.
   139  	// TODO(tandrii): this can be reduced significantly once safety "ping" events
   140  	// are originated from Config import cron tasks.
   141  	const clockDrift = 100 * time.Millisecond
   142  	now := clock.Now(ctx).Add(clockDrift) // Use the worst possible time.
   143  	if eta.IsZero() || eta.Before(now) {
   144  		eta = now
   145  	}
   146  	eta = eta.Truncate(PMTaskInterval).Add(PMTaskInterval)
   147  
   148  	if mocked {
   149  		mock(luciProject, eta)
   150  		return nil
   151  	}
   152  	return tr.TQDispatcher.AddTask(ctx, &tq.Task{
   153  		Title:            luciProject,
   154  		DeduplicationKey: fmt.Sprintf("%s\n%d", luciProject, eta.UnixNano()),
   155  		ETA:              eta,
   156  		Payload:          &ManageProjectTask{LuciProject: luciProject, Eta: timestamppb.New(eta)},
   157  	})
   158  }
   159  
   160  var mockDispatcherContextKey = "prjpb.mockDispatcher"
   161  
   162  // InstallMockDispatcher is used in test to run tests emitting PM events without
   163  // actually dispatching PM tasks.
   164  //
   165  // See pmtest.MockDispatch().
   166  func InstallMockDispatcher(ctx context.Context, f func(luciProject string, eta time.Time)) context.Context {
   167  	return context.WithValue(ctx, &mockDispatcherContextKey, f)
   168  }