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  }