github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/controller/rsm/transformer_update_strategy.go (about) 1 /* 2 Copyright (C) 2022-2023 ApeCloud Co., Ltd 3 4 This file is part of KubeBlocks project 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU Affero General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 This program is distributed in the hope that it will be useful 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU Affero General Public License for more details. 15 16 You should have received a copy of the GNU Affero General Public License 17 along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 package rsm 21 22 import ( 23 apps "k8s.io/api/apps/v1" 24 corev1 "k8s.io/api/core/v1" 25 "sigs.k8s.io/controller-runtime/pkg/client" 26 27 workloads "github.com/1aal/kubeblocks/apis/workloads/v1alpha1" 28 "github.com/1aal/kubeblocks/pkg/controller/graph" 29 "github.com/1aal/kubeblocks/pkg/controller/model" 30 intctrlutil "github.com/1aal/kubeblocks/pkg/controllerutil" 31 ) 32 33 type UpdateStrategyTransformer struct{} 34 35 var _ graph.Transformer = &UpdateStrategyTransformer{} 36 37 func (t *UpdateStrategyTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { 38 transCtx, _ := ctx.(*rsmTransformContext) 39 rsm := transCtx.rsm 40 rsmOrig := transCtx.rsmOrig 41 if !model.IsObjectStatusUpdating(rsmOrig) { 42 return nil 43 } 44 45 // read the underlying sts 46 stsObj := &apps.StatefulSet{} 47 if err := transCtx.Client.Get(transCtx.Context, client.ObjectKeyFromObject(rsm), stsObj); err != nil { 48 return err 49 } 50 // read all pods belong to the sts, hence belong to the rsm 51 pods, err := getPodsOfStatefulSet(transCtx.Context, transCtx.Client, stsObj) 52 if err != nil { 53 return err 54 } 55 56 // prepare to do pods Deletion, that's the only thing we should do, 57 // the stateful_set reconciler will do the others. 58 // to simplify the process, we do pods Deletion after stateful_set reconcile done, 59 // that is stsObj.Generation == stsObj.Status.ObservedGeneration 60 if stsObj.Generation != stsObj.Status.ObservedGeneration { 61 return nil 62 } 63 64 // then we wait all pods' presence, that is len(pods) == stsObj.Spec.Replicas 65 // only then, we have enough info about the previous pods before delete the current one 66 if len(pods) != int(*stsObj.Spec.Replicas) { 67 return nil 68 } 69 70 // we don't check whether pod role label present: prefer stateful_set's Update done than role probing ready 71 // TODO(free6om): maybe should wait rsm ready for high availability: 72 // 1. after some pods updated 73 // 2. before switchover 74 // 3. after switchover done 75 76 // generate the pods Deletion plan 77 plan := newUpdatePlan(*rsm, pods) 78 podsToBeUpdated, err := plan.execute() 79 if err != nil { 80 return err 81 } 82 83 // do switchover if leader in pods to be updated 84 switch shouldWaitNextLoop, err := doSwitchoverIfNeeded(transCtx, dag, pods, podsToBeUpdated); { 85 case err != nil: 86 return err 87 case shouldWaitNextLoop: 88 return nil 89 } 90 91 graphCli, _ := transCtx.Client.(model.GraphClient) 92 for _, pod := range podsToBeUpdated { 93 graphCli.Delete(dag, pod) 94 } 95 96 return nil 97 } 98 99 // return true means action created or in progress, should wait it to the termination state 100 func doSwitchoverIfNeeded(transCtx *rsmTransformContext, dag *graph.DAG, pods []corev1.Pod, podsToBeUpdated []*corev1.Pod) (bool, error) { 101 if len(podsToBeUpdated) == 0 { 102 return false, nil 103 } 104 105 rsm := transCtx.rsm 106 if !shouldSwitchover(rsm, podsToBeUpdated, pods) { 107 return false, nil 108 } 109 110 graphCli, _ := transCtx.Client.(model.GraphClient) 111 actionList, err := getActionList(transCtx, jobScenarioUpdate) 112 if err != nil { 113 return true, err 114 } 115 if len(actionList) == 0 { 116 return true, createSwitchoverAction(dag, graphCli, rsm, pods) 117 } 118 119 // switch status if found: 120 // 1. succeed means action executed successfully, 121 // but some kind of cluster may have false positive(apecloud-mysql only?), 122 // we can't wait forever, update is more important. 123 // do the next pod update stage 124 // 2. failed means action executed failed, 125 // but this doesn't mean the cluster didn't switchover(again, apecloud-mysql only?) 126 // we can't do anything either in this situation, emit failed event and 127 // do the next pod update state 128 // 3. in progress means action still running, 129 // return and wait it reaches termination state. 130 action := actionList[0] 131 switch { 132 case action.Status.Succeeded == 0 && action.Status.Failed == 0: 133 // action in progress, wait 134 return true, nil 135 case action.Status.Failed > 0: 136 emitActionFailedEvent(transCtx, jobTypeSwitchover, action.Name) 137 fallthrough 138 case action.Status.Succeeded > 0: 139 // clean up the action 140 doActionCleanup(dag, graphCli, action) 141 } 142 return false, nil 143 } 144 145 func createSwitchoverAction(dag *graph.DAG, cli model.GraphClient, rsm *workloads.ReplicatedStateMachine, pods []corev1.Pod) error { 146 leader := getLeaderPodName(rsm.Status.MembersStatus) 147 targetOrdinal := selectSwitchoverTarget(rsm, pods) 148 target := getPodName(rsm.Name, targetOrdinal) 149 actionType := jobTypeSwitchover 150 ordinal, _ := getPodOrdinal(leader) 151 actionName := getActionName(rsm.Name, int(rsm.Generation), ordinal, actionType) 152 action := buildAction(rsm, actionName, actionType, jobScenarioUpdate, leader, target) 153 154 // don't do cluster abnormal status analysis, prefer faster update process 155 return createAction(dag, cli, rsm, action) 156 } 157 158 func selectSwitchoverTarget(rsm *workloads.ReplicatedStateMachine, pods []corev1.Pod) int { 159 var podUpdated, podUpdatedWithLabel string 160 for _, pod := range pods { 161 if intctrlutil.GetPodRevision(&pod) != rsm.Status.UpdateRevision { 162 continue 163 } 164 if len(podUpdated) == 0 { 165 podUpdated = pod.Name 166 } 167 if _, ok := pod.Labels[roleLabelKey]; !ok { 168 continue 169 } 170 if len(podUpdatedWithLabel) == 0 { 171 podUpdatedWithLabel = pod.Name 172 break 173 } 174 } 175 var finalPod string 176 switch { 177 case len(podUpdatedWithLabel) > 0: 178 finalPod = podUpdatedWithLabel 179 case len(podUpdated) > 0: 180 finalPod = podUpdated 181 default: 182 finalPod = pods[0].Name 183 } 184 ordinal, _ := getPodOrdinal(finalPod) 185 return ordinal 186 } 187 188 func shouldSwitchover(rsm *workloads.ReplicatedStateMachine, podsToBeUpdated []*corev1.Pod, allPods []corev1.Pod) bool { 189 if len(allPods) < 2 { 190 // replicas is less than 2, no need to switchover 191 return false 192 } 193 reconfiguration := rsm.Spec.MembershipReconfiguration 194 if reconfiguration == nil { 195 return false 196 } 197 if reconfiguration.SwitchoverAction == nil { 198 return false 199 } 200 leaderName := getLeaderPodName(rsm.Status.MembersStatus) 201 for _, pod := range podsToBeUpdated { 202 if pod.Name == leaderName { 203 return true 204 } 205 } 206 return false 207 }