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  }