github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/engine/servermaster/scheduler/resource_filter.go (about)

     1  // Copyright 2022 PingCAP, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package scheduler
    15  
    16  import (
    17  	"context"
    18  
    19  	"github.com/pingcap/log"
    20  	"github.com/pingcap/tiflow/engine/model"
    21  	resModel "github.com/pingcap/tiflow/engine/pkg/externalresource/model"
    22  	schedModel "github.com/pingcap/tiflow/engine/servermaster/scheduler/model"
    23  	"github.com/pingcap/tiflow/pkg/errors"
    24  	"go.uber.org/zap"
    25  	"golang.org/x/exp/slices"
    26  )
    27  
    28  // PlacementConstrainer describes an object that provides
    29  // the placement constraint for an external resource.
    30  type PlacementConstrainer interface {
    31  	GetPlacementConstraint(
    32  		ctx context.Context,
    33  		resourceKey resModel.ResourceKey,
    34  	) (resModel.ExecutorID, bool, error)
    35  }
    36  
    37  // MockPlacementConstrainer uses a resource executor binding map to implement PlacementConstrainer
    38  type MockPlacementConstrainer struct {
    39  	ResourceList map[resModel.ResourceKey]model.ExecutorID
    40  }
    41  
    42  // GetPlacementConstraint implements PlacementConstrainer.GetPlacementConstraint
    43  func (c *MockPlacementConstrainer) GetPlacementConstraint(
    44  	_ context.Context,
    45  	resourceKey resModel.ResourceKey,
    46  ) (resModel.ExecutorID, bool, error) {
    47  	executorID, exists := c.ResourceList[resourceKey]
    48  	if !exists {
    49  		return "", false, errors.ErrResourceDoesNotExist.GenWithStackByArgs(resourceKey.ID)
    50  	}
    51  	if executorID == "" {
    52  		return "", false, nil
    53  	}
    54  	return executorID, true, nil
    55  }
    56  
    57  // resourceFilter filters candidate executors by
    58  // the request's resource requirements.
    59  type resourceFilter struct {
    60  	constrainer PlacementConstrainer
    61  }
    62  
    63  func newResourceFilter(constrainer PlacementConstrainer) *resourceFilter {
    64  	return &resourceFilter{constrainer: constrainer}
    65  }
    66  
    67  const (
    68  	noExecutorConstraint = ""
    69  )
    70  
    71  // getExecutor locates one unique executor onto which the task should be scheduled.
    72  // If no placement requirement is found, noExecutorConstraint is returned.
    73  func (f *resourceFilter) getExecutor(
    74  	ctx context.Context,
    75  	request *schedModel.SchedulerRequest,
    76  ) (model.ExecutorID, error) {
    77  	var (
    78  		lastResourceID resModel.ResourceID
    79  		ret            model.ExecutorID
    80  	)
    81  
    82  	ret = noExecutorConstraint
    83  	for _, resource := range request.ExternalResources {
    84  		resourceID := resource.ID
    85  		// GetPlacementConstraint() may incur database queries.
    86  		// Note the performance.
    87  		executorID, hasConstraint, err := f.constrainer.GetPlacementConstraint(ctx, resource)
    88  		if err != nil {
    89  			return noExecutorConstraint, err
    90  		}
    91  		if !hasConstraint {
    92  			// The resource requires no special placement.
    93  			log.Debug("No constraint is found for resource",
    94  				zap.String("resource-id", resourceID))
    95  			continue
    96  		}
    97  		log.Info("Found resource constraint for resource",
    98  			zap.String("resource-id", resourceID),
    99  			zap.String("executor-id", string(executorID)))
   100  
   101  		if ret != noExecutorConstraint && ret != executorID {
   102  			// Conflicting constraints.
   103  			// We are forced to schedule the task to
   104  			// two different executors, which is impossible.
   105  			log.Warn("Conflicting resource constraints",
   106  				zap.Any("resources", request.ExternalResources))
   107  			return noExecutorConstraint, errors.ErrResourceConflict.
   108  				GenWithStackByArgs(resourceID, lastResourceID, ret, executorID)
   109  		}
   110  		ret = executorID
   111  		lastResourceID = resourceID
   112  	}
   113  	return ret, nil
   114  }
   115  
   116  func (f *resourceFilter) GetEligibleExecutors(
   117  	ctx context.Context,
   118  	request *schedModel.SchedulerRequest,
   119  	candidates []model.ExecutorID,
   120  ) ([]model.ExecutorID, error) {
   121  	if len(request.ExternalResources) == 0 {
   122  		return candidates, nil
   123  	}
   124  
   125  	finalCandidate, err := f.getExecutor(ctx, request)
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  
   130  	if finalCandidate == noExecutorConstraint {
   131  		// Any executor is good.
   132  		return candidates, nil
   133  	}
   134  
   135  	// Check that finalCandidate is found in the input candidates.
   136  	if slices.Index(candidates, finalCandidate) == -1 {
   137  		return nil, errors.ErrFilterNoResult.GenWithStackByArgs("resource")
   138  	}
   139  	return []model.ExecutorID{finalCandidate}, nil
   140  }