|  | 
|  | 1 | +/* | 
|  | 2 | +Copyright 2025 The Kubernetes Authors. | 
|  | 3 | +
 | 
|  | 4 | +Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | 5 | +you may not use this file except in compliance with the License. | 
|  | 6 | +You may obtain a copy of the License at | 
|  | 7 | +
 | 
|  | 8 | +    http://www.apache.org/licenses/LICENSE-2.0 | 
|  | 9 | +
 | 
|  | 10 | +Unless required by applicable law or agreed to in writing, software | 
|  | 11 | +distributed under the License is distributed on an "AS IS" BASIS, | 
|  | 12 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | 13 | +See the License for the specific language governing permissions and | 
|  | 14 | +limitations under the License. | 
|  | 15 | +*/ | 
|  | 16 | + | 
|  | 17 | +package common | 
|  | 18 | + | 
|  | 19 | +import ( | 
|  | 20 | +	"context" | 
|  | 21 | +	"errors" | 
|  | 22 | +	"fmt" | 
|  | 23 | +	"time" | 
|  | 24 | + | 
|  | 25 | +	"k8s.io/klog/v2" | 
|  | 26 | +	"k8s.io/perf-tests/clusterloader2/pkg/measurement" | 
|  | 27 | +	measurementutil "k8s.io/perf-tests/clusterloader2/pkg/measurement/util" | 
|  | 28 | +	"k8s.io/perf-tests/clusterloader2/pkg/provider" | 
|  | 29 | +	"k8s.io/perf-tests/clusterloader2/pkg/util" | 
|  | 30 | +) | 
|  | 31 | + | 
|  | 32 | +const ( | 
|  | 33 | +	scaleNodesMeasurementName = "ScaleNodes" | 
|  | 34 | +	defaultScalingTimeout     = 30 * time.Minute | 
|  | 35 | +	nodeCountCheckInterval    = 30 * time.Second | 
|  | 36 | +) | 
|  | 37 | + | 
|  | 38 | +type scaleNodesMeasurement struct{} | 
|  | 39 | + | 
|  | 40 | +func init() { | 
|  | 41 | +	if err := measurement.Register(scaleNodesMeasurementName, createScaleNodesMeasurement); err != nil { | 
|  | 42 | +		klog.Fatalf("Cannot register %s: %v", scaleNodesMeasurementName, err) | 
|  | 43 | +	} | 
|  | 44 | +} | 
|  | 45 | + | 
|  | 46 | +func createScaleNodesMeasurement() measurement.Measurement { | 
|  | 47 | +	return &scaleNodesMeasurement{} | 
|  | 48 | +} | 
|  | 49 | + | 
|  | 50 | +// Execute performs the node scaling operation with the specified parameters | 
|  | 51 | +func (n *scaleNodesMeasurement) Execute(config *measurement.Config) ([]measurement.Summary, error) { | 
|  | 52 | +	// Get parameters from config.Params | 
|  | 53 | +	providerName, err := util.GetString(config.Params, "provider") | 
|  | 54 | +	if err != nil { | 
|  | 55 | +		return nil, err | 
|  | 56 | +	} | 
|  | 57 | +	region, err := util.GetString(config.Params, "region") | 
|  | 58 | +	if err != nil { | 
|  | 59 | +		return nil, err | 
|  | 60 | +	} | 
|  | 61 | +	clusterName, err := util.GetString(config.Params, "clusterName") | 
|  | 62 | +	if err != nil { | 
|  | 63 | +		return nil, err | 
|  | 64 | +	} | 
|  | 65 | +	batchSize, err := util.GetInt(config.Params, "batchSize") | 
|  | 66 | +	if err != nil { | 
|  | 67 | +		return nil, err | 
|  | 68 | +	} | 
|  | 69 | +	intervalSeconds, err := util.GetInt(config.Params, "intervalSeconds") | 
|  | 70 | +	if err != nil { | 
|  | 71 | +		return nil, err | 
|  | 72 | +	} | 
|  | 73 | +	targetNodeCount, err := util.GetInt(config.Params, "targetNodeCount") | 
|  | 74 | +	if err != nil { | 
|  | 75 | +		return nil, err | 
|  | 76 | +	} | 
|  | 77 | + | 
|  | 78 | +	// Get timeout with default value if not specified | 
|  | 79 | +	timeout, err := util.GetDurationOrDefault(config.Params, "timeout", defaultScalingTimeout) | 
|  | 80 | +	if err != nil { | 
|  | 81 | +		return nil, err | 
|  | 82 | +	} | 
|  | 83 | + | 
|  | 84 | +	// Initialize provider specific scaler | 
|  | 85 | +	scaler, err := provider.CreateNodeScaler(providerName, region, clusterName) | 
|  | 86 | +	if err != nil { | 
|  | 87 | +		return nil, fmt.Errorf("failed to create node scaler: %v", err) | 
|  | 88 | +	} | 
|  | 89 | + | 
|  | 90 | +	// Start scaling operation | 
|  | 91 | +	klog.Infof("Starting node scaling: target=%d, batchSize=%d/interval, interval=%ds, timeout=%v", | 
|  | 92 | +		targetNodeCount, batchSize, intervalSeconds, timeout) | 
|  | 93 | + | 
|  | 94 | +	// Start the scaling operation in a goroutine | 
|  | 95 | +	errCh := make(chan error) | 
|  | 96 | +	ctx, cancel := context.WithTimeout(context.Background(), timeout) | 
|  | 97 | +	defer cancel() | 
|  | 98 | +	go func() { | 
|  | 99 | +		errCh <- scaler.ScaleNodes(ctx, batchSize, intervalSeconds, targetNodeCount) | 
|  | 100 | +	}() | 
|  | 101 | + | 
|  | 102 | +	// Create stop channel for timeout | 
|  | 103 | +	stopCh := make(chan struct{}) | 
|  | 104 | +	time.AfterFunc(timeout, func() { | 
|  | 105 | +		close(stopCh) | 
|  | 106 | +	}) | 
|  | 107 | + | 
|  | 108 | +	// Set up options for waiting on nodes | 
|  | 109 | +	options := &measurementutil.WaitForNodeOptions{ | 
|  | 110 | +		Selector:             util.NewObjectSelector(), | 
|  | 111 | +		MinDesiredNodeCount:  targetNodeCount, | 
|  | 112 | +		MaxDesiredNodeCount:  targetNodeCount, | 
|  | 113 | +		CallerName:           n.String(), | 
|  | 114 | +		WaitForNodesInterval: nodeCountCheckInterval, | 
|  | 115 | +	} | 
|  | 116 | + | 
|  | 117 | +	// Wait for either the scaling operation to fail or nodes to be ready | 
|  | 118 | +	select { | 
|  | 119 | +	case err := <-errCh: | 
|  | 120 | +		if err != nil { | 
|  | 121 | +			if errors.Is(err, context.DeadlineExceeded) { | 
|  | 122 | +				return nil, fmt.Errorf("scaling operation timed out after %v", timeout) | 
|  | 123 | +			} | 
|  | 124 | +			return nil, fmt.Errorf("failed to scale nodes: %v", err) | 
|  | 125 | +		} | 
|  | 126 | +		// Scaling operation completed, now wait for nodes to be ready | 
|  | 127 | +		if err := measurementutil.WaitForNodes(config.ClusterFramework.GetClientSets().GetClient(), stopCh, options); err != nil { | 
|  | 128 | +			return nil, err | 
|  | 129 | +		} | 
|  | 130 | +		return nil, nil | 
|  | 131 | +	case <-stopCh: | 
|  | 132 | +		return nil, fmt.Errorf("timeout while waiting for scaling operation to complete after %v", timeout) | 
|  | 133 | +	} | 
|  | 134 | +} | 
|  | 135 | + | 
|  | 136 | +// Dispose cleans up after the measurement. | 
|  | 137 | +func (*scaleNodesMeasurement) Dispose() {} | 
|  | 138 | + | 
|  | 139 | +// String returns string representation of this measurement. | 
|  | 140 | +func (*scaleNodesMeasurement) String() string { | 
|  | 141 | +	return scaleNodesMeasurementName | 
|  | 142 | +} | 
0 commit comments