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 }