go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/run/eventpb/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 eventpb 16 17 import ( 18 "context" 19 "fmt" 20 "time" 21 22 "google.golang.org/protobuf/proto" 23 "google.golang.org/protobuf/types/known/timestamppb" 24 25 "go.chromium.org/luci/common/clock" 26 "go.chromium.org/luci/gae/service/datastore" 27 "go.chromium.org/luci/server/tq" 28 29 "go.chromium.org/luci/cv/internal/common" 30 ) 31 32 const ( 33 // ManageRunTaskClass is the ID of ManageRunTask Class. 34 ManageRunTaskClass = "manage-run" 35 // ManageRunLongOpTaskClass is the ID of the ManageRunLongOp Class. 36 ManageRunLongOpTaskClass = "manage-run-long-op" 37 // taskInterval is target frequency of executions of ManageRunTask. 38 // 39 // See Dispatch() for details. 40 taskInterval = time.Second 41 ) 42 43 // TasksBinding binds Run Manager tasks to a TQ Dispatcher. 44 // 45 // This struct exists to separate task creation and handling, 46 // which in turns avoids circular dependency. 47 type TasksBinding struct { 48 ManageRun tq.TaskClassRef 49 KickManage tq.TaskClassRef 50 ManageRunLongOp tq.TaskClassRef 51 TQDispatcher *tq.Dispatcher 52 } 53 54 // Register registers tasks with the given TQ Dispatcher. 55 func Register(tqd *tq.Dispatcher) TasksBinding { 56 t := TasksBinding{ 57 ManageRun: tqd.RegisterTaskClass(tq.TaskClass{ 58 ID: ManageRunTaskClass, 59 Prototype: &ManageRunTask{}, 60 Queue: "manage-run", 61 Kind: tq.NonTransactional, 62 QuietOnError: true, 63 Quiet: true, 64 }), 65 KickManage: tqd.RegisterTaskClass(tq.TaskClass{ 66 ID: fmt.Sprintf("kick-%s", ManageRunTaskClass), 67 Prototype: &KickManageRunTask{}, 68 Queue: "kick-manage-run", 69 Kind: tq.Transactional, 70 QuietOnError: true, 71 Quiet: true, 72 }), 73 ManageRunLongOp: tqd.RegisterTaskClass(tq.TaskClass{ 74 ID: ManageRunLongOpTaskClass, 75 Prototype: &ManageRunLongOpTask{}, 76 Queue: "manage-run-long-op", 77 Kind: tq.Transactional, 78 QuietOnError: true, 79 Quiet: true, 80 }), 81 TQDispatcher: tqd, 82 } 83 t.KickManage.AttachHandler(func(ctx context.Context, payload proto.Message) error { 84 task := payload.(*KickManageRunTask) 85 var eta time.Time 86 if t := task.GetEta(); t != nil { 87 eta = t.AsTime() 88 } 89 err := t.Dispatch(ctx, task.GetRunId(), eta) 90 return common.TQifyError(ctx, err) 91 }) 92 return t 93 } 94 95 // Dispatch ensures invocation of RunManager via ManageRunTask. 96 // 97 // RunManager will be invoked at approximately no earlier than both: 98 // - eta time (if given) 99 // - next possible. 100 // 101 // To avoid actually dispatching TQ tasks in tests, use runtest.MockDispatch(). 102 func (tr TasksBinding) Dispatch(ctx context.Context, runID string, eta time.Time) error { 103 mock, mocked := ctx.Value(&mockDispatcherContextKey).(func(string, time.Time)) 104 105 if datastore.CurrentTransaction(ctx) != nil { 106 payload := &KickManageRunTask{RunId: runID} 107 if !eta.IsZero() { 108 payload.Eta = timestamppb.New(eta) 109 } 110 111 if mocked { 112 mock(runID, eta) 113 return nil 114 } 115 return tr.TQDispatcher.AddTask(ctx, &tq.Task{ 116 DeduplicationKey: "", // not allowed in a transaction 117 Payload: payload, 118 }) 119 } 120 121 // If actual local clock is more than `clockDrift` behind, the "next" computed 122 // ManageRunTask moment might be already executing, meaning task dedup will 123 // ensure no new task will be scheduled AND the already executing run 124 // might not have read the Event that was just written. 125 // Thus, for safety, this should be large, however, will also leads to higher 126 // latency of event processing of non-busy RunManager. 127 // TODO(tandrii/yiwzhang): this can be reduced significantly once safety 128 // "ping" events are originated from Config import cron tasks. 129 const clockDrift = 100 * time.Millisecond 130 now := clock.Now(ctx).Add(clockDrift) // Use the worst possible time. 131 if eta.IsZero() || eta.Before(now) { 132 eta = now 133 } 134 eta = eta.Truncate(taskInterval).Add(taskInterval) 135 136 if mocked { 137 mock(runID, eta) 138 return nil 139 } 140 return tr.TQDispatcher.AddTask(ctx, &tq.Task{ 141 Title: runID, 142 DeduplicationKey: fmt.Sprintf("%s\n%d", runID, eta.UnixNano()), 143 ETA: eta, 144 Payload: &ManageRunTask{RunId: runID}, 145 }) 146 } 147 148 var mockDispatcherContextKey = "eventpb.mockDispatcher" 149 150 // InstallMockDispatcher is used in test to run tests emitting RM events without 151 // actually dispatching RM tasks. 152 // 153 // See runtest.MockDispatch(). 154 func InstallMockDispatcher(ctx context.Context, f func(runID string, eta time.Time)) context.Context { 155 return context.WithValue(ctx, &mockDispatcherContextKey, f) 156 }