From 54d269b13eb960c5e72e933751f85b2206eae60c Mon Sep 17 00:00:00 2001 From: Bo Liu Date: Tue, 7 Jan 2025 17:01:55 +0800 Subject: [PATCH] feat(instance): move to task v3 (#6022) Signed-off-by: liubo02 --- cmd/operator/main.go | 2 +- pkg/controllers/common/cond.go | 12 + pkg/controllers/common/interfaces.go | 11 +- pkg/controllers/common/task.go | 15 ++ pkg/controllers/common/task_finalizer.go | 14 ++ pkg/controllers/common/task_status.go | 52 ++++ pkg/controllers/pd/builder.go | 4 +- pkg/controllers/pd/tasks/state.go | 6 + pkg/controllers/pd/tasks/status.go | 42 ---- pkg/controllers/tidb/builder.go | 45 ++-- pkg/controllers/tidb/controller.go | 27 +- pkg/controllers/tidb/tasks/cm.go | 73 +++--- pkg/controllers/tidb/tasks/cm_test.go | 238 ------------------ pkg/controllers/tidb/tasks/ctx.go | 127 ++-------- pkg/controllers/tidb/tasks/finalizer.go | 22 +- pkg/controllers/tidb/tasks/pod.go | 93 +++---- pkg/controllers/tidb/tasks/pvc.go | 42 +--- pkg/controllers/tidb/tasks/server_labels.go | 101 ++++---- pkg/controllers/tidb/tasks/state.go | 98 ++++++++ pkg/controllers/tidb/tasks/status.go | 169 +++++-------- pkg/controllers/tiflash/builder.go | 47 ++-- pkg/controllers/tiflash/controller.go | 10 +- pkg/controllers/tiflash/tasks/cm.go | 93 +++---- pkg/controllers/tiflash/tasks/ctx.go | 116 +-------- pkg/controllers/tiflash/tasks/finalizer.go | 35 +-- pkg/controllers/tiflash/tasks/pod.go | 103 +++----- pkg/controllers/tiflash/tasks/pvc.go | 45 ++-- pkg/controllers/tiflash/tasks/state.go | 98 ++++++++ pkg/controllers/tiflash/tasks/status.go | 198 ++++++--------- pkg/controllers/tiflash/tasks/store_labels.go | 98 ++++---- pkg/controllers/tikv/builder.go | 50 ++-- pkg/controllers/tikv/controller.go | 11 +- pkg/controllers/tikv/tasks/cm.go | 69 ++--- pkg/controllers/tikv/tasks/ctx.go | 119 +-------- pkg/controllers/tikv/tasks/evict_leader.go | 57 ++--- pkg/controllers/tikv/tasks/finalizer.go | 40 ++- pkg/controllers/tikv/tasks/pod.go | 134 +++++----- pkg/controllers/tikv/tasks/pvc.go | 42 +--- pkg/controllers/tikv/tasks/state.go | 98 ++++++++ pkg/controllers/tikv/tasks/status.go | 213 ++++++---------- pkg/controllers/tikv/tasks/store_labels.go | 101 ++++---- pkg/utils/task/v2/result.go | 202 --------------- pkg/utils/task/v2/runner.go | 69 ----- pkg/utils/task/v2/task.go | 233 ----------------- 44 files changed, 1207 insertions(+), 2267 deletions(-) delete mode 100644 pkg/controllers/tidb/tasks/cm_test.go create mode 100644 pkg/controllers/tidb/tasks/state.go create mode 100644 pkg/controllers/tiflash/tasks/state.go create mode 100644 pkg/controllers/tikv/tasks/state.go delete mode 100644 pkg/utils/task/v2/result.go delete mode 100644 pkg/utils/task/v2/runner.go delete mode 100644 pkg/utils/task/v2/task.go diff --git a/cmd/operator/main.go b/cmd/operator/main.go index 78b7aed18c..c6c4dcfee9 100644 --- a/cmd/operator/main.go +++ b/cmd/operator/main.go @@ -221,7 +221,7 @@ func setupControllers(mgr ctrl.Manager, c client.Client, pdcm pdm.PDClientManage if err := tidbgroup.Setup(mgr, c); err != nil { return fmt.Errorf("unable to create controller TiDBGroup: %w", err) } - if err := tidb.Setup(mgr, c, vm); err != nil { + if err := tidb.Setup(mgr, c, pdcm, vm); err != nil { return fmt.Errorf("unable to create controller TiDB: %w", err) } if err := tikvgroup.Setup(mgr, c); err != nil { diff --git a/pkg/controllers/common/cond.go b/pkg/controllers/common/cond.go index e28f797864..ef8faa8f98 100644 --- a/pkg/controllers/common/cond.go +++ b/pkg/controllers/common/cond.go @@ -54,3 +54,15 @@ func CondGroupHasBeenDeleted[RG runtime.GroupT[G], G runtime.GroupSet](state Gro return state.Group() == nil }) } + +func CondInstanceIsDeleting[I runtime.Instance](state InstanceState[I]) task.Condition { + return task.CondFunc(func() bool { + return !state.Instance().GetDeletionTimestamp().IsZero() + }) +} + +func CondInstanceHasBeenDeleted[RI runtime.InstanceT[I], I runtime.InstanceSet](state InstanceState[RI]) task.Condition { + return task.CondFunc(func() bool { + return state.Instance() == nil + }) +} diff --git a/pkg/controllers/common/interfaces.go b/pkg/controllers/common/interfaces.go index 822dc43736..d2ac73d082 100644 --- a/pkg/controllers/common/interfaces.go +++ b/pkg/controllers/common/interfaces.go @@ -71,6 +71,15 @@ type GroupState[G runtime.Group] interface { Group() G } +type InstanceState[I runtime.Instance] interface { + Instance() I +} + +type InstanceAndPodState[I runtime.Instance] interface { + InstanceState[I] + PodState +} + type InstanceSliceState[I runtime.Instance] interface { Slice() []I } @@ -180,7 +189,7 @@ type ( TiFlashGroup() *v1alpha1.TiFlashGroup } TiFlashStateInitializer interface { - TiFlashInitializer() TiFlashGroupInitializer + TiFlashInitializer() TiFlashInitializer } TiFlashState interface { TiFlash() *v1alpha1.TiFlash diff --git a/pkg/controllers/common/task.go b/pkg/controllers/common/task.go index 634e68df4a..cc066021d0 100644 --- a/pkg/controllers/common/task.go +++ b/pkg/controllers/common/task.go @@ -98,6 +98,21 @@ func TaskContextPD(state PDStateInitializer, c client.Client) task.Task { return taskContextResource("PD", w, c, false) } +func TaskContextTiKV(state TiKVStateInitializer, c client.Client) task.Task { + w := state.TiKVInitializer() + return taskContextResource("TiKV", w, c, false) +} + +func TaskContextTiDB(state TiDBStateInitializer, c client.Client) task.Task { + w := state.TiDBInitializer() + return taskContextResource("TiDB", w, c, false) +} + +func TaskContextTiFlash(state TiFlashStateInitializer, c client.Client) task.Task { + w := state.TiFlashInitializer() + return taskContextResource("TiFlash", w, c, false) +} + func TaskContextCluster(state ClusterStateInitializer, c client.Client) task.Task { w := state.ClusterInitializer() return taskContextResource("Cluster", w, c, true) diff --git a/pkg/controllers/common/task_finalizer.go b/pkg/controllers/common/task_finalizer.go index 2ea8bb5441..281d6423f0 100644 --- a/pkg/controllers/common/task_finalizer.go +++ b/pkg/controllers/common/task_finalizer.go @@ -43,6 +43,20 @@ func TaskGroupFinalizerAdd[ }) } +func TaskInstanceFinalizerAdd[ + IT runtime.InstanceTuple[OI, RI], + OI client.Object, + RI runtime.Instance, +](state InstanceState[RI], c client.Client) task.Task { + return task.NameTaskFunc("FinalizerAdd", func(ctx context.Context) task.Result { + var t IT + if err := k8s.EnsureFinalizer(ctx, c, t.To(state.Instance())); err != nil { + return task.Fail().With("failed to ensure finalizer has been added: %v", err) + } + return task.Complete().With("finalizer is added") + }) +} + const defaultDelWaitTime = 10 * time.Second func TaskGroupFinalizerDel[ diff --git a/pkg/controllers/common/task_status.go b/pkg/controllers/common/task_status.go index 473434352d..7077adb1a3 100644 --- a/pkg/controllers/common/task_status.go +++ b/pkg/controllers/common/task_status.go @@ -26,6 +26,58 @@ import ( "github.com/pingcap/tidb-operator/pkg/utils/task/v3" ) +func TaskInstanceStatusSuspend[ + IT runtime.InstanceTuple[OI, RI], + OI client.Object, + RI runtime.Instance, +](state InstanceAndPodState[RI], c client.Client) task.Task { + var it IT + return task.NameTaskFunc("StatusSuspend", func(ctx context.Context) task.Result { + instance := state.Instance() + var ( + suspendStatus = metav1.ConditionFalse + suspendReason = v1alpha1.ReasonSuspending + suspendMessage = "instance is suspending" + + // when suspending, the health status should be false + healthStatus = metav1.ConditionFalse + healthReason = "Suspend" + healthMessage = "instance is suspending or suspended, mark it as unhealthy" + ) + + if state.Pod() == nil { + suspendReason = v1alpha1.ReasonSuspended + suspendStatus = metav1.ConditionTrue + suspendMessage = "instance is suspended" + } + + needUpdate := SetStatusObservedGeneration(instance) + needUpdate = SetStatusCondition(instance, &metav1.Condition{ + Type: v1alpha1.CondSuspended, + Status: suspendStatus, + ObservedGeneration: instance.GetGeneration(), + Reason: suspendReason, + Message: suspendMessage, + }) || needUpdate + + needUpdate = SetStatusCondition(instance, &metav1.Condition{ + Type: v1alpha1.CondHealth, + Status: healthStatus, + ObservedGeneration: instance.GetGeneration(), + Reason: healthReason, + Message: healthMessage, + }) || needUpdate + + if needUpdate { + if err := c.Status().Update(ctx, it.To(instance)); err != nil { + return task.Fail().With("cannot update status: %v", err) + } + } + + return task.Complete().With("status is updated") + }) +} + func TaskGroupStatusSuspend[ GT runtime.GroupTuple[OG, RG], OG client.Object, diff --git a/pkg/controllers/pd/builder.go b/pkg/controllers/pd/builder.go index 0e9e983eb6..ddb4dfd28b 100644 --- a/pkg/controllers/pd/builder.go +++ b/pkg/controllers/pd/builder.go @@ -17,6 +17,7 @@ package pd import ( "github.com/pingcap/tidb-operator/pkg/controllers/common" "github.com/pingcap/tidb-operator/pkg/controllers/pd/tasks" + "github.com/pingcap/tidb-operator/pkg/runtime" "github.com/pingcap/tidb-operator/pkg/utils/task/v3" ) @@ -45,8 +46,7 @@ func (r *Reconciler) NewRunner(state *tasks.ReconcileContext, reporter task.Task task.IfBreak( common.CondClusterIsSuspending(state), common.TaskSuspendPod(state, r.Client), - // TODO: extract as a common task - tasks.TaskStatusSuspend(state, r.Client), + common.TaskInstanceStatusSuspend[runtime.PDTuple](state, r.Client), ), common.TaskContextPDSlice(state, r.Client), diff --git a/pkg/controllers/pd/tasks/state.go b/pkg/controllers/pd/tasks/state.go index 0f88f11ff2..f71185d1db 100644 --- a/pkg/controllers/pd/tasks/state.go +++ b/pkg/controllers/pd/tasks/state.go @@ -20,6 +20,7 @@ import ( "github.com/pingcap/tidb-operator/apis/core/v1alpha1" "github.com/pingcap/tidb-operator/pkg/controllers/common" + "github.com/pingcap/tidb-operator/pkg/runtime" ) type state struct { @@ -42,6 +43,7 @@ type State interface { common.PodState common.PDSliceState + common.InstanceState[*runtime.PD] SetPod(*corev1.Pod) } @@ -64,6 +66,10 @@ func (s *state) Pod() *corev1.Pod { return s.pod } +func (s *state) Instance() *runtime.PD { + return runtime.FromPD(s.pd) +} + func (s *state) SetPod(pod *corev1.Pod) { s.pod = pod } diff --git a/pkg/controllers/pd/tasks/status.go b/pkg/controllers/pd/tasks/status.go index 324a882ba4..5d80e10fc7 100644 --- a/pkg/controllers/pd/tasks/status.go +++ b/pkg/controllers/pd/tasks/status.go @@ -28,48 +28,6 @@ import ( "github.com/pingcap/tidb-operator/third_party/kubernetes/pkg/controller/statefulset" ) -func TaskStatusSuspend(state *ReconcileContext, c client.Client) task.Task { - return task.NameTaskFunc("StatusSuspend", func(ctx context.Context) task.Result { - state.PD().Status.ObservedGeneration = state.PD().Generation - var ( - suspendStatus = metav1.ConditionFalse - suspendMessage = "pd is suspending" - - // when suspending, the health status should be false - healthStatus = metav1.ConditionFalse - healthMessage = "pd is not healthy" - ) - - if state.Pod() == nil { - suspendStatus = metav1.ConditionTrue - suspendMessage = "pd is suspended" - } - needUpdate := meta.SetStatusCondition(&state.PD().Status.Conditions, metav1.Condition{ - Type: v1alpha1.PDCondSuspended, - Status: suspendStatus, - ObservedGeneration: state.PD().Generation, - // TODO: use different reason for suspending and suspended - Reason: v1alpha1.PDSuspendReason, - Message: suspendMessage, - }) - - needUpdate = meta.SetStatusCondition(&state.PD().Status.Conditions, metav1.Condition{ - Type: v1alpha1.PDCondHealth, - Status: healthStatus, - ObservedGeneration: state.PD().Generation, - Reason: v1alpha1.PDHealthReason, - Message: healthMessage, - }) || needUpdate - if needUpdate { - if err := c.Status().Update(ctx, state.PD()); err != nil { - return task.Fail().With("cannot update status: %v", err) - } - } - - return task.Complete().With("status of suspend pd is updated") - }) -} - func TaskStatusUnknown() task.Task { return task.NameTaskFunc("StatusUnknown", func(_ context.Context) task.Result { return task.Wait().With("status of the pd is unknown") diff --git a/pkg/controllers/tidb/builder.go b/pkg/controllers/tidb/builder.go index 8a4de8c949..4f67b07303 100644 --- a/pkg/controllers/tidb/builder.go +++ b/pkg/controllers/tidb/builder.go @@ -15,44 +15,43 @@ package tidb import ( + "github.com/pingcap/tidb-operator/pkg/controllers/common" "github.com/pingcap/tidb-operator/pkg/controllers/tidb/tasks" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/runtime" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" ) -func (r *Reconciler) NewRunner(reporter task.TaskReporter) task.TaskRunner[tasks.ReconcileContext] { +func (r *Reconciler) NewRunner(state *tasks.ReconcileContext, reporter task.TaskReporter) task.TaskRunner { runner := task.NewTaskRunner(reporter, // get tidb - tasks.TaskContextTiDB(r.Client), + common.TaskContextTiDB(state, r.Client), // if it's deleted just return - task.NewSwitchTask(tasks.CondTiDBHasBeenDeleted()), + task.IfBreak(common.CondInstanceHasBeenDeleted(state)), // get cluster info, FinalizerDel will use it - tasks.TaskContextCluster(r.Client), - task.NewSwitchTask(tasks.CondPDIsNotInitialized()), + common.TaskContextCluster(state, r.Client), + // check whether it's paused + task.IfBreak(common.CondClusterIsPaused(state)), - task.NewSwitchTask(tasks.CondTiDBIsDeleting(), - tasks.TaskFinalizerDel(r.Client), + task.IfBreak(common.CondInstanceIsDeleting(state), + tasks.TaskFinalizerDel(state, r.Client), ), - - // check whether it's paused - task.NewSwitchTask(tasks.CondClusterIsPaused()), + common.TaskInstanceFinalizerAdd[runtime.TiDBTuple](state, r.Client), // get pod and check whether the cluster is suspending - tasks.TaskContextPod(r.Client), - task.NewSwitchTask(tasks.CondClusterIsSuspending(), - tasks.TaskFinalizerAdd(r.Client), - tasks.TaskPodSuspend(r.Client), - tasks.TaskStatusSuspend(r.Client), + common.TaskContextPod(state, r.Client), + task.IfBreak(common.CondClusterIsSuspending(state), + common.TaskSuspendPod(state, r.Client), + common.TaskInstanceStatusSuspend[runtime.TiDBTuple](state, r.Client), ), // normal process - tasks.TaskContextInfoFromPDAndTiDB(r.Client), - tasks.TaskFinalizerAdd(r.Client), - tasks.NewTaskConfigMap(r.Logger, r.Client), - tasks.NewTaskPVC(r.Logger, r.Client, r.VolumeModifier), - tasks.NewTaskPod(r.Logger, r.Client), - tasks.NewTaskServerLabels(r.Logger, r.Client), - tasks.NewTaskStatus(r.Logger, r.Client), + tasks.TaskContextInfoFromPDAndTiDB(state, r.Client, r.PDClientManager), + tasks.TaskConfigMap(state, r.Client), + tasks.TaskPVC(state, r.Logger, r.Client, r.VolumeModifier), + tasks.TaskPod(state, r.Client), + tasks.TaskServerLabels(state, r.Client), + tasks.TaskStatus(state, r.Client), ) return runner diff --git a/pkg/controllers/tidb/controller.go b/pkg/controllers/tidb/controller.go index 58e6e6407a..8184c0e799 100644 --- a/pkg/controllers/tidb/controller.go +++ b/pkg/controllers/tidb/controller.go @@ -27,22 +27,25 @@ import ( "github.com/pingcap/tidb-operator/apis/core/v1alpha1" "github.com/pingcap/tidb-operator/pkg/client" "github.com/pingcap/tidb-operator/pkg/controllers/tidb/tasks" + pdm "github.com/pingcap/tidb-operator/pkg/timanager/pd" "github.com/pingcap/tidb-operator/pkg/utils/k8s" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" "github.com/pingcap/tidb-operator/pkg/volumes" ) type Reconciler struct { - Logger logr.Logger - Client client.Client - VolumeModifier volumes.Modifier + Logger logr.Logger + Client client.Client + PDClientManager pdm.PDClientManager + VolumeModifier volumes.Modifier } -func Setup(mgr manager.Manager, c client.Client, vm volumes.Modifier) error { +func Setup(mgr manager.Manager, c client.Client, pdcm pdm.PDClientManager, vm volumes.Modifier) error { r := &Reconciler{ - Logger: mgr.GetLogger().WithName("TiDB"), - Client: c, - VolumeModifier: vm, + Logger: mgr.GetLogger().WithName("TiDB"), + Client: c, + PDClientManager: pdcm, + VolumeModifier: vm, } return ctrl.NewControllerManagedBy(mgr).For(&v1alpha1.TiDB{}). Owns(&corev1.Pod{}). @@ -66,11 +69,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu }() rtx := &tasks.ReconcileContext{ - // some fields will be set in the context task - Context: ctx, - Key: req.NamespacedName, + State: tasks.NewState(req.NamespacedName), } - runner := r.NewRunner(reporter) - return runner.Run(rtx) + runner := r.NewRunner(rtx, reporter) + return runner.Run(ctx) } diff --git a/pkg/controllers/tidb/tasks/cm.go b/pkg/controllers/tidb/tasks/cm.go index 498fee52b5..b6b2584cb0 100644 --- a/pkg/controllers/tidb/tasks/cm.go +++ b/pkg/controllers/tidb/tasks/cm.go @@ -15,7 +15,8 @@ package tasks import ( - "github.com/go-logr/logr" + "context" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -24,53 +25,39 @@ import ( tidbcfg "github.com/pingcap/tidb-operator/pkg/configs/tidb" "github.com/pingcap/tidb-operator/pkg/utils/hasher" maputil "github.com/pingcap/tidb-operator/pkg/utils/map" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" "github.com/pingcap/tidb-operator/pkg/utils/toml" ) -type TaskConfigMap struct { - Client client.Client - Logger logr.Logger -} - -func NewTaskConfigMap(logger logr.Logger, c client.Client) task.Task[ReconcileContext] { - return &TaskConfigMap{ - Client: c, - Logger: logger, - } -} - -func (*TaskConfigMap) Name() string { - return "ConfigMap" -} - -func (t *TaskConfigMap) Sync(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() +func TaskConfigMap(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("ConfigMap", func(ctx context.Context) task.Result { + cfg := tidbcfg.Config{} + decoder, encoder := toml.Codec[tidbcfg.Config]() + if err := decoder.Decode([]byte(state.TiDB().Spec.Config), &cfg); err != nil { + return task.Fail().With("tidb config cannot be decoded: %w", err) + } + if err := cfg.Overlay(state.Cluster(), state.TiDB()); err != nil { + return task.Fail().With("cannot generate tidb config: %w", err) + } + // NOTE(liubo02): don't use val in config file to generate pod + // TODO(liubo02): add a new field in api to control both config file and pod spec + state.GracefulWaitTimeInSeconds = int64(cfg.GracefulWaitBeforeShutdown) - c := tidbcfg.Config{} - decoder, encoder := toml.Codec[tidbcfg.Config]() - if err := decoder.Decode([]byte(rtx.TiDB.Spec.Config), &c); err != nil { - return task.Fail().With("tidb config cannot be decoded: %w", err) - } - if err := c.Overlay(rtx.Cluster, rtx.TiDB); err != nil { - return task.Fail().With("cannot generate tidb config: %w", err) - } - rtx.GracefulWaitTimeInSeconds = int64(c.GracefulWaitBeforeShutdown) + data, err := encoder.Encode(&cfg) + if err != nil { + return task.Fail().With("tidb config cannot be encoded: %w", err) + } - data, err := encoder.Encode(&c) - if err != nil { - return task.Fail().With("tidb config cannot be encoded: %w", err) - } - - rtx.ConfigHash, err = hasher.GenerateHash(rtx.TiDB.Spec.Config) - if err != nil { - return task.Fail().With("failed to generate hash for `tidb.spec.config`: %w", err) - } - expected := newConfigMap(rtx.TiDB, data, rtx.ConfigHash) - if e := t.Client.Apply(rtx, expected); e != nil { - return task.Fail().With("can't create/update cm of tidb: %w", e) - } - return task.Complete().With("cm is synced") + state.ConfigHash, err = hasher.GenerateHash(state.TiDB().Spec.Config) + if err != nil { + return task.Fail().With("failed to generate hash for `tidb.spec.config`: %w", err) + } + expected := newConfigMap(state.TiDB(), data, state.ConfigHash) + if e := c.Apply(ctx, expected); e != nil { + return task.Fail().With("can't create/update cm of tidb: %w", e) + } + return task.Complete().With("cm is synced") + }) } func newConfigMap(tidb *v1alpha1.TiDB, data []byte, hash string) *corev1.ConfigMap { diff --git a/pkg/controllers/tidb/tasks/cm_test.go b/pkg/controllers/tidb/tasks/cm_test.go deleted file mode 100644 index 54ba369d84..0000000000 --- a/pkg/controllers/tidb/tasks/cm_test.go +++ /dev/null @@ -1,238 +0,0 @@ -// Copyright 2024 PingCAP, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package tasks - -import ( - "context" - "testing" - - "github.com/go-logr/logr" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/utils/ptr" - - "github.com/pingcap/tidb-operator/apis/core/v1alpha1" - "github.com/pingcap/tidb-operator/pkg/client" - "github.com/pingcap/tidb-operator/pkg/utils/fake" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" -) - -func FakeContext(key types.NamespacedName, changes ...fake.ChangeFunc[ReconcileContext, *ReconcileContext]) *ReconcileContext { - ctx := fake.Fake(changes...) - ctx.Context = context.TODO() - ctx.Key = key - return ctx -} - -func WithTiDB(tidb *v1alpha1.TiDB) fake.ChangeFunc[ReconcileContext, *ReconcileContext] { - return func(obj *ReconcileContext) *ReconcileContext { - obj.TiDB = tidb - return obj - } -} - -func WithCluster(name string) fake.ChangeFunc[v1alpha1.TiDB, *v1alpha1.TiDB] { - return func(tidb *v1alpha1.TiDB) *v1alpha1.TiDB { - tidb.Spec.Cluster.Name = name - return tidb - } -} - -func withStatusPDURL(pdURL string) fake.ChangeFunc[v1alpha1.Cluster, *v1alpha1.Cluster] { - return func(cluster *v1alpha1.Cluster) *v1alpha1.Cluster { - cluster.Status.PD = pdURL - return cluster - } -} - -func withConfigForTiDB(config v1alpha1.ConfigFile) fake.ChangeFunc[v1alpha1.TiDB, *v1alpha1.TiDB] { - return func(tidb *v1alpha1.TiDB) *v1alpha1.TiDB { - tidb.Spec.Config = config - return tidb - } -} - -func withSubdomain(subdomain string) fake.ChangeFunc[v1alpha1.TiDB, *v1alpha1.TiDB] { - return func(tidb *v1alpha1.TiDB) *v1alpha1.TiDB { - tidb.Spec.Subdomain = subdomain - return tidb - } -} - -func TestConfigMap(t *testing.T) { - cases := []struct { - desc string - key types.NamespacedName - objs []client.Object - tidb *v1alpha1.TiDB - cluster *v1alpha1.Cluster - expected task.Result - expectedCM *corev1.ConfigMap - }{ - { - desc: "cm doesn't exist", - key: types.NamespacedName{ - Name: "test-tidb", - }, - objs: []client.Object{}, - tidb: fake.FakeObj( - "test-tidb", - WithCluster("test-cluster"), - withConfigForTiDB(v1alpha1.ConfigFile("")), - withSubdomain("subdomain"), - fake.Label[v1alpha1.TiDB]("aaa", "bbb"), - fake.UID[v1alpha1.TiDB]("test-uid"), - fake.Label[v1alpha1.TiDB](v1alpha1.LabelKeyInstanceRevisionHash, "foo"), - ), - cluster: fake.FakeObj("test-cluster", - withStatusPDURL("http://test-pd.default:2379"), - ), - expected: task.Complete().With(""), - expectedCM: &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-tidb-tidb", - Labels: map[string]string{ - "aaa": "bbb", - v1alpha1.LabelKeyInstance: "test-tidb", - v1alpha1.LabelKeyInstanceRevisionHash: "foo", - v1alpha1.LabelKeyConfigHash: "7d6fc488b7", - }, - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: v1alpha1.SchemeGroupVersion.String(), - Kind: "TiDB", - Name: "test-tidb", - UID: "test-uid", - BlockOwnerDeletion: ptr.To(true), - Controller: ptr.To(true), - }, - }, - }, - Data: map[string]string{ - v1alpha1.ConfigFileName: `advertise-address = 'test-tidb-tidb.subdomain.default.svc' -graceful-wait-before-shutdown = 60 -host = '::' -path = 'test-pd.default:2379' -store = 'tikv' - -[log] -slow-query-file = '/var/log/tidb/slowlog' -`, - }, - }, - }, - { - desc: "cm exists, update cm", - key: types.NamespacedName{ - Name: "test-tidb", - }, - objs: []client.Object{ - fake.FakeObj[corev1.ConfigMap]( - "test-tidb", - ), - }, - tidb: fake.FakeObj( - "test-tidb", - WithCluster("test-cluster"), - withConfigForTiDB(`zzz = 'zzz' -graceful-wait-before-shutdown = 60`), - withSubdomain("subdomain"), - fake.Label[v1alpha1.TiDB]("aaa", "bbb"), - fake.UID[v1alpha1.TiDB]("test-uid"), - fake.Label[v1alpha1.TiDB](v1alpha1.LabelKeyInstanceRevisionHash, "foo"), - ), - cluster: fake.FakeObj("test-cluster", - withStatusPDURL("http://test-pd.default:2379"), - ), - expected: task.Complete().With(""), - expectedCM: &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-tidb-tidb", - Labels: map[string]string{ - "aaa": "bbb", - v1alpha1.LabelKeyInstance: "test-tidb", - v1alpha1.LabelKeyInstanceRevisionHash: "foo", - v1alpha1.LabelKeyConfigHash: "7bd44dcc66", - }, - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: v1alpha1.SchemeGroupVersion.String(), - Kind: "TiDB", - Name: "test-tidb", - UID: "test-uid", - BlockOwnerDeletion: ptr.To(true), - Controller: ptr.To(true), - }, - }, - }, - Data: map[string]string{ - v1alpha1.ConfigFileName: `advertise-address = 'test-tidb-tidb.subdomain.default.svc' -graceful-wait-before-shutdown = 60 -host = '::' -path = 'test-pd.default:2379' -store = 'tikv' -zzz = 'zzz' - -[log] -slow-query-file = '/var/log/tidb/slowlog' -`, - }, - }, - }, - } - - for i := range cases { - c := &cases[i] - t.Run(c.desc, func(tt *testing.T) { - tt.Parallel() - - // append TiDB into store - objs := c.objs - objs = append(objs, c.tidb) - - ctx := FakeContext(c.key, WithTiDB(c.tidb)) - ctx.Cluster = c.cluster - fc := client.NewFakeClient(objs...) - tk := NewTaskConfigMap(logr.Discard(), fc) - res := tk.Sync(ctx) - - assert.Equal(tt, c.expected.Status(), res.Status(), res.Message()) - assert.Equal(tt, c.expected.RequeueAfter(), res.RequeueAfter(), res.Message()) - // Ignore message assertion - // TODO: maybe assert the message format? - - if res.Status() == task.SFail { - return - } - - cm := corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: ConfigMapName(ctx.TiDB.PodName()), - }, - } - err := fc.Get(ctx, client.ObjectKeyFromObject(&cm), &cm) - require.NoError(tt, err) - - // reset cm gvk and managed fields - cm.APIVersion = "" - cm.Kind = "" - cm.SetManagedFields(nil) - assert.Equal(tt, c.expectedCM, &cm) - }) - } -} diff --git a/pkg/controllers/tidb/tasks/ctx.go b/pkg/controllers/tidb/tasks/ctx.go index daae7aa937..a2161168ac 100644 --- a/pkg/controllers/tidb/tasks/ctx.go +++ b/pkg/controllers/tidb/tasks/ctx.go @@ -20,15 +20,12 @@ import ( "fmt" "time" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "github.com/pingcap/tidb-operator/apis/core/v1alpha1" "github.com/pingcap/tidb-operator/pkg/client" "github.com/pingcap/tidb-operator/pkg/pdapi/v1" "github.com/pingcap/tidb-operator/pkg/tidbapi/v1" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + pdm "github.com/pingcap/tidb-operator/pkg/timanager/pd" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" tlsutil "github.com/pingcap/tidb-operator/pkg/utils/tls" ) @@ -38,9 +35,7 @@ const ( ) type ReconcileContext struct { - context.Context - - Key types.NamespacedName + State TiDBClient tidbapi.TiDBClient PDClient pdapi.PDClient @@ -48,10 +43,6 @@ type ReconcileContext struct { Healthy bool Suspended bool - Cluster *v1alpha1.Cluster - TiDB *v1alpha1.TiDB - Pod *corev1.Pod - GracefulWaitTimeInSeconds int64 // ConfigHash stores the hash of **user-specified** config (i.e.`.Spec.Config`), @@ -64,120 +55,36 @@ type ReconcileContext struct { PodIsTerminating bool } -func (ctx *ReconcileContext) Self() *ReconcileContext { - return ctx -} - -func TaskContextTiDB(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("ContextTiDB", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - var tidb v1alpha1.TiDB - if err := c.Get(ctx, rtx.Key, &tidb); err != nil { - if !errors.IsNotFound(err) { - return task.Fail().With("can't get tidb instance: %w", err) - } - return task.Complete().With("tidb instance has been deleted") - } - rtx.TiDB = &tidb - return task.Complete().With("tidb is set") - }) -} - -func TaskContextCluster(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("ContextCluster", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - var cluster v1alpha1.Cluster - if err := c.Get(ctx, client.ObjectKey{ - Name: rtx.TiDB.Spec.Cluster.Name, - Namespace: rtx.TiDB.Namespace, - }, &cluster); err != nil { - return task.Fail().With("cannot find cluster %s: %w", rtx.TiDB.Spec.Cluster.Name, err) - } - rtx.Cluster = &cluster - return task.Complete().With("cluster is set") - }) -} - -func TaskContextPod(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("ContextPod", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - var pod corev1.Pod - if err := c.Get(ctx, client.ObjectKey{ - Name: rtx.TiDB.PodName(), - Namespace: rtx.TiDB.Namespace, - }, &pod); err != nil { - if errors.IsNotFound(err) { - return task.Complete().With("pod is not created") - } - return task.Fail().With("failed to get pod of tidb: %w", err) - } - - rtx.Pod = &pod - if !rtx.Pod.GetDeletionTimestamp().IsZero() { - rtx.PodIsTerminating = true - } - return task.Complete().With("pod is set") - }) -} - -func TaskContextInfoFromPDAndTiDB(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("ContextInfoFromPDAndTiDB", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - if rtx.Cluster.Status.PD == "" { - return task.Fail().With("pd url is not initialized") - } +func TaskContextInfoFromPDAndTiDB(state *ReconcileContext, c client.Client, cm pdm.PDClientManager) task.Task { + return task.NameTaskFunc("ContextInfoFromPDAndTiDB", func(ctx context.Context) task.Result { var ( scheme = "http" tlsConfig *tls.Config ) - if rtx.Cluster.IsTLSClusterEnabled() { + ck := state.Cluster() + if ck.IsTLSClusterEnabled() { scheme = "https" var err error tlsConfig, err = tlsutil.GetTLSConfigFromSecret(ctx, c, - rtx.Cluster.Namespace, v1alpha1.TLSClusterClientSecretName(rtx.Cluster.Name)) + ck.Namespace, v1alpha1.TLSClusterClientSecretName(ck.Name)) if err != nil { return task.Fail().With("cannot get tls config from secret: %w", err) } } - rtx.TiDBClient = tidbapi.NewTiDBClient(TiDBServiceURL(rtx.TiDB, scheme), tidbRequestTimeout, tlsConfig) - health, err := rtx.TiDBClient.GetHealth(ctx) + state.TiDBClient = tidbapi.NewTiDBClient(TiDBServiceURL(state.TiDB(), scheme), tidbRequestTimeout, tlsConfig) + health, err := state.TiDBClient.GetHealth(ctx) if err != nil { return task.Complete().With( fmt.Sprintf("context without health info is completed, tidb can't be reached: %v", err)) } - rtx.Healthy = health - rtx.PDClient = pdapi.NewPDClient(rtx.Cluster.Status.PD, pdRequestTimeout, tlsConfig) - - return task.Complete().With("get info from tidb") - }) -} + state.Healthy = health -func CondTiDBHasBeenDeleted() task.Condition[ReconcileContext] { - return task.CondFunc[ReconcileContext](func(ctx task.Context[ReconcileContext]) bool { - return ctx.Self().TiDB == nil - }) -} - -func CondTiDBIsDeleting() task.Condition[ReconcileContext] { - return task.CondFunc[ReconcileContext](func(ctx task.Context[ReconcileContext]) bool { - return !ctx.Self().TiDB.GetDeletionTimestamp().IsZero() - }) -} - -func CondClusterIsPaused() task.Condition[ReconcileContext] { - return task.CondFunc[ReconcileContext](func(ctx task.Context[ReconcileContext]) bool { - return ctx.Self().Cluster.ShouldPauseReconcile() - }) -} - -func CondClusterIsSuspending() task.Condition[ReconcileContext] { - return task.CondFunc[ReconcileContext](func(ctx task.Context[ReconcileContext]) bool { - return ctx.Self().Cluster.ShouldSuspendCompute() - }) -} + pdc, ok := cm.Get(pdm.PrimaryKey(ck.Namespace, ck.Name)) + if !ok { + return task.Complete().With("pd client is not registered") + } + state.PDClient = pdc.Underlay() -func CondPDIsNotInitialized() task.Condition[ReconcileContext] { - return task.CondFunc[ReconcileContext](func(ctx task.Context[ReconcileContext]) bool { - return ctx.Self().Cluster.Status.PD == "" + return task.Complete().With("get info from tidb") }) } diff --git a/pkg/controllers/tidb/tasks/finalizer.go b/pkg/controllers/tidb/tasks/finalizer.go index 96735f6cd3..6ce3227fd6 100644 --- a/pkg/controllers/tidb/tasks/finalizer.go +++ b/pkg/controllers/tidb/tasks/finalizer.go @@ -23,20 +23,19 @@ import ( "github.com/pingcap/tidb-operator/pkg/client" "github.com/pingcap/tidb-operator/pkg/runtime" "github.com/pingcap/tidb-operator/pkg/utils/k8s" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" ) -func TaskFinalizerDel(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("FinalizerDel", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - wait, err := EnsureSubResourcesDeleted(ctx, c, rtx.TiDB) +func TaskFinalizerDel(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("FinalizerDel", func(ctx context.Context) task.Result { + wait, err := EnsureSubResourcesDeleted(ctx, c, state.TiDB()) if err != nil { return task.Fail().With("cannot delete subresources: %w", err) } if wait { return task.Wait().With("wait all subresources deleted") } - if err := k8s.RemoveFinalizer(ctx, c, rtx.TiDB); err != nil { + if err := k8s.RemoveFinalizer(ctx, c, state.TiDB()); err != nil { return task.Fail().With("cannot remove finalizer: %w", err) } @@ -44,17 +43,6 @@ func TaskFinalizerDel(c client.Client) task.Task[ReconcileContext] { }) } -func TaskFinalizerAdd(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("FinalizerAdd", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - if err := k8s.EnsureFinalizer(ctx, c, rtx.TiDB); err != nil { - return task.Fail().With("failed to ensure finalizer has been added: %w", err) - } - - return task.Complete().With("finalizer is added") - }) -} - func EnsureSubResourcesDeleted(ctx context.Context, c client.Client, db *v1alpha1.TiDB) (wait bool, _ error) { wait1, err := k8s.DeleteInstanceSubresource(ctx, c, runtime.FromTiDB(db), &corev1.PodList{}) if err != nil { diff --git a/pkg/controllers/tidb/tasks/pod.go b/pkg/controllers/tidb/tasks/pod.go index a8c2e72521..c74a1d21a9 100644 --- a/pkg/controllers/tidb/tasks/pod.go +++ b/pkg/controllers/tidb/tasks/pod.go @@ -15,6 +15,7 @@ package tasks import ( + "context" "fmt" "path" "path/filepath" @@ -31,7 +32,7 @@ import ( "github.com/pingcap/tidb-operator/pkg/overlay" "github.com/pingcap/tidb-operator/pkg/utils/k8s" maputil "github.com/pingcap/tidb-operator/pkg/utils/map" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" ) const ( @@ -45,76 +46,48 @@ const ( defaultReadinessProbeInitialDelaySeconds = 10 ) -func TaskPodSuspend(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("PodSuspend", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - if rtx.Pod == nil { - return task.Complete().With("pod has been deleted") - } - if err := c.Delete(rtx, rtx.Pod); err != nil { - return task.Fail().With("can't delete pod of tidb: %w", err) - } - rtx.PodIsTerminating = true - return task.Wait().With("pod is deleting") - }) -} - -type TaskPod struct { - Client client.Client - Logger logr.Logger -} +func TaskPod(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("Pod", func(ctx context.Context) task.Result { + logger := logr.FromContextOrDiscard(ctx) -func NewTaskPod(logger logr.Logger, c client.Client) task.Task[ReconcileContext] { - return &TaskPod{ - Client: c, - Logger: logger, - } -} - -func (*TaskPod) Name() string { - return "Pod" -} - -func (t *TaskPod) Sync(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() + expected := newPod(state.Cluster(), state.TiDB(), state.GracefulWaitTimeInSeconds, state.ConfigHash) + if state.Pod() == nil { + if err := c.Apply(ctx, expected); err != nil { + return task.Fail().With("can't create pod of tidb: %w", err) + } - expected := t.newPod(rtx.Cluster, rtx.TiDB, rtx.GracefulWaitTimeInSeconds, rtx.ConfigHash) - if rtx.Pod == nil { - if err := t.Client.Apply(rtx, expected); err != nil { - return task.Fail().With("can't create pod of tidb: %w", err) + state.SetPod(expected) + return task.Complete().With("pod is created") } - rtx.Pod = expected - return task.Complete().With("pod is created") - } + res := k8s.ComparePods(state.Pod(), expected) + curHash, expectHash := state.Pod().Labels[v1alpha1.LabelKeyConfigHash], expected.Labels[v1alpha1.LabelKeyConfigHash] + configChanged := curHash != expectHash + logger.Info("compare pod", "result", res, "configChanged", configChanged, "currentConfigHash", curHash, "expectConfigHash", expectHash) - res := k8s.ComparePods(rtx.Pod, expected) - curHash, expectHash := rtx.Pod.Labels[v1alpha1.LabelKeyConfigHash], expected.Labels[v1alpha1.LabelKeyConfigHash] - configChanged := curHash != expectHash - t.Logger.Info("compare pod", "result", res, "configChanged", configChanged, "currentConfigHash", curHash, "expectConfigHash", expectHash) + if res == k8s.CompareResultRecreate || (configChanged && + state.TiDB().Spec.UpdateStrategy.Config == v1alpha1.ConfigUpdateStrategyRestart) { + logger.Info("will recreate the pod") + if err := c.Delete(ctx, state.Pod()); err != nil { + return task.Fail().With("can't delete pod of tidb: %w", err) + } - if res == k8s.CompareResultRecreate || (configChanged && - rtx.TiDB.Spec.UpdateStrategy.Config == v1alpha1.ConfigUpdateStrategyRestart) { - t.Logger.Info("will recreate the pod") - if err := t.Client.Delete(rtx, rtx.Pod); err != nil { - return task.Fail().With("can't delete pod of tidb: %w", err) - } + state.PodIsTerminating = true + return task.Complete().With("pod is deleting") + } else if res == k8s.CompareResultUpdate { + logger.Info("will update the pod in place") + if err := c.Apply(ctx, expected); err != nil { + return task.Fail().With("can't apply pod of tidb: %w", err) + } - rtx.PodIsTerminating = true - return task.Complete().With("pod is deleting") - } else if res == k8s.CompareResultUpdate { - t.Logger.Info("will update the pod in place") - if err := t.Client.Apply(rtx, expected); err != nil { - return task.Fail().With("can't apply pod of tidb: %w", err) + state.SetPod(expected) } - rtx.Pod = expected - } - - return task.Complete().With("pod is synced") + return task.Complete().With("pod is synced") + }) } -func (*TaskPod) newPod(cluster *v1alpha1.Cluster, +func newPod(cluster *v1alpha1.Cluster, tidb *v1alpha1.TiDB, gracePeriod int64, configHash string, ) *corev1.Pod { vols := []corev1.Volume{ diff --git a/pkg/controllers/tidb/tasks/pvc.go b/pkg/controllers/tidb/tasks/pvc.go index 49a572cbdd..380e996328 100644 --- a/pkg/controllers/tidb/tasks/pvc.go +++ b/pkg/controllers/tidb/tasks/pvc.go @@ -15,6 +15,8 @@ package tasks import ( + "context" + "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -22,39 +24,21 @@ import ( "github.com/pingcap/tidb-operator/apis/core/v1alpha1" "github.com/pingcap/tidb-operator/pkg/client" maputil "github.com/pingcap/tidb-operator/pkg/utils/map" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" "github.com/pingcap/tidb-operator/pkg/volumes" ) -type TaskPVC struct { - Client client.Client - Logger logr.Logger - VolumeModifier volumes.Modifier -} - -func NewTaskPVC(logger logr.Logger, c client.Client, vm volumes.Modifier) task.Task[ReconcileContext] { - return &TaskPVC{ - Client: c, - Logger: logger, - VolumeModifier: vm, - } -} - -func (*TaskPVC) Name() string { - return "PVC" -} - -func (t *TaskPVC) Sync(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - - pvcs := newPVCs(rtx.TiDB) - if wait, err := volumes.SyncPVCs(rtx, t.Client, pvcs, t.VolumeModifier, t.Logger); err != nil { - return task.Fail().With("failed to sync pvcs: %w", err) - } else if wait { - return task.Complete().With("waiting for pvcs to be synced") - } +func TaskPVC(state *ReconcileContext, logger logr.Logger, c client.Client, vm volumes.Modifier) task.Task { + return task.NameTaskFunc("PVC", func(ctx context.Context) task.Result { + pvcs := newPVCs(state.TiDB()) + if wait, err := volumes.SyncPVCs(ctx, c, pvcs, vm, logger); err != nil { + return task.Fail().With("failed to sync pvcs: %w", err) + } else if wait { + return task.Complete().With("waiting for pvcs to be synced") + } - return task.Complete().With("pvcs are synced") + return task.Complete().With("pvcs are synced") + }) } func newPVCs(tidb *v1alpha1.TiDB) []*corev1.PersistentVolumeClaim { diff --git a/pkg/controllers/tidb/tasks/server_labels.go b/pkg/controllers/tidb/tasks/server_labels.go index 9f23a8fdb3..2fe764474e 100644 --- a/pkg/controllers/tidb/tasks/server_labels.go +++ b/pkg/controllers/tidb/tasks/server_labels.go @@ -15,12 +15,13 @@ package tasks import ( - "github.com/go-logr/logr" + "context" + corev1 "k8s.io/api/core/v1" "github.com/pingcap/tidb-operator/pkg/client" "github.com/pingcap/tidb-operator/pkg/utils/k8s" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" ) var ( @@ -31,68 +32,52 @@ var ( tidbDCLabel = "zone" ) -type TaskServerLabels struct { - Client client.Client - Logger logr.Logger -} - -func NewTaskServerLabels(logger logr.Logger, c client.Client) task.Task[ReconcileContext] { - return &TaskServerLabels{ - Client: c, - Logger: logger, - } -} - -func (*TaskServerLabels) Name() string { - return "ServerLabels" -} - -func (t *TaskServerLabels) Sync(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - - if !rtx.Healthy || rtx.Pod == nil || rtx.PodIsTerminating { - return task.Complete().With("skip sync server labels as the instance is not healthy") - } +func TaskServerLabels(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("ServerLabels", func(ctx context.Context) task.Result { + if !state.Healthy || state.Pod() == nil || state.PodIsTerminating { + return task.Complete().With("skip sync server labels as the instance is not healthy") + } - nodeName := rtx.Pod.Spec.NodeName - if nodeName == "" { - return task.Fail().With("pod %s/%s has not been scheduled", rtx.Pod.Namespace, rtx.Pod.Name) - } - var node corev1.Node - if err := t.Client.Get(ctx, client.ObjectKey{Name: nodeName}, &node); err != nil { - return task.Fail().With("failed to get node %s: %s", nodeName, err) - } + nodeName := state.Pod().Spec.NodeName + if nodeName == "" { + return task.Fail().With("pod %s/%s has not been scheduled", state.Pod().Namespace, state.Pod().Name) + } + var node corev1.Node + if err := c.Get(ctx, client.ObjectKey{Name: nodeName}, &node); err != nil { + return task.Fail().With("failed to get node %s: %s", nodeName, err) + } - // TODO: too many API calls to PD? - pdCfg, err := rtx.PDClient.GetConfig(ctx) - if err != nil { - return task.Fail().With("failed to get pd config: %s", err) - } + // TODO: too many API calls to PD? + pdCfg, err := state.PDClient.GetConfig(ctx) + if err != nil { + return task.Fail().With("failed to get pd config: %s", err) + } - var zoneLabel string -outer: - for _, zl := range topologyZoneLabels { - for _, ll := range pdCfg.Replication.LocationLabels { - if ll == zl { - zoneLabel = zl - break outer + var zoneLabel string + outer: + for _, zl := range topologyZoneLabels { + for _, ll := range pdCfg.Replication.LocationLabels { + if ll == zl { + zoneLabel = zl + break outer + } } } - } - if zoneLabel == "" { - return task.Complete().With("zone labels not found in pd location-label, skip sync server labels") - } + if zoneLabel == "" { + return task.Complete().With("zone labels not found in pd location-label, skip sync server labels") + } - serverLabels := k8s.GetNodeLabelsForKeys(&node, pdCfg.Replication.LocationLabels) - if len(serverLabels) == 0 { - return task.Complete().With("no server labels from node %s to sync", nodeName) - } - serverLabels[tidbDCLabel] = serverLabels[zoneLabel] + serverLabels := k8s.GetNodeLabelsForKeys(&node, pdCfg.Replication.LocationLabels) + if len(serverLabels) == 0 { + return task.Complete().With("no server labels from node %s to sync", nodeName) + } + serverLabels[tidbDCLabel] = serverLabels[zoneLabel] - // TODO: is there any way to avoid unnecessary update? - if err := rtx.TiDBClient.SetServerLabels(ctx, serverLabels); err != nil { - return task.Fail().With("failed to set server labels: %s", err) - } + // TODO: is there any way to avoid unnecessary update? + if err := state.TiDBClient.SetServerLabels(ctx, serverLabels); err != nil { + return task.Fail().With("failed to set server labels: %s", err) + } - return task.Complete().With("server labels synced") + return task.Complete().With("server labels synced") + }) } diff --git a/pkg/controllers/tidb/tasks/state.go b/pkg/controllers/tidb/tasks/state.go new file mode 100644 index 0000000000..dc58e4da01 --- /dev/null +++ b/pkg/controllers/tidb/tasks/state.go @@ -0,0 +1,98 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tasks + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/pingcap/tidb-operator/apis/core/v1alpha1" + "github.com/pingcap/tidb-operator/pkg/controllers/common" + "github.com/pingcap/tidb-operator/pkg/runtime" +) + +type state struct { + key types.NamespacedName + + cluster *v1alpha1.Cluster + tidb *v1alpha1.TiDB + pod *corev1.Pod +} + +type State interface { + common.TiDBStateInitializer + common.ClusterStateInitializer + common.PodStateInitializer + + common.TiDBState + common.ClusterState + common.PodState + + common.InstanceState[*runtime.TiDB] + + SetPod(*corev1.Pod) +} + +func NewState(key types.NamespacedName) State { + s := &state{ + key: key, + } + return s +} + +func (s *state) TiDB() *v1alpha1.TiDB { + return s.tidb +} + +func (s *state) Cluster() *v1alpha1.Cluster { + return s.cluster +} + +func (s *state) Pod() *corev1.Pod { + return s.pod +} + +func (s *state) Instance() *runtime.TiDB { + return runtime.FromTiDB(s.tidb) +} + +func (s *state) SetPod(pod *corev1.Pod) { + s.pod = pod +} + +func (s *state) TiDBInitializer() common.TiDBInitializer { + return common.NewResource(func(tidb *v1alpha1.TiDB) { s.tidb = tidb }). + WithNamespace(common.Namespace(s.key.Namespace)). + WithName(common.Name(s.key.Name)). + Initializer() +} + +func (s *state) ClusterInitializer() common.ClusterInitializer { + return common.NewResource(func(cluster *v1alpha1.Cluster) { s.cluster = cluster }). + WithNamespace(common.Namespace(s.key.Namespace)). + WithName(common.Lazy[string](func() string { + return s.tidb.Spec.Cluster.Name + })). + Initializer() +} + +func (s *state) PodInitializer() common.PodInitializer { + return common.NewResource(s.SetPod). + WithNamespace(common.Namespace(s.key.Namespace)). + WithName(common.Lazy[string](func() string { + return s.tidb.PodName() + })). + Initializer() +} diff --git a/pkg/controllers/tidb/tasks/status.go b/pkg/controllers/tidb/tasks/status.go index a6c8ba7548..4666010845 100644 --- a/pkg/controllers/tidb/tasks/status.go +++ b/pkg/controllers/tidb/tasks/status.go @@ -15,15 +15,15 @@ package tasks import ( + "context" "time" - "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/pingcap/tidb-operator/apis/core/v1alpha1" "github.com/pingcap/tidb-operator/pkg/client" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" "github.com/pingcap/tidb-operator/third_party/kubernetes/pkg/controller/statefulset" ) @@ -31,128 +31,89 @@ const ( defaultTaskWaitDuration = 5 * time.Second ) -func TaskStatusSuspend(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("StatusSuspend", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - rtx.TiDB.Status.ObservedGeneration = rtx.TiDB.Generation +// TODO(liubo02): extract to common task +func TaskStatus(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("Status", func(ctx context.Context) task.Result { + needUpdate := false + tidb := state.TiDB() + pod := state.Pod() + // TODO(liubo02): simplify it + var healthy bool + + if pod != nil && + statefulset.IsPodRunningAndReady(pod) && + !state.PodIsTerminating && + state.Healthy { + healthy = true + } - var ( - suspendStatus = metav1.ConditionFalse - suspendMessage = "tidb is suspending" + needUpdate = syncHealthCond(tidb, healthy) || needUpdate + needUpdate = syncSuspendCond(tidb) || needUpdate - // when suspending, the health status should be false - healthStatus = metav1.ConditionFalse - healthMessage = "tidb is not healthy" - ) + needUpdate = SetIfChanged(&tidb.Status.ObservedGeneration, tidb.Generation) || needUpdate + needUpdate = SetIfChanged(&tidb.Status.UpdateRevision, tidb.Labels[v1alpha1.LabelKeyInstanceRevisionHash]) || needUpdate - if rtx.Pod == nil { - suspendStatus = metav1.ConditionTrue - suspendMessage = "tidb is suspended" + if healthy { + needUpdate = SetIfChanged(&tidb.Status.CurrentRevision, pod.Labels[v1alpha1.LabelKeyInstanceRevisionHash]) || needUpdate } - needUpdate := meta.SetStatusCondition(&rtx.TiDB.Status.Conditions, metav1.Condition{ - Type: v1alpha1.TiDBCondSuspended, - Status: suspendStatus, - ObservedGeneration: rtx.TiDB.Generation, - // TODO: use different reason for suspending and suspended - Reason: v1alpha1.TiDBSuspendReason, - Message: suspendMessage, - }) - - needUpdate = meta.SetStatusCondition(&rtx.TiDB.Status.Conditions, metav1.Condition{ - Type: v1alpha1.TiDBCondHealth, - Status: healthStatus, - ObservedGeneration: rtx.TiDB.Generation, - Reason: v1alpha1.TiDBHealthReason, - Message: healthMessage, - }) || needUpdate if needUpdate { - if err := c.Status().Update(ctx, rtx.TiDB); err != nil { + if err := c.Status().Update(ctx, state.TiDB()); err != nil { return task.Fail().With("cannot update status: %w", err) } } - return task.Complete().With("status is suspend tidb is updated") - }) -} - -type TaskStatus struct { - Client client.Client - Logger logr.Logger -} - -func NewTaskStatus(logger logr.Logger, c client.Client) task.Task[ReconcileContext] { - return &TaskStatus{ - Client: c, - Logger: logger, - } -} + if !state.Healthy || !v1alpha1.IsUpToDate(state.TiDB()) { + // can we only rely on Pod status events to trigger the retry? + // TODO(liubo02): change to task.Wait + return task.Retry(defaultTaskWaitDuration).With("tidb may not be healthy, requeue to retry") + } -func (*TaskStatus) Name() string { - return "Status" + return task.Complete().With("status is synced") + }) } -func (t *TaskStatus) Sync(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - +func syncHealthCond(tidb *v1alpha1.TiDB, healthy bool) bool { var ( - healthStatus = metav1.ConditionFalse - healthMessage = "tidb is not healthy" - - suspendStatus = metav1.ConditionFalse - suspendMessage = "tidb is not suspended" - - needUpdate = false + status = metav1.ConditionFalse + reason = "Unhealthy" + msg = "instance is not healthy" ) - - conditionChanged := meta.SetStatusCondition(&rtx.TiDB.Status.Conditions, metav1.Condition{ - Type: v1alpha1.TiDBCondSuspended, - Status: suspendStatus, - ObservedGeneration: rtx.TiDB.Generation, - Reason: v1alpha1.TiDBSuspendReason, - Message: suspendMessage, - }) - - if !v1alpha1.IsReconciled(rtx.TiDB) || rtx.TiDB.Status.UpdateRevision != rtx.TiDB.Labels[v1alpha1.LabelKeyInstanceRevisionHash] { - rtx.TiDB.Status.ObservedGeneration = rtx.TiDB.Generation - rtx.TiDB.Status.UpdateRevision = rtx.TiDB.Labels[v1alpha1.LabelKeyInstanceRevisionHash] - needUpdate = true + if healthy { + status = metav1.ConditionTrue + reason = "Healthy" + msg = "instance is healthy" } - if rtx.Pod == nil || rtx.PodIsTerminating { - rtx.Healthy = false - } else if statefulset.IsPodRunningAndReady(rtx.Pod) && rtx.Healthy { - if rtx.TiDB.Status.CurrentRevision != rtx.Pod.Labels[v1alpha1.LabelKeyInstanceRevisionHash] { - rtx.TiDB.Status.CurrentRevision = rtx.Pod.Labels[v1alpha1.LabelKeyInstanceRevisionHash] - needUpdate = true - } - } else { - rtx.Healthy = false - } + return meta.SetStatusCondition(&tidb.Status.Conditions, metav1.Condition{ + Type: v1alpha1.CondHealth, + Status: status, + ObservedGeneration: tidb.Generation, + Reason: reason, + Message: msg, + }) +} - if rtx.Healthy { - healthStatus = metav1.ConditionTrue - healthMessage = "tidb is healthy" - } - updateCond := metav1.Condition{ - Type: v1alpha1.TiDBCondHealth, - Status: healthStatus, - ObservedGeneration: rtx.TiDB.Generation, - Reason: v1alpha1.TiDBHealthReason, - Message: healthMessage, - } - conditionChanged = meta.SetStatusCondition(&rtx.TiDB.Status.Conditions, updateCond) || conditionChanged +func syncSuspendCond(tidb *v1alpha1.TiDB) bool { + // always set it as unsuspended + return meta.SetStatusCondition(&tidb.Status.Conditions, metav1.Condition{ + Type: v1alpha1.CondSuspended, + Status: metav1.ConditionFalse, + ObservedGeneration: tidb.Generation, + Reason: v1alpha1.ReasonUnsuspended, + Message: "instace is not suspended", + }) +} - if needUpdate || conditionChanged { - if err := t.Client.Status().Update(ctx, rtx.TiDB); err != nil { - return task.Fail().With("cannot update status: %w", err) - } +// TODO: move to utils +func SetIfChanged[T comparable](dst *T, src T) bool { + if src == *new(T) { + return false } - - if !rtx.Healthy || !v1alpha1.IsUpToDate(rtx.TiDB) { - // can we only rely on Pod status events to trigger the retry? - return task.Retry(defaultTaskWaitDuration).With("tidb may not be healthy, requeue to retry") + if *dst != src { + *dst = src + return true } - return task.Complete().With("status is synced") + return false } diff --git a/pkg/controllers/tiflash/builder.go b/pkg/controllers/tiflash/builder.go index 9cfa2d6297..4c3cbf0a3d 100644 --- a/pkg/controllers/tiflash/builder.go +++ b/pkg/controllers/tiflash/builder.go @@ -15,44 +15,45 @@ package tiflash import ( + "github.com/pingcap/tidb-operator/pkg/controllers/common" "github.com/pingcap/tidb-operator/pkg/controllers/tiflash/tasks" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/runtime" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" ) -func (r *Reconciler) NewRunner(reporter task.TaskReporter) task.TaskRunner[tasks.ReconcileContext] { +func (r *Reconciler) NewRunner(state *tasks.ReconcileContext, reporter task.TaskReporter) task.TaskRunner { runner := task.NewTaskRunner(reporter, // Get tiflash - tasks.TaskContextTiFlash(r.Client), - // If it's deleted just return - task.NewSwitchTask(tasks.CondTiFlashHasBeenDeleted()), + common.TaskContextTiFlash(state, r.Client), + // if it's deleted just return + task.IfBreak(common.CondInstanceHasBeenDeleted(state)), // get cluster info, FinalizerDel will use it - tasks.TaskContextCluster(r.Client), + common.TaskContextCluster(state, r.Client), + // check whether it's paused + task.IfBreak(common.CondClusterIsPaused(state)), + // get info from pd - tasks.TaskContextInfoFromPD(r.PDClientManager), + tasks.TaskContextInfoFromPD(state, r.PDClientManager), - task.NewSwitchTask(tasks.CondTiFlashIsDeleting(), - tasks.TaskFinalizerDel(r.Client), + task.IfBreak(common.CondInstanceIsDeleting(state), + tasks.TaskFinalizerDel(state, r.Client), ), - - // check whether it's paused - task.NewSwitchTask(tasks.CondClusterIsPaused()), + common.TaskInstanceFinalizerAdd[runtime.TiFlashTuple](state, r.Client), // get pod and check whether the cluster is suspending - tasks.TaskContextPod(r.Client), - task.NewSwitchTask(tasks.CondClusterIsSuspending(), - tasks.TaskFinalizerAdd(r.Client), - tasks.TaskPodSuspend(r.Client), - tasks.TaskStatusSuspend(r.Client), + common.TaskContextPod(state, r.Client), + task.IfBreak(common.CondClusterIsSuspending(state), + common.TaskSuspendPod(state, r.Client), + common.TaskInstanceStatusSuspend[runtime.TiFlashTuple](state, r.Client), ), // normal process - tasks.TaskFinalizerAdd(r.Client), - tasks.NewTaskConfigMap(r.Logger, r.Client), - tasks.NewTaskPVC(r.Logger, r.Client, r.VolumeModifier), - tasks.NewTaskPod(r.Logger, r.Client), - tasks.NewTaskStoreLabels(r.Logger, r.Client), - tasks.NewTaskStatus(r.Logger, r.Client), + tasks.TaskConfigMap(state, r.Client), + tasks.TaskPVC(state, r.Logger, r.Client, r.VolumeModifier), + tasks.TaskPod(state, r.Client), + tasks.TaskStoreLabels(state, r.Client), + tasks.TaskStatus(state, r.Client), ) return runner diff --git a/pkg/controllers/tiflash/controller.go b/pkg/controllers/tiflash/controller.go index bdd9b3a839..675055f1ae 100644 --- a/pkg/controllers/tiflash/controller.go +++ b/pkg/controllers/tiflash/controller.go @@ -30,7 +30,7 @@ import ( pdv1 "github.com/pingcap/tidb-operator/pkg/timanager/apis/pd/v1" pdm "github.com/pingcap/tidb-operator/pkg/timanager/pd" "github.com/pingcap/tidb-operator/pkg/utils/k8s" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" "github.com/pingcap/tidb-operator/pkg/volumes" ) @@ -71,11 +71,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu }() rtx := &tasks.ReconcileContext{ - // some fields will be set in the context task - Context: ctx, - Key: req.NamespacedName, + State: tasks.NewState(req.NamespacedName), } - runner := r.NewRunner(reporter) - return runner.Run(rtx) + runner := r.NewRunner(rtx, reporter) + return runner.Run(ctx) } diff --git a/pkg/controllers/tiflash/tasks/cm.go b/pkg/controllers/tiflash/tasks/cm.go index d0c3e89dd2..9597b3615b 100644 --- a/pkg/controllers/tiflash/tasks/cm.go +++ b/pkg/controllers/tiflash/tasks/cm.go @@ -15,7 +15,8 @@ package tasks import ( - "github.com/go-logr/logr" + "context" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -24,64 +25,48 @@ import ( tiflashcfg "github.com/pingcap/tidb-operator/pkg/configs/tiflash" "github.com/pingcap/tidb-operator/pkg/utils/hasher" maputil "github.com/pingcap/tidb-operator/pkg/utils/map" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" "github.com/pingcap/tidb-operator/pkg/utils/toml" ) -type TaskConfigMap struct { - Client client.Client - Logger logr.Logger -} +func TaskConfigMap(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("ConfigMap", func(ctx context.Context) task.Result { + cfg := tiflashcfg.Config{} + decoder, encoder := toml.Codec[tiflashcfg.Config]() + if err := decoder.Decode([]byte(state.TiFlash().Spec.Config), &cfg); err != nil { + return task.Fail().With("tiflash config cannot be decoded: %w", err) + } + if err := cfg.Overlay(state.Cluster(), state.TiFlash()); err != nil { + return task.Fail().With("cannot generate tiflash config: %w", err) + } + flashData, err := encoder.Encode(&cfg) + if err != nil { + return task.Fail().With("tiflash config cannot be encoded: %w", err) + } -func NewTaskConfigMap(logger logr.Logger, c client.Client) task.Task[ReconcileContext] { - return &TaskConfigMap{ - Client: c, - Logger: logger, - } -} + proxyConfig := tiflashcfg.ProxyConfig{} + decoderProxy, encoderProxy := toml.Codec[tiflashcfg.ProxyConfig]() + if err = decoderProxy.Decode([]byte(state.TiFlash().Spec.ProxyConfig), &proxyConfig); err != nil { + return task.Fail().With("tiflash proxy config cannot be decoded: %w", err) + } + if err = proxyConfig.Overlay(state.Cluster(), state.TiFlash()); err != nil { + return task.Fail().With("cannot generate tiflash proxy config: %w", err) + } + proxyData, err := encoderProxy.Encode(&proxyConfig) + if err != nil { + return task.Fail().With("tiflash proxy config cannot be encoded: %w", err) + } -func (*TaskConfigMap) Name() string { - return "ConfigMap" -} - -func (t *TaskConfigMap) Sync(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - - flashConfig := tiflashcfg.Config{} - decoder, encoder := toml.Codec[tiflashcfg.Config]() - if err := decoder.Decode([]byte(rtx.TiFlash.Spec.Config), &flashConfig); err != nil { - return task.Fail().With("tiflash config cannot be decoded: %w", err) - } - if err := flashConfig.Overlay(rtx.Cluster, rtx.TiFlash); err != nil { - return task.Fail().With("cannot generate tiflash config: %w", err) - } - flashData, err := encoder.Encode(&flashConfig) - if err != nil { - return task.Fail().With("tiflash config cannot be encoded: %w", err) - } - - proxyConfig := tiflashcfg.ProxyConfig{} - decoderProxy, encoderProxy := toml.Codec[tiflashcfg.ProxyConfig]() - if err = decoderProxy.Decode([]byte(rtx.TiFlash.Spec.ProxyConfig), &proxyConfig); err != nil { - return task.Fail().With("tiflash proxy config cannot be decoded: %w", err) - } - if err = proxyConfig.Overlay(rtx.Cluster, rtx.TiFlash); err != nil { - return task.Fail().With("cannot generate tiflash proxy config: %w", err) - } - proxyData, err := encoderProxy.Encode(&proxyConfig) - if err != nil { - return task.Fail().With("tiflash proxy config cannot be encoded: %w", err) - } - - rtx.ConfigHash, err = hasher.GenerateHash(rtx.TiFlash.Spec.Config) - if err != nil { - return task.Fail().With("failed to generate hash for `tiflash.spec.config`: %w", err) - } - expected := newConfigMap(rtx.TiFlash, flashData, proxyData, rtx.ConfigHash) - if e := t.Client.Apply(rtx, expected); e != nil { - return task.Fail().With("can't create/update cm of tiflash: %w", e) - } - return task.Complete().With("cm is synced") + state.ConfigHash, err = hasher.GenerateHash(state.TiFlash().Spec.Config) + if err != nil { + return task.Fail().With("failed to generate hash for `tiflash.spec.config`: %w", err) + } + expected := newConfigMap(state.TiFlash(), flashData, proxyData, state.ConfigHash) + if e := c.Apply(ctx, expected); e != nil { + return task.Fail().With("can't create/update cm of tiflash: %w", e) + } + return task.Complete().With("cm is synced") + }) } func newConfigMap(tiflash *v1alpha1.TiFlash, flashData, proxyData []byte, hash string) *corev1.ConfigMap { diff --git a/pkg/controllers/tiflash/tasks/ctx.go b/pkg/controllers/tiflash/tasks/ctx.go index 0cb1d26148..7e91bc1571 100644 --- a/pkg/controllers/tiflash/tasks/ctx.go +++ b/pkg/controllers/tiflash/tasks/ctx.go @@ -17,39 +17,27 @@ package tasks import ( "context" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" "github.com/pingcap/kvproto/pkg/metapb" - "github.com/pingcap/tidb-operator/apis/core/v1alpha1" - "github.com/pingcap/tidb-operator/pkg/client" tiflashconfig "github.com/pingcap/tidb-operator/pkg/configs/tiflash" "github.com/pingcap/tidb-operator/pkg/pdapi/v1" pdv1 "github.com/pingcap/tidb-operator/pkg/timanager/apis/pd/v1" pdm "github.com/pingcap/tidb-operator/pkg/timanager/pd" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" ) type ReconcileContext struct { - context.Context - - Key types.NamespacedName + State PDClient pdapi.PDClient - Healthy bool - Store *pdv1.Store StoreID string StoreState string StoreLabels []*metapb.StoreLabel - Cluster *v1alpha1.Cluster - TiFlash *v1alpha1.TiFlash - Pod *corev1.Pod - // ConfigHash stores the hash of **user-specified** config (i.e.`.Spec.Config`), // which will be used to determine whether the config has changed. // This ensures that our config overlay logic will not restart the tidb cluster unexpectedly. @@ -60,102 +48,20 @@ type ReconcileContext struct { PodIsTerminating bool } -func (ctx *ReconcileContext) Self() *ReconcileContext { - return ctx -} - -func TaskContextTiFlash(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("ContextTiFlash", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - - var tiflash v1alpha1.TiFlash - if err := c.Get(ctx, rtx.Key, &tiflash); err != nil { - if !errors.IsNotFound(err) { - return task.Fail().With("can't get tiflash instance: %w", err) - } - - return task.Complete().With("tiflash instance has been deleted") - } - rtx.TiFlash = &tiflash - return task.Complete().With("tiflash is set") - }) -} - -func CondTiFlashHasBeenDeleted() task.Condition[ReconcileContext] { - return task.CondFunc[ReconcileContext](func(ctx task.Context[ReconcileContext]) bool { - return ctx.Self().TiFlash == nil - }) -} - -func CondTiFlashIsDeleting() task.Condition[ReconcileContext] { - return task.CondFunc[ReconcileContext](func(ctx task.Context[ReconcileContext]) bool { - return !ctx.Self().TiFlash.GetDeletionTimestamp().IsZero() - }) -} - -func TaskContextCluster(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("ContextCluster", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - var cluster v1alpha1.Cluster - if err := c.Get(ctx, client.ObjectKey{ - Name: rtx.TiFlash.Spec.Cluster.Name, - Namespace: rtx.TiFlash.Namespace, - }, &cluster); err != nil { - return task.Fail().With("cannot find cluster %s: %w", rtx.TiFlash.Spec.Cluster.Name, err) - } - rtx.Cluster = &cluster - return task.Complete().With("cluster is set") - }) -} - -func CondClusterIsSuspending() task.Condition[ReconcileContext] { - return task.CondFunc[ReconcileContext](func(ctx task.Context[ReconcileContext]) bool { - return ctx.Self().Cluster.ShouldSuspendCompute() - }) -} - -func TaskContextPod(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("ContextPod", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - var pod corev1.Pod - if err := c.Get(ctx, client.ObjectKey{ - Name: rtx.TiFlash.PodName(), - Namespace: rtx.TiFlash.Namespace, - }, &pod); err != nil { - if errors.IsNotFound(err) { - return task.Complete().With("pod is not created") - } - return task.Fail().With("failed to get pod of pd: %w", err) - } - - rtx.Pod = &pod - if !rtx.Pod.GetDeletionTimestamp().IsZero() { - rtx.PodIsTerminating = true - } - return task.Complete().With("pod is set") - }) -} - -func CondClusterIsPaused() task.Condition[ReconcileContext] { - return task.CondFunc[ReconcileContext](func(ctx task.Context[ReconcileContext]) bool { - return ctx.Self().Cluster.ShouldPauseReconcile() - }) -} - -func TaskContextInfoFromPD(cm pdm.PDClientManager) task.Task[ReconcileContext] { - return task.NameTaskFunc("ContextInfoFromPD", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - c, ok := cm.Get(pdm.PrimaryKey(rtx.TiFlash.Namespace, rtx.TiFlash.Spec.Cluster.Name)) +func TaskContextInfoFromPD(state *ReconcileContext, cm pdm.PDClientManager) task.Task { + return task.NameTaskFunc("ContextInfoFromPD", func(context.Context) task.Result { + ck := state.Cluster() + c, ok := cm.Get(pdm.PrimaryKey(ck.Namespace, ck.Name)) if !ok { return task.Complete().With("pd client is not registered") } - rtx.PDClient = c.Underlay() + state.PDClient = c.Underlay() if !c.HasSynced() { return task.Complete().With("store info is not synced, just wait for next sync") } - s, err := c.Stores().Get(tiflashconfig.GetServiceAddr(rtx.TiFlash)) + s, err := c.Stores().Get(tiflashconfig.GetServiceAddr(state.TiFlash())) if err != nil { if !errors.IsNotFound(err) { return task.Fail().With("failed to get store info: %w", err) @@ -163,10 +69,10 @@ func TaskContextInfoFromPD(cm pdm.PDClientManager) task.Task[ReconcileContext] { return task.Complete().With("store does not exist") } - rtx.Store, rtx.StoreID, rtx.StoreState = s, s.ID, string(s.NodeState) - rtx.StoreLabels = make([]*metapb.StoreLabel, len(s.Labels)) + state.Store, state.StoreID, state.StoreState = s, s.ID, string(s.NodeState) + state.StoreLabels = make([]*metapb.StoreLabel, len(s.Labels)) for k, v := range s.Labels { - rtx.StoreLabels = append(rtx.StoreLabels, &metapb.StoreLabel{Key: k, Value: v}) + state.StoreLabels = append(state.StoreLabels, &metapb.StoreLabel{Key: k, Value: v}) } return task.Complete().With("got store info") }) diff --git a/pkg/controllers/tiflash/tasks/finalizer.go b/pkg/controllers/tiflash/tasks/finalizer.go index 92d48717c7..972a475965 100644 --- a/pkg/controllers/tiflash/tasks/finalizer.go +++ b/pkg/controllers/tiflash/tasks/finalizer.go @@ -24,19 +24,18 @@ import ( "github.com/pingcap/tidb-operator/pkg/client" "github.com/pingcap/tidb-operator/pkg/runtime" "github.com/pingcap/tidb-operator/pkg/utils/k8s" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" ) const ( removingWaitInterval = 10 * time.Second ) -func TaskFinalizerDel(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("FinalizerDel", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() +func TaskFinalizerDel(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("FinalizerDel", func(ctx context.Context) task.Result { switch { - case !rtx.Cluster.GetDeletionTimestamp().IsZero(): - wait, err := EnsureSubResourcesDeleted(ctx, c, rtx.TiFlash) + case !state.Cluster().GetDeletionTimestamp().IsZero(): + wait, err := EnsureSubResourcesDeleted(ctx, c, state.TiFlash()) if err != nil { return task.Fail().With("cannot delete sub resources: %w", err) } @@ -46,16 +45,16 @@ func TaskFinalizerDel(c client.Client) task.Task[ReconcileContext] { } // whole cluster is deleting - if err := k8s.RemoveFinalizer(ctx, c, rtx.TiFlash); err != nil { + if err := k8s.RemoveFinalizer(ctx, c, state.TiFlash()); err != nil { return task.Fail().With("cannot remove finalizer: %w", err) } - case rtx.StoreState == v1alpha1.StoreStateRemoving: + case state.StoreState == v1alpha1.StoreStateRemoving: // TODO: Complete task and retrigger reconciliation by polling PD return task.Retry(removingWaitInterval).With("wait until the store is removed") - case rtx.StoreState == v1alpha1.StoreStateRemoved || rtx.StoreID == "": - wait, err := EnsureSubResourcesDeleted(ctx, c, rtx.TiFlash) + case state.StoreState == v1alpha1.StoreStateRemoved || state.StoreID == "": + wait, err := EnsureSubResourcesDeleted(ctx, c, state.TiFlash()) if err != nil { return task.Fail().With("cannot delete sub resources: %w", err) } @@ -65,13 +64,13 @@ func TaskFinalizerDel(c client.Client) task.Task[ReconcileContext] { } // Store ID is empty may because of tiflash is not initialized // TODO: check whether tiflash is initialized - if err := k8s.RemoveFinalizer(ctx, c, rtx.TiFlash); err != nil { + if err := k8s.RemoveFinalizer(ctx, c, state.TiFlash()); err != nil { return task.Fail().With("cannot remove finalizer: %w", err) } default: // get store info successfully and the store still exists - if err := rtx.PDClient.DeleteStore(ctx, rtx.StoreID); err != nil { - return task.Fail().With("cannot delete store %s: %v", rtx.StoreID, err) + if err := state.PDClient.DeleteStore(ctx, state.StoreID); err != nil { + return task.Fail().With("cannot delete store %s: %v", state.StoreID, err) } return task.Retry(removingWaitInterval).With("the store is removing") @@ -80,16 +79,6 @@ func TaskFinalizerDel(c client.Client) task.Task[ReconcileContext] { }) } -func TaskFinalizerAdd(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("FinalizerAdd", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - if err := k8s.EnsureFinalizer(ctx, c, rtx.TiFlash); err != nil { - return task.Fail().With("failed to ensure finalizer has been added: %w", err) - } - return task.Complete().With("finalizer is added") - }) -} - func EnsureSubResourcesDeleted(ctx context.Context, c client.Client, f *v1alpha1.TiFlash) (wait bool, _ error) { wait1, err := k8s.DeleteInstanceSubresource(ctx, c, runtime.FromTiFlash(f), &corev1.PodList{}) if err != nil { diff --git a/pkg/controllers/tiflash/tasks/pod.go b/pkg/controllers/tiflash/tasks/pod.go index 92c14ef408..9ffb0896bc 100644 --- a/pkg/controllers/tiflash/tasks/pod.go +++ b/pkg/controllers/tiflash/tasks/pod.go @@ -15,13 +15,15 @@ package tasks import ( + "context" "fmt" "path/filepath" - "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/go-logr/logr" + "github.com/pingcap/tidb-operator/apis/core/v1alpha1" "github.com/pingcap/tidb-operator/pkg/client" tiflashcfg "github.com/pingcap/tidb-operator/pkg/configs/tiflash" @@ -29,78 +31,49 @@ import ( "github.com/pingcap/tidb-operator/pkg/overlay" "github.com/pingcap/tidb-operator/pkg/utils/k8s" maputil "github.com/pingcap/tidb-operator/pkg/utils/map" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" ) -func TaskPodSuspend(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("PodSuspend", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - if rtx.Pod == nil { - return task.Complete().With("pod has been deleted") +func TaskPod(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("Pod", func(ctx context.Context) task.Result { + logger := logr.FromContextOrDiscard(ctx) + expected := newPod(state.Cluster(), state.TiFlash(), state.ConfigHash) + if state.Pod() == nil { + if err := c.Apply(ctx, expected); err != nil { + return task.Fail().With("can't apply pod of tiflash: %w", err) + } + + state.SetPod(expected) + return task.Complete().With("pod is created") } - if err := c.Delete(rtx, rtx.Pod); err != nil { - return task.Fail().With("can't delete pod of pd: %w", err) - } - rtx.PodIsTerminating = true - return task.Wait().With("pod is deleting") - }) -} -type TaskPod struct { - Client client.Client - Logger logr.Logger -} - -func NewTaskPod(logger logr.Logger, c client.Client) task.Task[ReconcileContext] { - return &TaskPod{ - Client: c, - Logger: logger, - } -} - -func (*TaskPod) Name() string { - return "Pod" -} - -func (t *TaskPod) Sync(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - - expected := t.newPod(rtx.Cluster, rtx.TiFlash, rtx.ConfigHash) - if rtx.Pod == nil { - if err := t.Client.Apply(rtx, expected); err != nil { - return task.Fail().With("can't apply pod of tiflash: %w", err) + res := k8s.ComparePods(state.Pod(), expected) + curHash, expectHash := state.Pod().Labels[v1alpha1.LabelKeyConfigHash], expected.Labels[v1alpha1.LabelKeyConfigHash] + configChanged := curHash != expectHash + logger.Info("compare pod", "result", res, "configChanged", configChanged, "currentConfigHash", curHash, "expectConfigHash", expectHash) + + if res == k8s.CompareResultRecreate || (configChanged && + state.TiFlash().Spec.UpdateStrategy.Config == v1alpha1.ConfigUpdateStrategyRestart) { + logger.Info("will recreate the pod") + if err := c.Delete(ctx, state.Pod()); err != nil { + return task.Fail().With("can't delete pod of tiflash: %w", err) + } + + state.PodIsTerminating = true + return task.Complete().With("pod is deleting") + } else if res == k8s.CompareResultUpdate { + logger.Info("will update the pod in place") + if err := c.Apply(ctx, expected); err != nil { + return task.Fail().With("can't apply pod of tiflash: %w", err) + } + state.SetPod(expected) } - rtx.Pod = expected - return task.Complete().With("pod is created") - } - - res := k8s.ComparePods(rtx.Pod, expected) - curHash, expectHash := rtx.Pod.Labels[v1alpha1.LabelKeyConfigHash], expected.Labels[v1alpha1.LabelKeyConfigHash] - configChanged := curHash != expectHash - t.Logger.Info("compare pod", "result", res, "configChanged", configChanged, "currentConfigHash", curHash, "expectConfigHash", expectHash) - - if res == k8s.CompareResultRecreate || (configChanged && - rtx.TiFlash.Spec.UpdateStrategy.Config == v1alpha1.ConfigUpdateStrategyRestart) { - t.Logger.Info("will recreate the pod") - if err := t.Client.Delete(rtx, rtx.Pod); err != nil { - return task.Fail().With("can't delete pod of tiflash: %w", err) - } - - rtx.PodIsTerminating = true - return task.Complete().With("pod is deleting") - } else if res == k8s.CompareResultUpdate { - t.Logger.Info("will update the pod in place") - if err := t.Client.Apply(rtx, expected); err != nil { - return task.Fail().With("can't apply pod of tiflash: %w", err) - } - rtx.Pod = expected - } - - return task.Complete().With("pod is synced") + return task.Complete().With("pod is synced") + }) } -func (*TaskPod) newPod(cluster *v1alpha1.Cluster, tiflash *v1alpha1.TiFlash, configHash string) *corev1.Pod { +func newPod(cluster *v1alpha1.Cluster, tiflash *v1alpha1.TiFlash, configHash string) *corev1.Pod { vols := []corev1.Volume{ { Name: v1alpha1.VolumeNameConfig, diff --git a/pkg/controllers/tiflash/tasks/pvc.go b/pkg/controllers/tiflash/tasks/pvc.go index 872dee19fb..fd84b9eaa0 100644 --- a/pkg/controllers/tiflash/tasks/pvc.go +++ b/pkg/controllers/tiflash/tasks/pvc.go @@ -15,46 +15,31 @@ package tasks import ( - "github.com/go-logr/logr" + "context" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/go-logr/logr" + "github.com/pingcap/tidb-operator/apis/core/v1alpha1" "github.com/pingcap/tidb-operator/pkg/client" maputil "github.com/pingcap/tidb-operator/pkg/utils/map" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" "github.com/pingcap/tidb-operator/pkg/volumes" ) -type TaskPVC struct { - Client client.Client - Logger logr.Logger - VolumeModifier volumes.Modifier -} - -func NewTaskPVC(logger logr.Logger, c client.Client, vm volumes.Modifier) task.Task[ReconcileContext] { - return &TaskPVC{ - Client: c, - Logger: logger, - VolumeModifier: vm, - } -} - -func (*TaskPVC) Name() string { - return "PVC" -} - -func (t *TaskPVC) Sync(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - - pvcs := newPVCs(rtx.TiFlash) - if wait, err := volumes.SyncPVCs(rtx, t.Client, pvcs, t.VolumeModifier, t.Logger); err != nil { - return task.Fail().With("failed to sync pvcs: %w", err) - } else if wait { - return task.Wait().With("waiting for pvcs to be synced") - } +func TaskPVC(state *ReconcileContext, logger logr.Logger, c client.Client, vm volumes.Modifier) task.Task { + return task.NameTaskFunc("PVC", func(ctx context.Context) task.Result { + pvcs := newPVCs(state.TiFlash()) + if wait, err := volumes.SyncPVCs(ctx, c, pvcs, vm, logger); err != nil { + return task.Fail().With("failed to sync pvcs: %w", err) + } else if wait { + return task.Wait().With("waiting for pvcs to be synced") + } - return task.Complete().With("pvcs are synced") + return task.Complete().With("pvcs are synced") + }) } func newPVCs(tiflash *v1alpha1.TiFlash) []*corev1.PersistentVolumeClaim { diff --git a/pkg/controllers/tiflash/tasks/state.go b/pkg/controllers/tiflash/tasks/state.go new file mode 100644 index 0000000000..0738353eba --- /dev/null +++ b/pkg/controllers/tiflash/tasks/state.go @@ -0,0 +1,98 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tasks + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/pingcap/tidb-operator/apis/core/v1alpha1" + "github.com/pingcap/tidb-operator/pkg/controllers/common" + "github.com/pingcap/tidb-operator/pkg/runtime" +) + +type state struct { + key types.NamespacedName + + cluster *v1alpha1.Cluster + tiflash *v1alpha1.TiFlash + pod *corev1.Pod +} + +type State interface { + common.TiFlashStateInitializer + common.ClusterStateInitializer + common.PodStateInitializer + + common.TiFlashState + common.ClusterState + common.PodState + + common.InstanceState[*runtime.TiFlash] + + SetPod(*corev1.Pod) +} + +func NewState(key types.NamespacedName) State { + s := &state{ + key: key, + } + return s +} + +func (s *state) TiFlash() *v1alpha1.TiFlash { + return s.tiflash +} + +func (s *state) Cluster() *v1alpha1.Cluster { + return s.cluster +} + +func (s *state) Pod() *corev1.Pod { + return s.pod +} + +func (s *state) Instance() *runtime.TiFlash { + return runtime.FromTiFlash(s.tiflash) +} + +func (s *state) SetPod(pod *corev1.Pod) { + s.pod = pod +} + +func (s *state) TiFlashInitializer() common.TiFlashInitializer { + return common.NewResource(func(tiflash *v1alpha1.TiFlash) { s.tiflash = tiflash }). + WithNamespace(common.Namespace(s.key.Namespace)). + WithName(common.Name(s.key.Name)). + Initializer() +} + +func (s *state) ClusterInitializer() common.ClusterInitializer { + return common.NewResource(func(cluster *v1alpha1.Cluster) { s.cluster = cluster }). + WithNamespace(common.Namespace(s.key.Namespace)). + WithName(common.Lazy[string](func() string { + return s.tiflash.Spec.Cluster.Name + })). + Initializer() +} + +func (s *state) PodInitializer() common.PodInitializer { + return common.NewResource(s.SetPod). + WithNamespace(common.Namespace(s.key.Namespace)). + WithName(common.Lazy[string](func() string { + return s.tiflash.PodName() + })). + Initializer() +} diff --git a/pkg/controllers/tiflash/tasks/status.go b/pkg/controllers/tiflash/tasks/status.go index 38e44768f2..9b3591f18a 100644 --- a/pkg/controllers/tiflash/tasks/status.go +++ b/pkg/controllers/tiflash/tasks/status.go @@ -15,158 +15,108 @@ package tasks import ( - "github.com/go-logr/logr" + "context" + "time" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/pingcap/tidb-operator/apis/core/v1alpha1" "github.com/pingcap/tidb-operator/pkg/client" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" "github.com/pingcap/tidb-operator/third_party/kubernetes/pkg/controller/statefulset" ) -func TaskStatusSuspend(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("StatusSuspend", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - rtx.TiFlash.Status.ObservedGeneration = rtx.TiFlash.Generation +const ( + defaultTaskWaitDuration = 5 * time.Second +) - var ( - suspendStatus = metav1.ConditionFalse - suspendMessage = "tiflash is suspending" +// TODO(liubo02): extract to common task +// +//nolint:gocyclo // refactor is possible +func TaskStatus(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("Status", func(ctx context.Context) task.Result { + needUpdate := false + tiflash := state.TiFlash() + pod := state.Pod() + // TODO(liubo02): simplify it + var healthy bool + if pod != nil && + statefulset.IsPodRunningAndReady(pod) && + !state.PodIsTerminating && + state.Store.NodeState == v1alpha1.StoreStateServing { + healthy = true + } + needUpdate = syncHealthCond(tiflash, healthy) || needUpdate + needUpdate = syncSuspendCond(tiflash) || needUpdate + needUpdate = SetIfChanged(&tiflash.Status.ID, state.StoreID) || needUpdate + needUpdate = SetIfChanged(&tiflash.Status.State, state.StoreState) || needUpdate - // when suspending, the health status should be false - healthStatus = metav1.ConditionFalse - healthMessage = "tiflash is not healthy" - ) + needUpdate = SetIfChanged(&tiflash.Status.ObservedGeneration, tiflash.Generation) || needUpdate + needUpdate = SetIfChanged(&tiflash.Status.UpdateRevision, tiflash.Labels[v1alpha1.LabelKeyInstanceRevisionHash]) || needUpdate - if rtx.Pod == nil { - suspendStatus = metav1.ConditionTrue - suspendMessage = "tiflash is suspended" + if healthy { + needUpdate = SetIfChanged(&tiflash.Status.CurrentRevision, pod.Labels[v1alpha1.LabelKeyInstanceRevisionHash]) || needUpdate } - needUpdate := meta.SetStatusCondition(&rtx.TiFlash.Status.Conditions, metav1.Condition{ - Type: v1alpha1.TiFlashCondSuspended, - Status: suspendStatus, - ObservedGeneration: rtx.TiFlash.Generation, - // TODO: use different reason for suspending and suspended - Reason: v1alpha1.TiFlashSuspendReason, - Message: suspendMessage, - }) - - needUpdate = meta.SetStatusCondition(&rtx.TiFlash.Status.Conditions, metav1.Condition{ - Type: v1alpha1.TiFlashCondHealth, - Status: healthStatus, - ObservedGeneration: rtx.TiFlash.Generation, - Reason: v1alpha1.TiFlashHealthReason, - Message: healthMessage, - }) || needUpdate if needUpdate { - if err := c.Status().Update(ctx, rtx.TiFlash); err != nil { + if err := c.Status().Update(ctx, tiflash); err != nil { return task.Fail().With("cannot update status: %w", err) } } - return task.Complete().With("status of suspend tiflash is updated") - }) -} - -type TaskStatus struct { - Client client.Client - Logger logr.Logger -} - -func NewTaskStatus(logger logr.Logger, c client.Client) task.Task[ReconcileContext] { - return &TaskStatus{ - Client: c, - Logger: logger, - } -} + // TODO: use a condition to refactor it + if tiflash.Status.ID == "" || tiflash.Status.State != v1alpha1.StoreStateServing || !v1alpha1.IsUpToDate(tiflash) { + // can we only rely on the PD member events for this condition? + // TODO(liubo02): change to task.Wait + return task.Retry(defaultTaskWaitDuration).With("tiflash may not be initialized, retry") + } -func (*TaskStatus) Name() string { - return "Status" + return task.Complete().With("status is synced") + }) } -//nolint:gocyclo // refactor is possible -func (t *TaskStatus) Sync(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - +func syncHealthCond(tiflash *v1alpha1.TiFlash, healthy bool) bool { var ( - healthStatus = metav1.ConditionFalse - healthMessage = "tiflash is not healthy" - - suspendStatus = metav1.ConditionFalse - suspendMessage = "tiflash is not suspended" - - needUpdate = false + status = metav1.ConditionFalse + reason = "Unhealthy" + msg = "instance is not healthy" ) - - if rtx.StoreID != "" { - if rtx.TiFlash.Status.ID != rtx.StoreID { - rtx.TiFlash.Status.ID = rtx.StoreID - needUpdate = true - } - - info, err := rtx.PDClient.GetStore(ctx, rtx.StoreID) - if err == nil && info != nil && info.Store != nil { - rtx.StoreState = info.Store.NodeState.String() - } else { - t.Logger.Error(err, "failed to get tiflash store info", "store", rtx.StoreID) - } - } - if rtx.StoreState != "" && rtx.TiFlash.Status.State != rtx.StoreState { - rtx.TiFlash.Status.State = rtx.StoreState - needUpdate = true + if healthy { + status = metav1.ConditionTrue + reason = "Healthy" + msg = "instance is healthy" } - needUpdate = meta.SetStatusCondition(&rtx.TiFlash.Status.Conditions, metav1.Condition{ - Type: v1alpha1.TiFlashCondSuspended, - Status: suspendStatus, - ObservedGeneration: rtx.TiFlash.Generation, - Reason: v1alpha1.TiFlashSuspendReason, - Message: suspendMessage, - }) || needUpdate - - if needUpdate || !v1alpha1.IsReconciled(rtx.TiFlash) || - rtx.TiFlash.Status.UpdateRevision != rtx.TiFlash.Labels[v1alpha1.LabelKeyInstanceRevisionHash] { - rtx.TiFlash.Status.ObservedGeneration = rtx.TiFlash.Generation - rtx.TiFlash.Status.UpdateRevision = rtx.TiFlash.Labels[v1alpha1.LabelKeyInstanceRevisionHash] - needUpdate = true - } + return meta.SetStatusCondition(&tiflash.Status.Conditions, metav1.Condition{ + Type: v1alpha1.CondHealth, + Status: status, + ObservedGeneration: tiflash.Generation, + Reason: reason, + Message: msg, + }) +} - if rtx.Pod == nil || rtx.PodIsTerminating { - rtx.Healthy = false - } else if statefulset.IsPodRunningAndReady(rtx.Pod) && rtx.StoreState == v1alpha1.StoreStateServing { - rtx.Healthy = true - if rtx.TiFlash.Status.CurrentRevision != rtx.Pod.Labels[v1alpha1.LabelKeyInstanceRevisionHash] { - rtx.TiFlash.Status.CurrentRevision = rtx.Pod.Labels[v1alpha1.LabelKeyInstanceRevisionHash] - needUpdate = true - } - } else { - rtx.Healthy = false - } +func syncSuspendCond(tiflash *v1alpha1.TiFlash) bool { + // always set it as unsuspended + return meta.SetStatusCondition(&tiflash.Status.Conditions, metav1.Condition{ + Type: v1alpha1.CondSuspended, + Status: metav1.ConditionFalse, + ObservedGeneration: tiflash.Generation, + Reason: v1alpha1.ReasonUnsuspended, + Message: "instace is not suspended", + }) +} - if rtx.Healthy { - healthStatus = metav1.ConditionTrue - healthMessage = "tiflash is healthy" - } - needUpdate = meta.SetStatusCondition(&rtx.TiFlash.Status.Conditions, metav1.Condition{ - Type: v1alpha1.TiFlashCondHealth, - Status: healthStatus, - ObservedGeneration: rtx.TiFlash.Generation, - Reason: v1alpha1.TiFlashHealthReason, - Message: healthMessage, - }) || needUpdate - - if needUpdate { - if err := t.Client.Status().Update(ctx, rtx.TiFlash); err != nil { - return task.Fail().With("cannot update status: %w", err) - } +// TODO: move to utils +func SetIfChanged[T comparable](dst *T, src T) bool { + if src == *new(T) { + return false } - - // TODO: use a condition to refactor it - if rtx.TiFlash.Status.ID == "" || rtx.TiFlash.Status.State != v1alpha1.StoreStateServing || !v1alpha1.IsUpToDate(rtx.TiFlash) { - return task.Fail().With("tiflash may not be initialized, retry") + if *dst != src { + *dst = src + return true } - return task.Complete().With("status is synced") + return false } diff --git a/pkg/controllers/tiflash/tasks/store_labels.go b/pkg/controllers/tiflash/tasks/store_labels.go index 3101171b85..4515b9ac01 100644 --- a/pkg/controllers/tiflash/tasks/store_labels.go +++ b/pkg/controllers/tiflash/tasks/store_labels.go @@ -15,6 +15,7 @@ package tasks import ( + "context" "reflect" "strconv" @@ -25,71 +26,56 @@ import ( "github.com/pingcap/tidb-operator/apis/core/v1alpha1" "github.com/pingcap/tidb-operator/pkg/client" "github.com/pingcap/tidb-operator/pkg/utils/k8s" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" ) -type TaskStoreLabels struct { - Client client.Client - Logger logr.Logger -} - -func NewTaskStoreLabels(logger logr.Logger, c client.Client) task.Task[ReconcileContext] { - return &TaskStoreLabels{ - Client: c, - Logger: logger, - } -} - -func (*TaskStoreLabels) Name() string { - return "StoreLabels" -} - -func (t *TaskStoreLabels) Sync(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - - if rtx.StoreState != v1alpha1.StoreStateServing || rtx.PodIsTerminating || rtx.Pod == nil { - return task.Complete().With("skip sync store labels as the store is not serving") - } - - nodeName := rtx.Pod.Spec.NodeName - if nodeName == "" { - return task.Fail().With("pod %s/%s has not been scheduled", rtx.Pod.Namespace, rtx.Pod.Name) - } - - var node corev1.Node - if err := t.Client.Get(ctx, client.ObjectKey{Name: nodeName}, &node); err != nil { - return task.Fail().With("failed to get node %s: %s", nodeName, err) - } +func TaskStoreLabels(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("StoreLabels", func(ctx context.Context) task.Result { + logger := logr.FromContextOrDiscard(ctx) + if state.StoreState != v1alpha1.StoreStateServing || state.PodIsTerminating || state.Pod() == nil { + return task.Complete().With("skip sync store labels as the store is not serving") + } - // TODO: too many API calls to PD? - pdCfg, err := rtx.PDClient.GetConfig(ctx) - if err != nil { - return task.Fail().With("failed to get pd config: %s", err) - } - keys := pdCfg.Replication.LocationLabels - if len(keys) == 0 { - return task.Complete().With("no store labels need to sync") - } + nodeName := state.Pod().Spec.NodeName + if nodeName == "" { + return task.Fail().With("pod %s/%s has not been scheduled", state.Pod().Namespace, state.Pod().Name) + } - storeLabels := k8s.GetNodeLabelsForKeys(&node, keys) - if len(storeLabels) == 0 { - return task.Complete().With("no store labels from node %s to sync", nodeName) - } + var node corev1.Node + if err := c.Get(ctx, client.ObjectKey{Name: nodeName}, &node); err != nil { + return task.Fail().With("failed to get node %s: %s", nodeName, err) + } - if !storeLabelsEqualNodeLabels(rtx.StoreLabels, storeLabels) { - storeID, err := strconv.ParseUint(rtx.StoreID, 10, 64) + // TODO: too many API calls to PD? + pdCfg, err := state.PDClient.GetConfig(ctx) if err != nil { - return task.Fail().With("failed to parse store id %s: %s", rtx.StoreID, err) + return task.Fail().With("failed to get pd config: %s", err) } - set, err := rtx.PDClient.SetStoreLabels(ctx, storeID, storeLabels) - if err != nil { - return task.Fail().With("failed to set store labels: %s", err) - } else if set { - t.Logger.Info("store labels synced", "storeID", rtx.StoreID, "storeLabels", storeLabels) + keys := pdCfg.Replication.LocationLabels + if len(keys) == 0 { + return task.Complete().With("no store labels need to sync") + } + + storeLabels := k8s.GetNodeLabelsForKeys(&node, keys) + if len(storeLabels) == 0 { + return task.Complete().With("no store labels from node %s to sync", nodeName) + } + + if !storeLabelsEqualNodeLabels(state.StoreLabels, storeLabels) { + storeID, err := strconv.ParseUint(state.StoreID, 10, 64) + if err != nil { + return task.Fail().With("failed to parse store id %s: %s", state.StoreID, err) + } + set, err := state.PDClient.SetStoreLabels(ctx, storeID, storeLabels) + if err != nil { + return task.Fail().With("failed to set store labels: %s", err) + } else if set { + logger.Info("store labels synced", "storeID", state.StoreID, "storeLabels", storeLabels) + } } - } - return task.Complete().With("store labels synced") + return task.Complete().With("store labels synced") + }) } func storeLabelsEqualNodeLabels(storeLabels []*metapb.StoreLabel, nodeLabels map[string]string) bool { diff --git a/pkg/controllers/tikv/builder.go b/pkg/controllers/tikv/builder.go index 8d8616641f..e18b8e89a6 100644 --- a/pkg/controllers/tikv/builder.go +++ b/pkg/controllers/tikv/builder.go @@ -15,45 +15,49 @@ package tikv import ( + "github.com/pingcap/tidb-operator/pkg/controllers/common" "github.com/pingcap/tidb-operator/pkg/controllers/tikv/tasks" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/runtime" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" ) -func (r *Reconciler) NewRunner(reporter task.TaskReporter) task.TaskRunner[tasks.ReconcileContext] { +func (r *Reconciler) NewRunner(state *tasks.ReconcileContext, reporter task.TaskReporter) task.TaskRunner { runner := task.NewTaskRunner(reporter, // get tikv - tasks.TaskContextTiKV(r.Client), + common.TaskContextTiKV(state, r.Client), // if it's deleted just return - task.NewSwitchTask(tasks.CondTiKVHasBeenDeleted()), + task.IfBreak(common.CondInstanceHasBeenDeleted(state)), // get cluster info, FinalizerDel will use it - tasks.TaskContextCluster(r.Client), + common.TaskContextCluster(state, r.Client), + + // check whether it's paused + task.IfBreak(common.CondClusterIsPaused(state)), + // get info from pd - tasks.TaskContextInfoFromPD(r.PDClientManager), + tasks.TaskContextInfoFromPD(state, r.PDClientManager), - task.NewSwitchTask(tasks.CondTiKVIsDeleting(), - tasks.TaskFinalizerDel(r.Client), + task.IfBreak(common.CondInstanceIsDeleting(state), + tasks.TaskFinalizerDel(state, r.Client), ), - - // check whether it's paused - task.NewSwitchTask(tasks.CondClusterIsPaused()), + common.TaskInstanceFinalizerAdd[runtime.TiKVTuple](state, r.Client), // get pod and check whether the cluster is suspending - tasks.TaskContextPod(r.Client), - task.NewSwitchTask(tasks.CondClusterIsSuspending(), - tasks.TaskFinalizerAdd(r.Client), - tasks.TaskPodSuspend(r.Client), - tasks.TaskStatusSuspend(r.Client), + common.TaskContextPod(state, r.Client), + task.IfBreak(common.CondClusterIsSuspending(state), + // NOTE: suspend tikv pod should delete with grace peroid + // TODO(liubo02): combine with the common one + tasks.TaskSuspendPod(state, r.Client), + common.TaskInstanceStatusSuspend[runtime.TiKVTuple](state, r.Client), ), // normal process - tasks.TaskFinalizerAdd(r.Client), - tasks.NewTaskConfigMap(r.Logger, r.Client), - tasks.NewTaskPVC(r.Logger, r.Client, r.VolumeModifier), - tasks.NewTaskPod(r.Logger, r.Client), - tasks.NewTaskStoreLabels(r.Logger, r.Client), - tasks.NewTaskEvictLeader(r.Logger, r.Client), - tasks.NewTaskStatus(r.Logger, r.Client), + tasks.TaskConfigMap(state, r.Client), + tasks.TaskPVC(state, r.Logger, r.Client, r.VolumeModifier), + tasks.TaskPod(state, r.Client), + tasks.TaskStoreLabels(state, r.Client), + tasks.TaskEvictLeader(state), + tasks.TaskStatus(state, r.Client), ) return runner diff --git a/pkg/controllers/tikv/controller.go b/pkg/controllers/tikv/controller.go index 08c8578388..03d1d3761d 100644 --- a/pkg/controllers/tikv/controller.go +++ b/pkg/controllers/tikv/controller.go @@ -30,7 +30,7 @@ import ( pdv1 "github.com/pingcap/tidb-operator/pkg/timanager/apis/pd/v1" pdm "github.com/pingcap/tidb-operator/pkg/timanager/pd" "github.com/pingcap/tidb-operator/pkg/utils/k8s" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" "github.com/pingcap/tidb-operator/pkg/volumes" ) @@ -72,11 +72,10 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu }() rtx := &tasks.ReconcileContext{ - // some fields will be set in the context task - Context: ctx, - Key: req.NamespacedName, + State: tasks.NewState(req.NamespacedName), } - runner := r.NewRunner(reporter) - return runner.Run(rtx) + runner := r.NewRunner(rtx, reporter) + + return runner.Run(ctx) } diff --git a/pkg/controllers/tikv/tasks/cm.go b/pkg/controllers/tikv/tasks/cm.go index 23b0540e65..de0a12f0bb 100644 --- a/pkg/controllers/tikv/tasks/cm.go +++ b/pkg/controllers/tikv/tasks/cm.go @@ -15,7 +15,8 @@ package tasks import ( - "github.com/go-logr/logr" + "context" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -24,52 +25,36 @@ import ( tikvcfg "github.com/pingcap/tidb-operator/pkg/configs/tikv" "github.com/pingcap/tidb-operator/pkg/utils/hasher" maputil "github.com/pingcap/tidb-operator/pkg/utils/map" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" "github.com/pingcap/tidb-operator/pkg/utils/toml" ) -type TaskConfigMap struct { - Client client.Client - Logger logr.Logger -} - -func NewTaskConfigMap(logger logr.Logger, c client.Client) task.Task[ReconcileContext] { - return &TaskConfigMap{ - Client: c, - Logger: logger, - } -} - -func (*TaskConfigMap) Name() string { - return "ConfigMap" -} - -func (t *TaskConfigMap) Sync(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() +func TaskConfigMap(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("ConfigMap", func(ctx context.Context) task.Result { + cfg := tikvcfg.Config{} + decoder, encoder := toml.Codec[tikvcfg.Config]() + if err := decoder.Decode([]byte(state.TiKV().Spec.Config), &cfg); err != nil { + return task.Fail().With("tikv config cannot be decoded: %w", err) + } + if err := cfg.Overlay(state.Cluster(), state.TiKV()); err != nil { + return task.Fail().With("cannot generate tikv config: %w", err) + } - c := tikvcfg.Config{} - decoder, encoder := toml.Codec[tikvcfg.Config]() - if err := decoder.Decode([]byte(rtx.TiKV.Spec.Config), &c); err != nil { - return task.Fail().With("tikv config cannot be decoded: %w", err) - } - if err := c.Overlay(rtx.Cluster, rtx.TiKV); err != nil { - return task.Fail().With("cannot generate tikv config: %w", err) - } + data, err := encoder.Encode(&cfg) + if err != nil { + return task.Fail().With("tikv config cannot be encoded: %w", err) + } - data, err := encoder.Encode(&c) - if err != nil { - return task.Fail().With("tikv config cannot be encoded: %w", err) - } - - rtx.ConfigHash, err = hasher.GenerateHash(rtx.TiKV.Spec.Config) - if err != nil { - return task.Fail().With("failed to generate hash for `tikv.spec.config`: %w", err) - } - expected := newConfigMap(rtx.TiKV, data, rtx.ConfigHash) - if e := t.Client.Apply(rtx, expected); e != nil { - return task.Fail().With("can't create/update cm of tikv: %w", e) - } - return task.Complete().With("cm is synced") + state.ConfigHash, err = hasher.GenerateHash(state.TiKV().Spec.Config) + if err != nil { + return task.Fail().With("failed to generate hash for `tikv.spec.config`: %w", err) + } + expected := newConfigMap(state.TiKV(), data, state.ConfigHash) + if e := c.Apply(ctx, expected); e != nil { + return task.Fail().With("can't create/update cm of tikv: %w", e) + } + return task.Complete().With("cm is synced") + }) } func newConfigMap(tikv *v1alpha1.TiKV, data []byte, hash string) *corev1.ConfigMap { diff --git a/pkg/controllers/tikv/tasks/ctx.go b/pkg/controllers/tikv/tasks/ctx.go index 3bb66282b6..c938a0f04b 100644 --- a/pkg/controllers/tikv/tasks/ctx.go +++ b/pkg/controllers/tikv/tasks/ctx.go @@ -17,39 +17,25 @@ package tasks import ( "context" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "github.com/pingcap/tidb-operator/apis/core/v1alpha1" - "github.com/pingcap/tidb-operator/pkg/client" kvcfg "github.com/pingcap/tidb-operator/pkg/configs/tikv" "github.com/pingcap/tidb-operator/pkg/pdapi/v1" pdv1 "github.com/pingcap/tidb-operator/pkg/timanager/apis/pd/v1" pdm "github.com/pingcap/tidb-operator/pkg/timanager/pd" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" ) type ReconcileContext struct { - context.Context - - Key types.NamespacedName + State PDClient pdapi.PDClient - Healthy bool - StoreExists bool StoreID string StoreState string LeaderEvicting bool - Suspended bool - - Cluster *v1alpha1.Cluster - TiKV *v1alpha1.TiKV - Pod *corev1.Pod - Store *pdv1.Store // ConfigHash stores the hash of **user-specified** config (i.e.`.Spec.Config`), @@ -62,121 +48,40 @@ type ReconcileContext struct { PodIsTerminating bool } -func (ctx *ReconcileContext) Self() *ReconcileContext { - return ctx -} - -func TaskContextTiKV(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("ContextTiKV", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - var tikv v1alpha1.TiKV - if err := c.Get(ctx, rtx.Key, &tikv); err != nil { - if !errors.IsNotFound(err) { - return task.Fail().With("can't get tikv instance: %w", err) - } - - return task.Complete().With("tikv instance has been deleted") - } - rtx.TiKV = &tikv - return task.Complete().With("tikv is set") - }) -} - -func TaskContextInfoFromPD(cm pdm.PDClientManager) task.Task[ReconcileContext] { - return task.NameTaskFunc("ContextInfoFromPD", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - c, ok := cm.Get(pdm.PrimaryKey(rtx.TiKV.Namespace, rtx.TiKV.Spec.Cluster.Name)) +func TaskContextInfoFromPD(state *ReconcileContext, cm pdm.PDClientManager) task.Task { + return task.NameTaskFunc("ContextInfoFromPD", func(ctx context.Context) task.Result { + ck := state.Cluster() + c, ok := cm.Get(pdm.PrimaryKey(ck.Namespace, ck.Name)) if !ok { return task.Complete().With("pd client is not registered") } - rtx.PDClient = c.Underlay() + state.PDClient = c.Underlay() if !c.HasSynced() { return task.Complete().With("store info is not synced, just wait for next sync") } - s, err := c.Stores().Get(kvcfg.GetAdvertiseClientURLs(rtx.TiKV)) + s, err := c.Stores().Get(kvcfg.GetAdvertiseClientURLs(state.TiKV())) if err != nil { if !errors.IsNotFound(err) { return task.Fail().With("failed to get store info: %w", err) } return task.Complete().With("store does not exist") } - rtx.Store, rtx.StoreID, rtx.StoreState = s, s.ID, string(s.NodeState) + state.Store, state.StoreID, state.StoreState = s, s.ID, string(s.NodeState) // TODO: cache evict leader scheduler info, then we don't need to check suspend here - if rtx.Cluster.ShouldSuspendCompute() { + if state.Cluster().ShouldSuspendCompute() { return task.Complete().With("cluster is suspending") } - scheduler, err := rtx.PDClient.GetEvictLeaderScheduler(ctx, rtx.StoreID) + scheduler, err := state.PDClient.GetEvictLeaderScheduler(ctx, state.StoreID) if err != nil { return task.Fail().With("pd is unexpectedly crashed: %w", err) } if scheduler != "" { - rtx.LeaderEvicting = true + state.LeaderEvicting = true } return task.Complete().With("get store info") }) } - -func TaskContextCluster(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("ContextCluster", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - var cluster v1alpha1.Cluster - if err := c.Get(ctx, client.ObjectKey{ - Name: rtx.TiKV.Spec.Cluster.Name, - Namespace: rtx.TiKV.Namespace, - }, &cluster); err != nil { - return task.Fail().With("cannot find cluster %s: %w", rtx.TiKV.Spec.Cluster.Name, err) - } - rtx.Cluster = &cluster - return task.Complete().With("cluster is set") - }) -} - -func TaskContextPod(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("ContextPod", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - var pod corev1.Pod - if err := c.Get(ctx, client.ObjectKey{ - Name: rtx.TiKV.PodName(), - Namespace: rtx.TiKV.Namespace, - }, &pod); err != nil { - if errors.IsNotFound(err) { - return task.Complete().With("pod is not created") - } - return task.Fail().With("failed to get pod of tikv: %w", err) - } - - rtx.Pod = &pod - if !rtx.Pod.GetDeletionTimestamp().IsZero() { - rtx.PodIsTerminating = true - } - return task.Complete().With("pod is set") - }) -} - -func CondTiKVHasBeenDeleted() task.Condition[ReconcileContext] { - return task.CondFunc[ReconcileContext](func(ctx task.Context[ReconcileContext]) bool { - return ctx.Self().TiKV == nil - }) -} - -func CondTiKVIsDeleting() task.Condition[ReconcileContext] { - return task.CondFunc[ReconcileContext](func(ctx task.Context[ReconcileContext]) bool { - return !ctx.Self().TiKV.GetDeletionTimestamp().IsZero() - }) -} - -func CondClusterIsPaused() task.Condition[ReconcileContext] { - return task.CondFunc[ReconcileContext](func(ctx task.Context[ReconcileContext]) bool { - return ctx.Self().Cluster.ShouldPauseReconcile() - }) -} - -func CondClusterIsSuspending() task.Condition[ReconcileContext] { - return task.CondFunc[ReconcileContext](func(ctx task.Context[ReconcileContext]) bool { - return ctx.Self().Cluster.ShouldSuspendCompute() - }) -} diff --git a/pkg/controllers/tikv/tasks/evict_leader.go b/pkg/controllers/tikv/tasks/evict_leader.go index 1abde936ae..1f91d43750 100644 --- a/pkg/controllers/tikv/tasks/evict_leader.go +++ b/pkg/controllers/tikv/tasks/evict_leader.go @@ -15,47 +15,30 @@ package tasks import ( - "github.com/go-logr/logr" + "context" - "github.com/pingcap/tidb-operator/pkg/client" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" ) -type TaskEvictLeader struct { - Client client.Client - Logger logr.Logger -} - -func NewTaskEvictLeader(logger logr.Logger, c client.Client) task.Task[ReconcileContext] { - return &TaskEvictLeader{ - Client: c, - Logger: logger, - } -} - -func (*TaskEvictLeader) Name() string { - return "EvictLeader" -} - -func (*TaskEvictLeader) Sync(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - - switch { - case rtx.Store == nil: - return task.Complete().With("store has been deleted or not created") - case rtx.PodIsTerminating: - if !rtx.LeaderEvicting { - if err := rtx.PDClient.BeginEvictLeader(ctx, rtx.StoreID); err != nil { - return task.Fail().With("cannot add evict leader scheduler: %v", err) +func TaskEvictLeader(state *ReconcileContext) task.Task { + return task.NameTaskFunc("EvictLeader", func(ctx context.Context) task.Result { + switch { + case state.Store == nil: + return task.Complete().With("store has been deleted or not created") + case state.PodIsTerminating: + if !state.LeaderEvicting { + if err := state.PDClient.BeginEvictLeader(ctx, state.StoreID); err != nil { + return task.Fail().With("cannot add evict leader scheduler: %v", err) + } } - } - return task.Complete().With("ensure evict leader scheduler exists") - default: - if rtx.LeaderEvicting { - if err := rtx.PDClient.EndEvictLeader(ctx, rtx.StoreID); err != nil { - return task.Fail().With("cannot remove evict leader scheduler: %v", err) + return task.Complete().With("ensure evict leader scheduler exists") + default: + if state.LeaderEvicting { + if err := state.PDClient.EndEvictLeader(ctx, state.StoreID); err != nil { + return task.Fail().With("cannot remove evict leader scheduler: %v", err) + } } + return task.Complete().With("ensure evict leader scheduler doesn't exist") } - return task.Complete().With("ensure evict leader scheduler doesn't exist") - } + }) } diff --git a/pkg/controllers/tikv/tasks/finalizer.go b/pkg/controllers/tikv/tasks/finalizer.go index cca2333525..1c6c60b763 100644 --- a/pkg/controllers/tikv/tasks/finalizer.go +++ b/pkg/controllers/tikv/tasks/finalizer.go @@ -24,23 +24,22 @@ import ( "github.com/pingcap/tidb-operator/pkg/client" "github.com/pingcap/tidb-operator/pkg/runtime" "github.com/pingcap/tidb-operator/pkg/utils/k8s" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" ) const ( removingWaitInterval = 10 * time.Second ) -func TaskFinalizerDel(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("FinalizerDel", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() +func TaskFinalizerDel(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("FinalizerDel", func(ctx context.Context) task.Result { regionCount := 0 - if rtx.Store != nil { - regionCount = rtx.Store.RegionCount + if state.Store != nil { + regionCount = state.Store.RegionCount } switch { - case !rtx.Cluster.GetDeletionTimestamp().IsZero(): - wait, err := EnsureSubResourcesDeleted(ctx, c, rtx.TiKV, regionCount) + case !state.Cluster().GetDeletionTimestamp().IsZero(): + wait, err := EnsureSubResourcesDeleted(ctx, c, state.TiKV(), regionCount) if err != nil { return task.Fail().With("cannot delete subresources: %w", err) } @@ -49,15 +48,15 @@ func TaskFinalizerDel(c client.Client) task.Task[ReconcileContext] { } // whole cluster is deleting - if err := k8s.RemoveFinalizer(ctx, c, rtx.TiKV); err != nil { + if err := k8s.RemoveFinalizer(ctx, c, state.TiKV()); err != nil { return task.Fail().With("cannot remove finalizer: %w", err) } - case rtx.StoreState == v1alpha1.StoreStateRemoving: + case state.StoreState == v1alpha1.StoreStateRemoving: // TODO: Complete task and retrigger reconciliation by polling PD return task.Retry(removingWaitInterval).With("wait until the store is removed") - case rtx.StoreState == v1alpha1.StoreStateRemoved || rtx.StoreID == "": - wait, err := EnsureSubResourcesDeleted(ctx, c, rtx.TiKV, regionCount) + case state.StoreState == v1alpha1.StoreStateRemoved || state.StoreID == "": + wait, err := EnsureSubResourcesDeleted(ctx, c, state.TiKV(), regionCount) if err != nil { return task.Fail().With("cannot delete subresources: %w", err) } @@ -66,13 +65,13 @@ func TaskFinalizerDel(c client.Client) task.Task[ReconcileContext] { } // Store ID is empty may because of tikv is not initialized // TODO: check whether tikv is initialized - if err := k8s.RemoveFinalizer(ctx, c, rtx.TiKV); err != nil { + if err := k8s.RemoveFinalizer(ctx, c, state.TiKV()); err != nil { return task.Fail().With("cannot remove finalizer: %w", err) } default: // get store info successfully and the store still exists - if err := rtx.PDClient.DeleteStore(ctx, rtx.StoreID); err != nil { - return task.Fail().With("cannot delete store %s: %v", rtx.StoreID, err) + if err := state.PDClient.DeleteStore(ctx, state.StoreID); err != nil { + return task.Fail().With("cannot delete store %s: %v", state.StoreID, err) } return task.Retry(removingWaitInterval).With("the store is removing") @@ -82,17 +81,6 @@ func TaskFinalizerDel(c client.Client) task.Task[ReconcileContext] { }) } -func TaskFinalizerAdd(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("FinalizerAdd", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - if err := k8s.EnsureFinalizer(ctx, c, rtx.TiKV); err != nil { - return task.Fail().With("failed to ensure finalizer has been added: %w", err) - } - - return task.Complete().With("finalizer is added") - }) -} - func EnsureSubResourcesDeleted(ctx context.Context, c client.Client, tikv *v1alpha1.TiKV, regionCount int) (wait bool, _ error) { gracePeriod := CalcGracePeriod(regionCount) wait1, err := k8s.DeleteInstanceSubresource(ctx, c, runtime.FromTiKV(tikv), &corev1.PodList{}, client.GracePeriodSeconds(gracePeriod)) diff --git a/pkg/controllers/tikv/tasks/pod.go b/pkg/controllers/tikv/tasks/pod.go index 4fa27d5d62..d77fb85430 100644 --- a/pkg/controllers/tikv/tasks/pod.go +++ b/pkg/controllers/tikv/tasks/pod.go @@ -15,14 +15,16 @@ package tasks import ( + "context" "path/filepath" "strings" - "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" + "github.com/go-logr/logr" + "github.com/pingcap/tidb-operator/apis/core/v1alpha1" "github.com/pingcap/tidb-operator/pkg/client" kvcfg "github.com/pingcap/tidb-operator/pkg/configs/tikv" @@ -30,7 +32,7 @@ import ( "github.com/pingcap/tidb-operator/pkg/overlay" "github.com/pingcap/tidb-operator/pkg/utils/k8s" maputil "github.com/pingcap/tidb-operator/pkg/utils/map" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" ) const ( @@ -39,99 +41,83 @@ const ( RegionsPerSecond = 200 ) -func TaskPodSuspend(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("PodSuspend", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - if rtx.Pod == nil { +func TaskSuspendPod(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("PodSuspend", func(ctx context.Context) task.Result { + if state.Pod() == nil { return task.Complete().With("pod has been deleted") } regionCount := 0 - if rtx.Store != nil { - regionCount = rtx.Store.RegionCount + if state.Store != nil { + regionCount = state.Store.RegionCount } - if err := DeletePodWithGracePeriod(rtx, c, rtx.Pod, regionCount); err != nil { + if err := DeletePodWithGracePeriod(ctx, c, state.Pod(), regionCount); err != nil { return task.Fail().With("can't delete pod of tikv: %w", err) } - rtx.PodIsTerminating = true + state.PodIsTerminating = true return task.Wait().With("pod is deleting") }) } -type TaskPod struct { - Client client.Client - Logger logr.Logger -} - -func NewTaskPod(logger logr.Logger, c client.Client) task.Task[ReconcileContext] { - return &TaskPod{ - Client: c, - Logger: logger, - } -} - -func (*TaskPod) Name() string { - return "Pod" -} - -func (t *TaskPod) Sync(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() +func TaskPod(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("Pod", func(ctx context.Context) task.Result { + logger := logr.FromContextOrDiscard(ctx) + expected := newPod(state.Cluster(), state.TiKV(), state.ConfigHash) + if state.Pod() == nil { + if err := c.Apply(ctx, expected); err != nil { + return task.Fail().With("can't apply pod of tikv: %w", err) + } - expected := t.newPod(rtx.Cluster, rtx.TiKV, rtx.ConfigHash) - if rtx.Pod == nil { - if err := t.Client.Apply(rtx, expected); err != nil { - return task.Fail().With("can't apply pod of tikv: %w", err) + state.SetPod(expected) + return task.Complete().With("pod is created") } - rtx.Pod = expected - return task.Complete().With("pod is created") - } + // minimize the deletion grace period seconds + if !state.Pod().GetDeletionTimestamp().IsZero() { + regionCount := 0 + if state.Store != nil { + regionCount = state.Store.RegionCount + } + if err := DeletePodWithGracePeriod(ctx, c, state.Pod(), regionCount); err != nil { + return task.Fail().With("can't minimize the deletion grace period of pod of tikv: %w", err) + } - // minimize the deletion grace period seconds - if !rtx.Pod.GetDeletionTimestamp().IsZero() { - regionCount := 0 - if rtx.Store != nil { - regionCount = rtx.Store.RegionCount - } - if err := DeletePodWithGracePeriod(ctx, t.Client, rtx.Pod, regionCount); err != nil { - return task.Fail().With("can't minimize the deletion grace period of pod of tikv: %w", err) + // key will be requeued after the pod is changed + return task.Complete().With("pod is deleting") } - // key will be requeued after the pod is changed - return task.Complete().With("pod is deleting") - } + res := k8s.ComparePods(state.Pod(), expected) + curHash, expectHash := state.Pod().Labels[v1alpha1.LabelKeyConfigHash], expected.Labels[v1alpha1.LabelKeyConfigHash] + configChanged := curHash != expectHash + logger.Info("compare pod", "result", res, "configChanged", configChanged, "currentConfigHash", curHash, "expectConfigHash", expectHash) - res := k8s.ComparePods(rtx.Pod, expected) - curHash, expectHash := rtx.Pod.Labels[v1alpha1.LabelKeyConfigHash], expected.Labels[v1alpha1.LabelKeyConfigHash] - configChanged := curHash != expectHash - t.Logger.Info("compare pod", "result", res, "configChanged", configChanged, "currentConfigHash", curHash, "expectConfigHash", expectHash) + if res == k8s.CompareResultRecreate || (configChanged && + state.TiKV().Spec.UpdateStrategy.Config == v1alpha1.ConfigUpdateStrategyRestart) { + logger.Info("will recreate the pod") + regionCount := 0 + if state.Store != nil { + regionCount = state.Store.RegionCount + } + if err := DeletePodWithGracePeriod(ctx, c, state.Pod(), regionCount); err != nil { + return task.Fail().With("can't minimize the deletion grace period of pod of tikv: %w", err) + } - if res == k8s.CompareResultRecreate || (configChanged && - rtx.TiKV.Spec.UpdateStrategy.Config == v1alpha1.ConfigUpdateStrategyRestart) { - t.Logger.Info("will recreate the pod") - regionCount := 0 - if rtx.Store != nil { - regionCount = rtx.Store.RegionCount - } - if err := DeletePodWithGracePeriod(ctx, t.Client, rtx.Pod, regionCount); err != nil { - return task.Fail().With("can't minimize the deletion grace period of pod of tikv: %w", err) - } + state.PodIsTerminating = true + return task.Complete().With("pod is deleting") + } else if res == k8s.CompareResultUpdate { + logger.Info("will update the pod in place") + if err := c.Apply(ctx, expected); err != nil { + return task.Fail().With("can't apply pod of tikv: %w", err) + } - rtx.PodIsTerminating = true - return task.Complete().With("pod is deleting") - } else if res == k8s.CompareResultUpdate { - t.Logger.Info("will update the pod in place") - if err := t.Client.Apply(rtx, expected); err != nil { - return task.Fail().With("can't apply pod of tikv: %w", err) + // write apply result back to ctx + state.SetPod(expected) } - // write apply result back to ctx - rtx.Pod = expected - } - - return task.Complete().With("pod is synced") + return task.Complete().With("pod is synced") + }) } -func (t *TaskPod) newPod(cluster *v1alpha1.Cluster, tikv *v1alpha1.TiKV, configHash string) *corev1.Pod { +func newPod(cluster *v1alpha1.Cluster, tikv *v1alpha1.TiKV, configHash string) *corev1.Pod { vols := []corev1.Volume{ { Name: v1alpha1.VolumeNameConfig, @@ -274,7 +260,7 @@ func (t *TaskPod) newPod(cluster *v1alpha1.Cluster, tikv *v1alpha1.TiKV, configH Command: []string{ "/bin/sh", "-c", - t.buildPrestopCheckScript(cluster, tikv), + buildPrestopCheckScript(cluster, tikv), }, }, }, @@ -293,7 +279,7 @@ func (t *TaskPod) newPod(cluster *v1alpha1.Cluster, tikv *v1alpha1.TiKV, configH return pod } -func (*TaskPod) buildPrestopCheckScript(cluster *v1alpha1.Cluster, tikv *v1alpha1.TiKV) string { +func buildPrestopCheckScript(cluster *v1alpha1.Cluster, tikv *v1alpha1.TiKV) string { sb := strings.Builder{} sb.WriteString(v1alpha1.DirNamePrestop) sb.WriteString("/prestop-checker") diff --git a/pkg/controllers/tikv/tasks/pvc.go b/pkg/controllers/tikv/tasks/pvc.go index c54adae11d..7ccda0133e 100644 --- a/pkg/controllers/tikv/tasks/pvc.go +++ b/pkg/controllers/tikv/tasks/pvc.go @@ -15,6 +15,8 @@ package tasks import ( + "context" + "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -22,39 +24,21 @@ import ( "github.com/pingcap/tidb-operator/apis/core/v1alpha1" "github.com/pingcap/tidb-operator/pkg/client" maputil "github.com/pingcap/tidb-operator/pkg/utils/map" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" "github.com/pingcap/tidb-operator/pkg/volumes" ) -type TaskPVC struct { - Client client.Client - Logger logr.Logger - VolumeModifier volumes.Modifier -} - -func NewTaskPVC(logger logr.Logger, c client.Client, vm volumes.Modifier) task.Task[ReconcileContext] { - return &TaskPVC{ - Client: c, - Logger: logger, - VolumeModifier: vm, - } -} - -func (*TaskPVC) Name() string { - return "PVC" -} - -func (t *TaskPVC) Sync(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - - pvcs := newPVCs(rtx.TiKV) - if wait, err := volumes.SyncPVCs(rtx, t.Client, pvcs, t.VolumeModifier, t.Logger); err != nil { - return task.Fail().With("failed to sync pvcs: %w", err) - } else if wait { - return task.Complete().With("waiting for pvcs to be synced") - } +func TaskPVC(state *ReconcileContext, logger logr.Logger, c client.Client, vm volumes.Modifier) task.Task { + return task.NameTaskFunc("PVC", func(ctx context.Context) task.Result { + pvcs := newPVCs(state.TiKV()) + if wait, err := volumes.SyncPVCs(ctx, c, pvcs, vm, logger); err != nil { + return task.Fail().With("failed to sync pvcs: %w", err) + } else if wait { + return task.Complete().With("waiting for pvcs to be synced") + } - return task.Complete().With("pvcs are synced") + return task.Complete().With("pvcs are synced") + }) } func newPVCs(tikv *v1alpha1.TiKV) []*corev1.PersistentVolumeClaim { diff --git a/pkg/controllers/tikv/tasks/state.go b/pkg/controllers/tikv/tasks/state.go new file mode 100644 index 0000000000..ea91487369 --- /dev/null +++ b/pkg/controllers/tikv/tasks/state.go @@ -0,0 +1,98 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tasks + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/pingcap/tidb-operator/apis/core/v1alpha1" + "github.com/pingcap/tidb-operator/pkg/controllers/common" + "github.com/pingcap/tidb-operator/pkg/runtime" +) + +type state struct { + key types.NamespacedName + + cluster *v1alpha1.Cluster + tikv *v1alpha1.TiKV + pod *corev1.Pod +} + +type State interface { + common.TiKVStateInitializer + common.ClusterStateInitializer + common.PodStateInitializer + + common.TiKVState + common.ClusterState + common.PodState + + common.InstanceState[*runtime.TiKV] + + SetPod(*corev1.Pod) +} + +func NewState(key types.NamespacedName) State { + s := &state{ + key: key, + } + return s +} + +func (s *state) TiKV() *v1alpha1.TiKV { + return s.tikv +} + +func (s *state) Cluster() *v1alpha1.Cluster { + return s.cluster +} + +func (s *state) Pod() *corev1.Pod { + return s.pod +} + +func (s *state) Instance() *runtime.TiKV { + return runtime.FromTiKV(s.tikv) +} + +func (s *state) SetPod(pod *corev1.Pod) { + s.pod = pod +} + +func (s *state) TiKVInitializer() common.TiKVInitializer { + return common.NewResource(func(tikv *v1alpha1.TiKV) { s.tikv = tikv }). + WithNamespace(common.Namespace(s.key.Namespace)). + WithName(common.Name(s.key.Name)). + Initializer() +} + +func (s *state) ClusterInitializer() common.ClusterInitializer { + return common.NewResource(func(cluster *v1alpha1.Cluster) { s.cluster = cluster }). + WithNamespace(common.Namespace(s.key.Namespace)). + WithName(common.Lazy[string](func() string { + return s.tikv.Spec.Cluster.Name + })). + Initializer() +} + +func (s *state) PodInitializer() common.PodInitializer { + return common.NewResource(s.SetPod). + WithNamespace(common.Namespace(s.key.Namespace)). + WithName(common.Lazy[string](func() string { + return s.tikv.PodName() + })). + Initializer() +} diff --git a/pkg/controllers/tikv/tasks/status.go b/pkg/controllers/tikv/tasks/status.go index 2b63d59b2e..209d59bb55 100644 --- a/pkg/controllers/tikv/tasks/status.go +++ b/pkg/controllers/tikv/tasks/status.go @@ -15,173 +15,103 @@ package tasks import ( + "context" "time" - "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/pingcap/tidb-operator/apis/core/v1alpha1" "github.com/pingcap/tidb-operator/pkg/client" pdv1 "github.com/pingcap/tidb-operator/pkg/timanager/apis/pd/v1" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" "github.com/pingcap/tidb-operator/third_party/kubernetes/pkg/controller/statefulset" ) -func TaskStatusSuspend(c client.Client) task.Task[ReconcileContext] { - return task.NameTaskFunc("StatusSuspend", func(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - rtx.TiKV.Status.ObservedGeneration = rtx.TiKV.Generation +const ( + defaultTaskWaitDuration = 5 * time.Second +) - var ( - suspendStatus = metav1.ConditionFalse - suspendMessage = "tikv is suspending" +// TODO(liubo02): extract to common task +// +//nolint:gocyclo // refactor is possible +func TaskStatus(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("Status", func(ctx context.Context) task.Result { + needUpdate := false + tikv := state.TiKV() + pod := state.Pod() + // TODO(liubo02): simplify it + var healthy bool + if pod != nil && + statefulset.IsPodRunningAndReady(pod) && + !state.PodIsTerminating && + state.Store.NodeState == v1alpha1.StoreStateServing { + healthy = true + } + needUpdate = syncHealthCond(tikv, healthy) || needUpdate + needUpdate = syncSuspendCond(tikv) || needUpdate + needUpdate = syncLeadersEvictedCond(tikv, state.Store, state.LeaderEvicting) || needUpdate + needUpdate = SetIfChanged(&tikv.Status.ID, state.StoreID) || needUpdate + needUpdate = SetIfChanged(&tikv.Status.State, state.StoreState) || needUpdate - // when suspending, the health status should be false - healthStatus = metav1.ConditionFalse - healthMessage = "tikv is not healthy" - ) + needUpdate = SetIfChanged(&tikv.Status.ObservedGeneration, tikv.Generation) || needUpdate + needUpdate = SetIfChanged(&tikv.Status.UpdateRevision, tikv.Labels[v1alpha1.LabelKeyInstanceRevisionHash]) || needUpdate - if rtx.Pod == nil { - suspendStatus = metav1.ConditionTrue - suspendMessage = "tikv is suspended" + if healthy { + needUpdate = SetIfChanged(&tikv.Status.CurrentRevision, pod.Labels[v1alpha1.LabelKeyInstanceRevisionHash]) || needUpdate } - needUpdate := meta.SetStatusCondition(&rtx.TiKV.Status.Conditions, metav1.Condition{ - Type: v1alpha1.TiKVCondSuspended, - Status: suspendStatus, - ObservedGeneration: rtx.TiKV.Generation, - // TODO: use different reason for suspending and suspended - Reason: v1alpha1.TiKVSuspendReason, - Message: suspendMessage, - }) - - needUpdate = meta.SetStatusCondition(&rtx.TiKV.Status.Conditions, metav1.Condition{ - Type: v1alpha1.TiKVCondHealth, - Status: healthStatus, - ObservedGeneration: rtx.TiKV.Generation, - Reason: v1alpha1.TiKVHealthReason, - Message: healthMessage, - }) || needUpdate if needUpdate { - if err := c.Status().Update(ctx, rtx.TiKV); err != nil { + if err := c.Status().Update(ctx, tikv); err != nil { return task.Fail().With("cannot update status: %w", err) } } - return task.Complete().With("status of suspend tikv is updated") - }) -} - -type TaskStatus struct { - Client client.Client - Logger logr.Logger -} - -func NewTaskStatus(logger logr.Logger, c client.Client) task.Task[ReconcileContext] { - return &TaskStatus{ - Client: c, - Logger: logger, - } -} + // TODO: use a condition to refactor it + if tikv.Status.ID == "" || tikv.Status.State != v1alpha1.StoreStateServing || !v1alpha1.IsUpToDate(tikv) { + // can we only rely on the PD member events for this condition? + // TODO(liubo02): change to task.Wait + return task.Retry(defaultTaskWaitDuration).With("tikv may not be initialized, retry") + } -func (*TaskStatus) Name() string { - return "Status" + return task.Complete().With("status is synced") + }) } -//nolint:gocyclo // refactor is possible -func (t *TaskStatus) Sync(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - +func syncHealthCond(tikv *v1alpha1.TiKV, healthy bool) bool { var ( - healthStatus = metav1.ConditionFalse - healthMessage = "tikv is not healthy" - - suspendStatus = metav1.ConditionFalse - suspendMessage = "tikv is not suspended" - - needUpdate = false + status = metav1.ConditionFalse + reason = "Unhealthy" + msg = "instance is not healthy" ) - - if rtx.StoreID != "" { - if rtx.TiKV.Status.ID != rtx.StoreID { - rtx.TiKV.Status.ID = rtx.StoreID - needUpdate = true - } - - info, err := rtx.PDClient.GetStore(ctx, rtx.StoreID) - if err == nil && info != nil && info.Store != nil { - rtx.StoreState = info.Store.NodeState.String() - } else { - t.Logger.Error(err, "failed to get tikv store info", "store", rtx.StoreID) - } - } - if rtx.StoreState != "" && rtx.TiKV.Status.State != rtx.StoreState { - rtx.TiKV.Status.State = rtx.StoreState - needUpdate = true - } - - needUpdate = meta.SetStatusCondition(&rtx.TiKV.Status.Conditions, metav1.Condition{ - Type: v1alpha1.TiKVCondSuspended, - Status: suspendStatus, - ObservedGeneration: rtx.TiKV.Generation, - Reason: v1alpha1.TiKVSuspendReason, - Message: suspendMessage, - }) || needUpdate - - if needUpdate || !v1alpha1.IsReconciled(rtx.TiKV) || - rtx.TiKV.Status.UpdateRevision != rtx.TiKV.Labels[v1alpha1.LabelKeyInstanceRevisionHash] { - rtx.TiKV.Status.ObservedGeneration = rtx.TiKV.Generation - rtx.TiKV.Status.UpdateRevision = rtx.TiKV.Labels[v1alpha1.LabelKeyInstanceRevisionHash] - needUpdate = true - } - - if rtx.Pod == nil || rtx.PodIsTerminating { - rtx.Healthy = false - } else if statefulset.IsPodRunningAndReady(rtx.Pod) && rtx.StoreState == v1alpha1.StoreStateServing { - rtx.Healthy = true - if rtx.TiKV.Status.CurrentRevision != rtx.Pod.Labels[v1alpha1.LabelKeyInstanceRevisionHash] { - rtx.TiKV.Status.CurrentRevision = rtx.Pod.Labels[v1alpha1.LabelKeyInstanceRevisionHash] - needUpdate = true - } - } else { - rtx.Healthy = false - } - - if rtx.Healthy { - healthStatus = metav1.ConditionTrue - healthMessage = "tikv is healthy" - } - needUpdate = meta.SetStatusCondition(&rtx.TiKV.Status.Conditions, metav1.Condition{ - Type: v1alpha1.TiKVCondHealth, - Status: healthStatus, - ObservedGeneration: rtx.TiKV.Generation, - Reason: v1alpha1.TiKVHealthReason, - Message: healthMessage, - }) || needUpdate - - if t.syncLeadersEvictedCond(rtx.TiKV, rtx.Store, rtx.LeaderEvicting) { - needUpdate = true - } - - if needUpdate { - if err := t.Client.Status().Update(ctx, rtx.TiKV); err != nil { - return task.Fail().With("cannot update status: %w", err) - } + if healthy { + status = metav1.ConditionTrue + reason = "Healthy" + msg = "instance is healthy" } - // TODO: use a condition to refactor it - if rtx.TiKV.Status.ID == "" || rtx.TiKV.Status.State != v1alpha1.StoreStateServing || !v1alpha1.IsUpToDate(rtx.TiKV) { - // can we only rely on the PD member events for this condition? - //nolint:mnd // refactor to use a constant - return task.Retry(5 * time.Second).With("tikv may not be initialized, retry") - } + return meta.SetStatusCondition(&tikv.Status.Conditions, metav1.Condition{ + Type: v1alpha1.CondHealth, + Status: status, + ObservedGeneration: tikv.Generation, + Reason: reason, + Message: msg, + }) +} - return task.Complete().With("status is synced") +func syncSuspendCond(tikv *v1alpha1.TiKV) bool { + // always set it as unsuspended + return meta.SetStatusCondition(&tikv.Status.Conditions, metav1.Condition{ + Type: v1alpha1.CondSuspended, + Status: metav1.ConditionFalse, + ObservedGeneration: tikv.Generation, + Reason: v1alpha1.ReasonUnsuspended, + Message: "instace is not suspended", + }) } // Status of this condition can only transfer as the below -func (*TaskStatus) syncLeadersEvictedCond(tikv *v1alpha1.TiKV, store *pdv1.Store, isEvicting bool) bool { +func syncLeadersEvictedCond(tikv *v1alpha1.TiKV, store *pdv1.Store, isEvicting bool) bool { status := metav1.ConditionFalse reason := "NotEvicted" msg := "leaders are not all evicted" @@ -204,3 +134,16 @@ func (*TaskStatus) syncLeadersEvictedCond(tikv *v1alpha1.TiKV, store *pdv1.Store Message: msg, }) } + +// TODO: move to utils +func SetIfChanged[T comparable](dst *T, src T) bool { + if src == *new(T) { + return false + } + if *dst != src { + *dst = src + return true + } + + return false +} diff --git a/pkg/controllers/tikv/tasks/store_labels.go b/pkg/controllers/tikv/tasks/store_labels.go index 733ccc8a4d..eb43ae1292 100644 --- a/pkg/controllers/tikv/tasks/store_labels.go +++ b/pkg/controllers/tikv/tasks/store_labels.go @@ -15,77 +15,64 @@ package tasks import ( + "context" "reflect" "strconv" - "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" + "github.com/go-logr/logr" + "github.com/pingcap/tidb-operator/apis/core/v1alpha1" "github.com/pingcap/tidb-operator/pkg/client" "github.com/pingcap/tidb-operator/pkg/utils/k8s" - "github.com/pingcap/tidb-operator/pkg/utils/task/v2" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" ) -type TaskStoreLabels struct { - Client client.Client - Logger logr.Logger -} - -func NewTaskStoreLabels(logger logr.Logger, c client.Client) task.Task[ReconcileContext] { - return &TaskStoreLabels{ - Client: c, - Logger: logger, - } -} - -func (*TaskStoreLabels) Name() string { - return "StoreLabels" -} - -func (t *TaskStoreLabels) Sync(ctx task.Context[ReconcileContext]) task.Result { - rtx := ctx.Self() - - if rtx.StoreState != v1alpha1.StoreStateServing || rtx.PodIsTerminating || rtx.Pod == nil { - return task.Complete().With("skip sync store labels as the store is not serving") - } - - nodeName := rtx.Pod.Spec.NodeName - if nodeName == "" { - return task.Fail().With("pod %s/%s has not been scheduled", rtx.TiKV.Namespace, rtx.TiKV.Name) - } - var node corev1.Node - if err := t.Client.Get(ctx, client.ObjectKey{Name: nodeName}, &node); err != nil { - return task.Fail().With("failed to get node %s: %s", nodeName, err) - } - - // TODO: too many API calls to PD? - pdCfg, err := rtx.PDClient.GetConfig(ctx) - if err != nil { - return task.Fail().With("failed to get pd config: %s", err) - } - keys := pdCfg.Replication.LocationLabels - if len(keys) == 0 { - return task.Complete().With("no store labels need to sync") - } +func TaskStoreLabels(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("StoreLabels", func(ctx context.Context) task.Result { + logger := logr.FromContextOrDiscard(ctx) + if state.StoreState != v1alpha1.StoreStateServing || state.PodIsTerminating || state.Pod() == nil { + return task.Complete().With("skip sync store labels as the store is not serving") + } - storeLabels := k8s.GetNodeLabelsForKeys(&node, keys) - if len(storeLabels) == 0 { - return task.Complete().With("no store labels from node %s to sync", nodeName) - } + nodeName := state.Pod().Spec.NodeName + if nodeName == "" { + return task.Fail().With("pod %s/%s has not been scheduled", state.TiKV().Namespace, state.TiKV().Name) + } + var node corev1.Node + if err := c.Get(ctx, client.ObjectKey{Name: nodeName}, &node); err != nil { + return task.Fail().With("failed to get node %s: %s", nodeName, err) + } - if !reflect.DeepEqual(rtx.Store.Labels, storeLabels) { - storeID, err := strconv.ParseUint(rtx.StoreID, 10, 64) + // TODO: too many API calls to PD? + pdCfg, err := state.PDClient.GetConfig(ctx) if err != nil { - return task.Fail().With("failed to parse store id %s: %s", rtx.StoreID, err) + return task.Fail().With("failed to get pd config: %s", err) } - set, err := rtx.PDClient.SetStoreLabels(ctx, storeID, storeLabels) - if err != nil { - return task.Fail().With("failed to set store labels: %s", err) - } else if set { - t.Logger.Info("store labels synced", "storeID", rtx.StoreID, "storeLabels", storeLabels) + keys := pdCfg.Replication.LocationLabels + if len(keys) == 0 { + return task.Complete().With("no store labels need to sync") + } + + storeLabels := k8s.GetNodeLabelsForKeys(&node, keys) + if len(storeLabels) == 0 { + return task.Complete().With("no store labels from node %s to sync", nodeName) + } + + if !reflect.DeepEqual(state.Store.Labels, storeLabels) { + storeID, err := strconv.ParseUint(state.StoreID, 10, 64) + if err != nil { + return task.Fail().With("failed to parse store id %s: %s", state.StoreID, err) + } + set, err := state.PDClient.SetStoreLabels(ctx, storeID, storeLabels) + if err != nil { + return task.Fail().With("failed to set store labels: %s", err) + } else if set { + logger.Info("store labels synced", "storeID", state.StoreID, "storeLabels", storeLabels) + } } - } - return task.Complete().With("store labels synced") + return task.Complete().With("store labels synced") + }) } diff --git a/pkg/utils/task/v2/result.go b/pkg/utils/task/v2/result.go deleted file mode 100644 index cf43ac3127..0000000000 --- a/pkg/utils/task/v2/result.go +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright 2024 PingCAP, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package task - -import ( - "fmt" - "strings" - "time" -) - -type Status int - -const ( - // task is complete and will not be requeue - SComplete Status = iota - // task is unexpectedly failed, runner will be interrupted - SFail - // some preconditions are not met, wait update events to trigger next run - SWait - // retry tasks after specified duration - SRetry -) - -func (s Status) String() string { - switch s { - case SComplete: - return "Complete" - case SFail: - return "Fail" - case SWait: - return "Wait" - case SRetry: - return "Retry" - } - - return "Unknown" -} - -// Result defines the result of a task -type Result interface { - Status() Status - RequeueAfter() time.Duration - Message() string -} - -type NamedResult interface { - Result - Name() string -} - -type AggregateResult interface { - Result - Results() []Result -} - -// WithMessage defines an interface to set message into task result -type WithMessage interface { - With(format string, args ...any) Result -} - -type taskResult struct { - status Status - requeueAfter time.Duration - message string -} - -func (r *taskResult) Status() Status { - return r.status -} - -func (r *taskResult) RequeueAfter() time.Duration { - return r.requeueAfter -} - -func (r *taskResult) Message() string { - if r.requeueAfter > 0 { - return fmt.Sprintf("%s(requeue after %s)", r.message, r.requeueAfter) - } - return r.message -} - -func (r *taskResult) With(format string, args ...any) Result { - r.message = fmt.Sprintf(format, args...) - return r -} - -// Complete means complete the current task and run the next one -func Complete() WithMessage { - return &taskResult{ - status: SComplete, - } -} - -// Fail means fail the current task and skip all next tasks -func Fail() WithMessage { - return &taskResult{ - status: SFail, - } -} - -// Retry means continue all next tasks and retry after dur -func Retry(dur time.Duration) WithMessage { - return &taskResult{ - status: SRetry, - requeueAfter: dur, - } -} - -// Wait means continue all next tasks and wait until next event triggers task run -func Wait() WithMessage { - return &taskResult{ - status: SWait, - } -} - -type namedResult struct { - Result - name string -} - -func AnnotateName(name string, r Result) Result { - if _, ok := r.(AggregateResult); ok { - return r - } - return &namedResult{ - Result: r, - name: name, - } -} - -func (r *namedResult) Name() string { - return r.name -} - -type aggregateResult struct { - rs []Result -} - -func NewAggregateResult(rs ...Result) AggregateResult { - return &aggregateResult{rs: rs} -} - -func (r *aggregateResult) Results() []Result { - return r.rs -} - -func (r *aggregateResult) Status() Status { - needRetry := false - needWait := false - for _, res := range r.rs { - switch res.Status() { - case SFail: - return SFail - case SRetry: - needRetry = true - case SWait: - needWait = true - } - } - - if needRetry { - return SRetry - } - - if needWait { - return SWait - } - - return SComplete -} - -func (r *aggregateResult) RequeueAfter() time.Duration { - var minDur time.Duration = 0 - for _, res := range r.rs { - if minDur < res.RequeueAfter() { - minDur = res.RequeueAfter() - } - } - - return minDur -} - -func (r *aggregateResult) Message() string { - sb := strings.Builder{} - for _, res := range r.rs { - sb.WriteString(res.Message()) - } - - return sb.String() -} diff --git a/pkg/utils/task/v2/runner.go b/pkg/utils/task/v2/runner.go deleted file mode 100644 index ca4f866a48..0000000000 --- a/pkg/utils/task/v2/runner.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2024 PingCAP, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package task - -import ( - "fmt" - - ctrl "sigs.k8s.io/controller-runtime" -) - -// TaskRunner is an executor to run a series of tasks sequentially -type TaskRunner[T any] interface { - Run(ctx Context[T]) (ctrl.Result, error) -} - -type taskRunner[T any] struct { - reporter TaskReporter - taskQueue Task[T] -} - -// There are four status of tasks -// - Complete: means this task is complete and all is expected -// - Failed: means an err occurred -// - Retry: means this task need to wait an interval and retry -// - Wait: means this task will wait for next event trigger -// And five results of reconiling -// 1. All tasks are complete, the key will not be re-added -// 2. Some tasks are failed, return err and wait with backoff -// 3. Some tasks need retry, requeue after an interval -// 4. Some tasks are not run, return err and wait with backoff -// 5. Particular tasks are complete and left are skipped, the key will not be re-added -func NewTaskRunner[T any](reporter TaskReporter, ts ...Task[T]) TaskRunner[T] { - if reporter == nil { - reporter = &dummyReporter{} - } - return &taskRunner[T]{ - reporter: reporter, - taskQueue: NewTaskQueue(ts...), - } -} - -func (r *taskRunner[T]) Run(ctx Context[T]) (ctrl.Result, error) { - res := r.taskQueue.Sync(ctx) - r.reporter.AddResult(res) - - switch res.Status() { - case SFail: - return ctrl.Result{}, fmt.Errorf("some tasks are failed: %v", res.Message()) - case SRetry: - return ctrl.Result{ - RequeueAfter: res.RequeueAfter(), - }, nil - default: - // SComplete and SWait - return ctrl.Result{}, nil - } -} diff --git a/pkg/utils/task/v2/task.go b/pkg/utils/task/v2/task.go deleted file mode 100644 index 7c5f0752fc..0000000000 --- a/pkg/utils/task/v2/task.go +++ /dev/null @@ -1,233 +0,0 @@ -// Copyright 2024 PingCAP, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package task - -import ( - "context" - "reflect" - "strings" - - "github.com/olekukonko/tablewriter" -) - -// Context is a wrapper of any struct which can return its self -// It's defined to avoid calling ctx.Value() -type Context[T any] interface { - context.Context - Self() *T -} - -type Task[T any] interface { - Sync(ctx Context[T]) Result -} - -type Condition[T any] interface { - Satisfy(ctx Context[T]) bool -} - -type OptionalTask[T any] interface { - Task[T] - Satisfied(ctx Context[T]) bool -} - -type FinalTask[T any] interface { - Task[T] - IsFinal() bool -} - -type TaskNamer interface { - Name() string -} - -type TaskFunc[T any] func(ctx Context[T]) Result - -func (f TaskFunc[T]) Sync(ctx Context[T]) Result { - return f(ctx) -} - -type namedTask[T any] struct { - Task[T] - name string -} - -func NameTaskFunc[T any](name string, t TaskFunc[T]) Task[T] { - return &namedTask[T]{ - Task: t, - name: name, - } -} - -func (t *namedTask[T]) Name() string { - return t.name -} - -type CondFunc[T any] func(ctx Context[T]) bool - -func (f CondFunc[T]) Satisfy(ctx Context[T]) bool { - return f(ctx) -} - -type optional[T any] struct { - Task[T] - cond Condition[T] -} - -var ( - _ OptionalTask[int] = &optional[int]{} - _ FinalTask[int] = &optional[int]{} -) - -func (t *optional[T]) Satisfied(ctx Context[T]) bool { - return t.cond.Satisfy(ctx) -} - -func (t *optional[T]) IsFinal() bool { - final, ok := t.Task.(FinalTask[T]) - if ok { - return final.IsFinal() - } - return false -} - -func NewOptionalTask[T any](cond Condition[T], ts ...Task[T]) Task[T] { - return &optional[T]{ - Task: NewTaskQueue(ts...), - cond: cond, - } -} - -func NewSwitchTask[T any](cond Condition[T], ts ...Task[T]) Task[T] { - return NewOptionalTask(cond, NewFinalTask(ts...)) -} - -type final[T any] struct { - Task[T] -} - -func (t *final[T]) Satisfied(ctx Context[T]) bool { - optional, ok := t.Task.(OptionalTask[T]) - if ok { - return optional.Satisfied(ctx) - } - return true -} - -func (*final[T]) IsFinal() bool { - return true -} - -var ( - _ OptionalTask[int] = &final[int]{} - _ FinalTask[int] = &final[int]{} -) - -func NewFinalTask[T any](ts ...Task[T]) Task[T] { - return &final[T]{Task: NewTaskQueue(ts...)} -} - -type queue[T any] struct { - ts []Task[T] - isFinal bool -} - -func (t *queue[T]) Sync(ctx Context[T]) Result { - rs := []Result{} - for _, tt := range t.ts { - optional, ok := tt.(OptionalTask[T]) - if ok && !optional.Satisfied(ctx) { - continue - } - - r := tt.Sync(ctx) - rs = append(rs, AnnotateName(Name(tt), r)) - if r.Status() == SFail { - break - } - - if final, ok := tt.(FinalTask[T]); ok && final.IsFinal() { - t.isFinal = true - break - } - } - - return NewAggregateResult(rs...) -} - -func (*queue[T]) Satisfied(Context[T]) bool { - return true -} - -func (t *queue[T]) IsFinal() bool { - return t.isFinal -} - -func NewTaskQueue[T any](ts ...Task[T]) Task[T] { - return &queue[T]{ts: ts} -} - -type TaskReporter interface { - AddResult(r Result) - Summary() string -} - -type tableReporter struct { - table *tablewriter.Table - builder *strings.Builder -} - -type dummyReporter struct{} - -func (*dummyReporter) AddResult(Result) {} - -func (*dummyReporter) Summary() string { - return "" -} - -func NewTableTaskReporter() TaskReporter { - builder := strings.Builder{} - table := tablewriter.NewWriter(&builder) - table.SetHeader([]string{"Name", "Status", "Message"}) - return &tableReporter{ - table: table, - builder: &builder, - } -} - -func (t *tableReporter) AddResult(r Result) { - switch underlying := r.(type) { - case AggregateResult: - for _, rr := range underlying.Results() { - t.AddResult(rr) - } - case NamedResult: - t.table.Append([]string{underlying.Name(), r.Status().String(), r.Message()}) - default: - t.table.Append([]string{"", r.Status().String(), r.Message()}) - } -} - -func (t *tableReporter) Summary() string { - t.table.Render() - return t.builder.String() -} - -func Name[T any](t Task[T]) string { - namer, ok := t.(TaskNamer) - if ok { - return namer.Name() - } - - return reflect.TypeOf(t).Name() -}