github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/engine/framework/internal/eventloop/runner.go (about) 1 // Copyright 2022 PingCAP, Inc. 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 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package eventloop 15 16 import ( 17 "context" 18 "fmt" 19 "time" 20 21 "github.com/pingcap/log" 22 frameErrors "github.com/pingcap/tiflow/engine/framework/internal/errors" 23 "github.com/pingcap/tiflow/engine/pkg/clock" 24 "github.com/pingcap/tiflow/pkg/errors" 25 "go.uber.org/atomic" 26 "go.uber.org/zap" 27 ) 28 29 const ( 30 defaultEventLoopTickInterval = 50 * time.Millisecond 31 ) 32 33 // Runner runs a Task. 34 type Runner[T Task] struct { 35 task T 36 37 alreadyRun atomic.Bool 38 39 clk clock.Clock 40 } 41 42 // NewRunner returns a new Runner. 43 func NewRunner[T Task](t T) *Runner[T] { 44 return &Runner[T]{ 45 task: t, 46 clk: clock.New(), 47 } 48 } 49 50 // Run is called by the runtime. Cancelling the 51 // ctx cancels the task immediately and forcefully. 52 func (r *Runner[R]) Run(ctx context.Context) error { 53 if r.alreadyRun.Swap(true) { 54 panic(fmt.Sprintf("duplicate calls to Run: %s", r.task.ID())) 55 } 56 57 err := r.doRun(ctx) 58 if err == nil { 59 panic(fmt.Sprintf("unexpected exiting with nil error: %s", r.task.ID())) 60 } 61 62 if !isForcefulExitError(err) { 63 // Exit gracefully. 64 r.doGracefulExit(ctx, err) 65 } 66 67 if IsTerminatedError(err) { 68 if stopErr := r.task.Stop(context.Background()); stopErr != nil { 69 log.Warn("Stop task returned error", 70 zap.String("label", r.task.ID()), zap.Error(stopErr)) 71 } 72 } else { 73 if closeErr := r.task.Close(context.Background()); closeErr != nil { 74 log.Warn("Closing task returned error", zap.String("label", r.task.ID())) 75 } 76 } 77 78 return err 79 } 80 81 func (r *Runner[R]) doRun(ctx context.Context) error { 82 if err := r.task.Init(ctx); err != nil { 83 return errors.Trace(err) 84 } 85 86 ticker := r.clk.Ticker(defaultEventLoopTickInterval) 87 defer ticker.Stop() 88 89 for { 90 select { 91 case <-ctx.Done(): 92 return errors.Trace(ctx.Err()) 93 case <-ticker.C: 94 if err := r.task.Poll(ctx); err != nil { 95 return errors.Trace(err) 96 } 97 } 98 } 99 } 100 101 func (r *Runner[R]) doGracefulExit(ctx context.Context, errIn error) { 102 timeoutCtx, cancel := context.WithTimeout(ctx, 30*time.Second) 103 defer cancel() 104 105 err := r.task.NotifyExit(timeoutCtx, errIn) 106 if err != nil && !errors.Is(err, context.Canceled) { 107 log.Error("an error is encountered when a task is already exiting", 108 zap.Error(err), zap.NamedError("original-err", errIn)) 109 } 110 } 111 112 func isForcefulExitError(errIn error) bool { 113 if errors.Is(errIn, context.Canceled) { 114 // Cancellation should result in a forceful exit. 115 return true 116 } 117 118 if frameErrors.IsFailFastError(errIn) { 119 return true 120 } 121 122 // Suicides should result in a forceful exit. 123 return errors.Is(errIn, errors.ErrWorkerSuicide) 124 } 125 126 // IsTerminatedError checks whether task enters a terminated state, which include 127 // finished, canceled 128 func IsTerminatedError(errIn error) bool { 129 return errors.Is(errIn, errors.ErrWorkerCancel) || 130 errors.Is(errIn, errors.ErrWorkerFinish) || 131 errors.Is(errIn, errors.ErrWorkerFailed) 132 }