github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/controller/rsm/transformer_member_reconfiguration.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 "fmt" 24 "regexp" 25 "strconv" 26 27 apps "k8s.io/api/apps/v1" 28 batchv1 "k8s.io/api/batch/v1" 29 "sigs.k8s.io/controller-runtime/pkg/client" 30 31 workloads "github.com/1aal/kubeblocks/apis/workloads/v1alpha1" 32 "github.com/1aal/kubeblocks/pkg/controller/graph" 33 "github.com/1aal/kubeblocks/pkg/controller/model" 34 ) 35 36 // MemberReconfigurationTransformer handles member reconfiguration 37 type MemberReconfigurationTransformer struct{} 38 39 var _ graph.Transformer = &MemberReconfigurationTransformer{} 40 41 type actionInfo struct { 42 shortActionName string 43 ordinal int 44 actionType string 45 } 46 47 type conditionChecker = func() bool 48 49 var actionNameRegex = regexp.MustCompile(`(.*)-([0-9]+)-([0-9]+)-([a-zA-Z\-]+)$`) 50 51 func (t *MemberReconfigurationTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { 52 transCtx, _ := ctx.(*rsmTransformContext) 53 if model.IsObjectDeleting(transCtx.rsm) { 54 return nil 55 } 56 rsm := transCtx.rsm 57 graphCli, _ := transCtx.Client.(model.GraphClient) 58 59 if len(rsm.Spec.Roles) == 0 || rsm.Spec.RoleProbe == nil { 60 return nil 61 } 62 63 // handle cluster initialization 64 // set initReplicas at creation 65 if rsm.Status.InitReplicas == 0 { 66 rsm.Status.InitReplicas = *rsm.Spec.Replicas 67 return nil 68 } 69 // update readyInitReplicas 70 if rsm.Status.ReadyInitReplicas < rsm.Status.InitReplicas { 71 rsm.Status.ReadyInitReplicas = int32(len(rsm.Status.MembersStatus)) 72 } 73 // return if cluster initialization not done 74 if rsm.Status.ReadyInitReplicas != rsm.Status.InitReplicas { 75 return nil 76 } 77 78 // cluster initialization done, handle dynamic membership reconfiguration 79 80 // rsm is ready 81 if IsRSMReady(rsm) { 82 return cleanAction(transCtx, dag) 83 } 84 85 if !shouldHaveActions(rsm) { 86 return nil 87 } 88 89 // get the underlying sts 90 sts := &apps.StatefulSet{} 91 if err := graphCli.Get(transCtx.Context, client.ObjectKeyFromObject(rsm), sts); err != nil { 92 return err 93 } 94 95 // no enough replicas in scale out, tell sts to create them. 96 memberReadyReplicas := int32(len(rsm.Status.MembersStatus)) 97 if memberReadyReplicas < *rsm.Spec.Replicas && 98 sts.Status.ReadyReplicas < *rsm.Spec.Replicas { 99 return nil 100 } 101 102 graphCli.Noop(dag, sts) 103 104 // barrier: the underlying sts is ready and has enough replicas 105 if sts.Status.ReadyReplicas < *rsm.Spec.Replicas || !isStatefulSetReady(sts) { 106 return nil 107 } 108 109 // get last action 110 actionList, err := getActionList(transCtx, jobScenarioMembership) 111 if err != nil { 112 return err 113 } 114 115 // if no action, create the first one 116 if len(actionList) == 0 { 117 return createNextAction(transCtx, dag, rsm, nil) 118 } 119 120 // got action, there should be only one action 121 action := actionList[0] 122 switch { 123 case action.Status.Succeeded > 0: 124 // wait action's result: 125 // e.g. action with ordinal 3 and type member-join, wait member 3 until it appears in status.membersStatus 126 if !isActionDone(rsm, action) { 127 return nil 128 } 129 // mark it as 'handled' 130 deleteAction(transCtx, dag, action) 131 return createNextAction(transCtx, dag, rsm, action) 132 case action.Status.Failed > 0: 133 emitEvent(transCtx, action) 134 if !isSwitchoverAction(action) { 135 // need manual handling 136 return nil 137 } 138 return createNextAction(transCtx, dag, rsm, action) 139 default: 140 // action in progress 141 return nil 142 } 143 } 144 145 func isStatefulSetReady(sts *apps.StatefulSet) bool { 146 if sts == nil { 147 return false 148 } 149 if sts.Status.ObservedGeneration == sts.Generation && 150 sts.Status.Replicas == *sts.Spec.Replicas && 151 sts.Status.ReadyReplicas == sts.Status.Replicas { 152 return true 153 } 154 return false 155 } 156 157 func cleanAction(transCtx *rsmTransformContext, dag *graph.DAG) error { 158 actionList, err := getActionList(transCtx, jobScenarioMembership) 159 if err != nil { 160 return err 161 } 162 if len(actionList) == 0 { 163 return nil 164 } 165 action := actionList[0] 166 switch { 167 case action.Status.Succeeded > 0: 168 deleteAction(transCtx, dag, action) 169 case action.Status.Failed > 0: 170 emitEvent(transCtx, action) 171 } 172 return nil 173 } 174 175 func isActionDone(rsm *workloads.ReplicatedStateMachine, action *batchv1.Job) bool { 176 ordinal, _ := getActionOrdinal(action.Name) 177 podName := getPodName(rsm.Name, ordinal) 178 membersStatus := rsm.Status.MembersStatus 179 switch action.Labels[jobTypeLabel] { 180 case jobTypeSwitchover: 181 leader := getLeaderPodName(rsm.Status.MembersStatus) 182 return podName != leader 183 case jobTypeMemberLeaveNotifying: 184 return !isMemberReady(podName, membersStatus) 185 case jobTypeMemberJoinNotifying: 186 return isMemberReady(podName, membersStatus) 187 case jobTypeLogSync, jobTypePromote: 188 // no info, ignore them 189 } 190 return true 191 } 192 193 func isSwitchoverAction(action *batchv1.Job) bool { 194 return action.Labels[jobTypeLabel] == jobTypeSwitchover 195 } 196 197 func deleteAction(transCtx *rsmTransformContext, dag *graph.DAG, action *batchv1.Job) { 198 cli, _ := transCtx.Client.(model.GraphClient) 199 doActionCleanup(dag, cli, action) 200 } 201 202 func createNextAction(transCtx *rsmTransformContext, dag *graph.DAG, rsm *workloads.ReplicatedStateMachine, currentAction *batchv1.Job) error { 203 actionInfoList := generateActionInfoList(rsm) 204 205 if len(actionInfoList) == 0 { 206 return nil 207 } 208 209 nextActionInfo := actionInfoList[0] 210 leader := getLeaderPodName(rsm.Status.MembersStatus) 211 ordinal := nextActionInfo.ordinal 212 if nextActionInfo.actionType == jobTypeSwitchover { 213 ordinal = 0 214 } 215 target := getPodName(rsm.Name, ordinal) 216 actionName := getActionName(rsm.Name, int(rsm.Generation), nextActionInfo.ordinal, nextActionInfo.actionType) 217 nextAction := buildAction(rsm, actionName, nextActionInfo.actionType, jobScenarioMembership, leader, target) 218 219 if err := abnormalAnalysis(rsm, nextAction); err != nil { 220 emitAbnormalEvent(transCtx, nextActionInfo.actionType, actionName, err) 221 return err 222 } 223 224 cli, _ := transCtx.Client.(model.GraphClient) 225 return createAction(dag, cli, rsm, nextAction) 226 } 227 228 func generateActionInfoList(rsm *workloads.ReplicatedStateMachine) []*actionInfo { 229 var actionInfoList []*actionInfo 230 memberReadyReplicas := int32(len(rsm.Status.MembersStatus)) 231 232 switch { 233 case memberReadyReplicas < *rsm.Spec.Replicas: 234 // member join 235 // members with ordinal less than 'spec.replicas' should in the active cluster 236 actionTypeList := []string{jobTypeMemberJoinNotifying, jobTypeLogSync, jobTypePromote} 237 for i := memberReadyReplicas; i < *rsm.Spec.Replicas; i++ { 238 actionInfos := generateActionInfos(rsm, int(i), actionTypeList) 239 actionInfoList = append(actionInfoList, actionInfos...) 240 } 241 case memberReadyReplicas > *rsm.Spec.Replicas: 242 // member leave 243 // members with ordinal greater than 'spec.replicas - 1' should not in the active cluster 244 actionTypeList := []string{jobTypeSwitchover, jobTypeMemberLeaveNotifying} 245 for i := memberReadyReplicas - 1; i >= *rsm.Spec.Replicas; i-- { 246 actionInfos := generateActionInfos(rsm, int(i), actionTypeList) 247 actionInfoList = append(actionInfoList, actionInfos...) 248 } 249 } 250 251 return actionInfoList 252 } 253 254 func isPreAction(actionType string) bool { 255 return actionType == jobTypeSwitchover || actionType == jobTypeMemberLeaveNotifying 256 } 257 258 func shouldHaveActions(rsm *workloads.ReplicatedStateMachine) bool { 259 currentReplicas := len(rsm.Status.MembersStatus) 260 expectedReplicas := int(*rsm.Spec.Replicas) 261 262 var actionTypeList []string 263 switch { 264 case currentReplicas > expectedReplicas: 265 actionTypeList = []string{jobTypeSwitchover, jobTypeMemberLeaveNotifying} 266 case currentReplicas < expectedReplicas: 267 actionTypeList = []string{jobTypeMemberJoinNotifying, jobTypeLogSync, jobTypePromote} 268 } 269 for _, actionType := range actionTypeList { 270 if shouldCreateAction(rsm, actionType, nil) { 271 return true 272 } 273 } 274 return false 275 } 276 277 func shouldCreateAction(rsm *workloads.ReplicatedStateMachine, actionType string, checker conditionChecker) bool { 278 if checker != nil && !checker() { 279 return false 280 } 281 reconfiguration := rsm.Spec.MembershipReconfiguration 282 if reconfiguration == nil { 283 return false 284 } 285 switch actionType { 286 case jobTypeSwitchover: 287 return reconfiguration.SwitchoverAction != nil 288 case jobTypeMemberJoinNotifying: 289 return reconfiguration.MemberJoinAction != nil 290 case jobTypeMemberLeaveNotifying: 291 return reconfiguration.MemberLeaveAction != nil 292 case jobTypeLogSync: 293 return reconfiguration.LogSyncAction != nil 294 case jobTypePromote: 295 return reconfiguration.PromoteAction != nil 296 } 297 return false 298 } 299 300 func buildShortActionName(parent string, ordinal int, actionType string) string { 301 return fmt.Sprintf("%s-%d-%s", parent, ordinal, actionType) 302 } 303 304 func getActionOrdinal(actionName string) (int, error) { 305 subMatches := actionNameRegex.FindStringSubmatch(actionName) 306 if len(subMatches) < 5 { 307 return 0, fmt.Errorf("error actionName: %s", actionName) 308 } 309 return strconv.Atoi(subMatches[3]) 310 } 311 312 // all members with ordinal less than action target pod should be in a good replication state: 313 // 1. they should be in membersStatus 314 // 2. they should have a leader 315 func abnormalAnalysis(rsm *workloads.ReplicatedStateMachine, action *batchv1.Job) error { 316 membersStatus := rsm.Status.MembersStatus 317 statusMap := make(map[string]workloads.MemberStatus, len(membersStatus)) 318 for _, status := range membersStatus { 319 statusMap[status.PodName] = status 320 } 321 ordinal, _ := getActionOrdinal(action.Name) 322 currentMembers := ordinal 323 if isPreAction(action.Labels[jobTypeLabel]) { 324 currentMembers = ordinal + 1 325 } 326 var abnormalPodList, leaderPodList []string 327 for i := 0; i < currentMembers; i++ { 328 podName := getPodName(rsm.Name, i) 329 status, ok := statusMap[podName] 330 if !ok { 331 abnormalPodList = append(abnormalPodList, podName) 332 } 333 if status.IsLeader { 334 leaderPodList = append(leaderPodList, podName) 335 } 336 } 337 338 var message string 339 if len(abnormalPodList) > 0 { 340 message = fmt.Sprintf("abnormal pods: %v", abnormalPodList) 341 } 342 switch len(leaderPodList) { 343 case 0: 344 message = fmt.Sprintf("%s, no leader exists", message) 345 case 1: 346 default: 347 message = fmt.Sprintf("%s, too many leaders: %v", message, leaderPodList) 348 } 349 if len(message) > 0 { 350 return fmt.Errorf("cluster unhealthy: %s", message) 351 } 352 353 return nil 354 } 355 356 func generateActionInfos(rsm *workloads.ReplicatedStateMachine, ordinal int, actionTypeList []string) []*actionInfo { 357 var actionInfos []*actionInfo 358 leaderPodName := getLeaderPodName(rsm.Status.MembersStatus) 359 podName := getPodName(rsm.Name, ordinal) 360 for _, actionType := range actionTypeList { 361 checker := func() bool { 362 return podName == leaderPodName 363 } 364 if actionType != jobTypeSwitchover { 365 checker = nil 366 } 367 if !shouldCreateAction(rsm, actionType, checker) { 368 continue 369 } 370 info := &actionInfo{ 371 shortActionName: buildShortActionName(rsm.Name, ordinal, actionType), 372 ordinal: ordinal, 373 actionType: actionType, 374 } 375 actionInfos = append(actionInfos, info) 376 } 377 return actionInfos 378 }