Skip to content

Commit 3eaae66

Browse files
committed
Try end2end testing the bug
Signed-off-by: Oliver Trosien <oliver.trosien@zalando.de>
1 parent 7f54560 commit 3eaae66

File tree

1 file changed

+188
-0
lines changed

1 file changed

+188
-0
lines changed

operator/autoscaler_test.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,194 @@ func TestGetManagedNodes(t *testing.T) {
729729
require.Equal(t, "1.2.3.4", actual[0].IP)
730730
}
731731

732+
// TestScalingBoundaryChange verifies that when scaling boundaries change,
733+
// the autoscaler correctly detects the need to scale and updates spec.replicas.
734+
// This test validates the fix for the bug where edsReplicas() was prematurely
735+
// enforcing boundary constraints, causing the autoscaler to skip spec updates.
736+
//
737+
// Scenario:
738+
// - Initial state: spec.replicas = 3, spec.scaling.minReplicas = 2
739+
// - Boundary change: set spec.scaling.minReplicas = 4
740+
// - Expected: autoscaler should detect that 3 < 4 and return NodeReplicas = 4
741+
//
742+
// With the bug: edsReplicas() would return max(3, 4) = 4, causing the
743+
// condition (4 != 4) to be false, skipping the spec update.
744+
//
745+
// With the fix: edsReplicas() returns 3 (actual value), allowing the
746+
// condition (4 != 3) to be true, triggering the spec update.
747+
func TestScalingBoundaryChange(t *testing.T) {
748+
// Initial state: cluster running with 3 replicas, minReplicas = 2
749+
initialReplicas := int32(3)
750+
eds := &zv1.ElasticsearchDataSet{
751+
ObjectMeta: metav1.ObjectMeta{
752+
Name: "test-eds",
753+
Namespace: "default",
754+
},
755+
Spec: zv1.ElasticsearchDataSetSpec{
756+
Scaling: &zv1.ElasticsearchDataSetScaling{
757+
Enabled: true,
758+
MinReplicas: 2,
759+
MaxReplicas: 10,
760+
MinShardsPerNode: 1,
761+
MaxShardsPerNode: 10,
762+
MinIndexReplicas: 0,
763+
MaxIndexReplicas: 2,
764+
ScaleUpCPUBoundary: 50,
765+
ScaleUpThresholdDurationSeconds: 60,
766+
ScaleDownCPUBoundary: 20,
767+
ScaleDownThresholdDurationSeconds: 60,
768+
DiskUsagePercentScaledownWatermark: 0,
769+
},
770+
Replicas: &initialReplicas,
771+
},
772+
}
773+
774+
// Verify edsReplicas returns the actual spec.replicas value (3), not max(3, 2)
775+
actualReplicas := edsReplicas(eds)
776+
require.Equal(t, int32(3), actualReplicas, "edsReplicas should return actual spec.replicas value, not max(spec.replicas, minReplicas)")
777+
778+
// Now simulate a boundary change: increase minReplicas to 4
779+
eds.Spec.Scaling.MinReplicas = 4
780+
781+
// Create some test indices to trigger scaling logic
782+
esIndices := map[string]ESIndex{
783+
"test-index": {
784+
Index: "test-index",
785+
Replicas: 1,
786+
Primaries: 2,
787+
},
788+
}
789+
esNodes := []ESNode{}
790+
791+
as := systemUnderTest(eds, nil, nil)
792+
793+
// Call calculateScalingOperation with UP hint
794+
scalingOperation := as.calculateScalingOperation(esIndices, esNodes, UP)
795+
796+
// Verify that the autoscaler correctly identifies scaling is needed
797+
require.NotNil(t, scalingOperation.NodeReplicas, "NodeReplicas should not be nil when scaling is needed")
798+
require.Equal(t, int32(4), *scalingOperation.NodeReplicas, "Should scale to minReplicas (4)")
799+
require.Equal(t, UP, scalingOperation.ScalingDirection, "Should indicate scale up direction")
800+
801+
// The key assertion: with the fix, edsReplicas() still returns 3 (not 4)
802+
// This allows the comparison at operator/elasticsearch.go:945 to work correctly:
803+
// if *scalingOperation.NodeReplicas != currentReplicas (4 != 3 = true)
804+
currentReplicasAfter := edsReplicas(eds)
805+
require.Equal(t, int32(3), currentReplicasAfter, "edsReplicas should still return actual spec.replicas (3), allowing autoscaler to detect change")
806+
require.NotEqual(t, *scalingOperation.NodeReplicas, currentReplicasAfter, "Target replicas (4) should differ from current replicas (3), triggering spec update")
807+
}
808+
809+
// TestScalingBoundaryChangeScaleDown verifies the same behavior for scale-down scenarios.
810+
// When maxReplicas is decreased below current replicas, the autoscaler should detect
811+
// the need to scale down based on shard distribution and boundary constraints.
812+
func TestScalingBoundaryChangeScaleDown(t *testing.T) {
813+
// Initial state: cluster running with 6 replicas, maxReplicas = 10
814+
// We have enough shards to justify 6 nodes initially
815+
initialReplicas := int32(6)
816+
eds := &zv1.ElasticsearchDataSet{
817+
ObjectMeta: metav1.ObjectMeta{
818+
Name: "test-eds",
819+
Namespace: "default",
820+
},
821+
Spec: zv1.ElasticsearchDataSetSpec{
822+
Scaling: &zv1.ElasticsearchDataSetScaling{
823+
Enabled: true,
824+
MinReplicas: 1,
825+
MaxReplicas: 10,
826+
MinShardsPerNode: 2,
827+
MaxShardsPerNode: 4,
828+
MinIndexReplicas: 1,
829+
MaxIndexReplicas: 2,
830+
ScaleUpCPUBoundary: 50,
831+
ScaleUpThresholdDurationSeconds: 60,
832+
ScaleDownCPUBoundary: 20,
833+
ScaleDownThresholdDurationSeconds: 60,
834+
DiskUsagePercentScaledownWatermark: 0,
835+
},
836+
Replicas: &initialReplicas,
837+
},
838+
}
839+
840+
// Verify edsReplicas returns the actual spec.replicas value (6)
841+
actualReplicas := edsReplicas(eds)
842+
require.Equal(t, int32(6), actualReplicas, "edsReplicas should return actual spec.replicas value")
843+
844+
// Now simulate a boundary change: decrease maxReplicas to 3
845+
eds.Spec.Scaling.MaxReplicas = 3
846+
847+
// Create test indices: 2 indices with 2 primaries each and 2 replicas
848+
// Total shards: 2 * 2 * (2+1) = 12 shards
849+
// With MaxShardsPerNode=4, we need at least 3 nodes
850+
// But after scale-down, we'll reduce to 1 replica: 2 * 2 * (1+1) = 8 shards
851+
// With MaxShardsPerNode=4, we need 2 nodes, which fits within maxReplicas=3
852+
esIndices := map[string]ESIndex{
853+
"test-index-1": {
854+
Index: "test-index-1",
855+
Replicas: 2,
856+
Primaries: 2,
857+
},
858+
"test-index-2": {
859+
Index: "test-index-2",
860+
Replicas: 2,
861+
Primaries: 2,
862+
},
863+
}
864+
esNodes := []ESNode{}
865+
866+
as := systemUnderTest(eds, nil, nil)
867+
868+
// Call calculateScalingOperation with DOWN hint
869+
scalingOperation := as.calculateScalingOperation(esIndices, esNodes, DOWN)
870+
871+
// Verify that the autoscaler correctly identifies scaling down is needed
872+
require.NotNil(t, scalingOperation.NodeReplicas, "NodeReplicas should not be nil when scaling is needed")
873+
// The autoscaler will want to scale down index replicas from 2 to 1
874+
// and adjust node count accordingly, bounded by maxReplicas=3
875+
require.LessOrEqual(t, *scalingOperation.NodeReplicas, int32(3), "Should not exceed maxReplicas (3)")
876+
require.Equal(t, DOWN, scalingOperation.ScalingDirection, "Should indicate scale down direction")
877+
878+
// Verify edsReplicas() still returns the actual value, not the bounded value
879+
currentReplicasAfter := edsReplicas(eds)
880+
require.Equal(t, int32(6), currentReplicasAfter, "edsReplicas should still return actual spec.replicas (6)")
881+
require.NotEqual(t, *scalingOperation.NodeReplicas, currentReplicasAfter, "Target replicas should differ from current replicas (6), triggering spec update")
882+
}
883+
884+
// TestInitialScalingWithNilReplicas verifies that when spec.replicas is nil
885+
// and autoscaling is enabled, edsReplicas() returns 0, allowing the autoscaler
886+
// to make an explicit initial scaling decision.
887+
func TestInitialScalingWithNilReplicas(t *testing.T) {
888+
eds := &zv1.ElasticsearchDataSet{
889+
ObjectMeta: metav1.ObjectMeta{
890+
Name: "test-eds",
891+
Namespace: "default",
892+
},
893+
Spec: zv1.ElasticsearchDataSetSpec{
894+
Scaling: &zv1.ElasticsearchDataSetScaling{
895+
Enabled: true,
896+
MinReplicas: 3,
897+
MaxReplicas: 10,
898+
MinShardsPerNode: 1,
899+
MaxShardsPerNode: 10,
900+
MinIndexReplicas: 0,
901+
MaxIndexReplicas: 2,
902+
ScaleUpCPUBoundary: 50,
903+
ScaleUpThresholdDurationSeconds: 60,
904+
ScaleDownCPUBoundary: 20,
905+
ScaleDownThresholdDurationSeconds: 60,
906+
DiskUsagePercentScaledownWatermark: 0,
907+
},
908+
Replicas: nil, // Not yet initialized
909+
},
910+
}
911+
912+
// With the fix: should return 0 when replicas is nil and autoscaling is enabled
913+
actualReplicas := edsReplicas(eds)
914+
require.Equal(t, int32(0), actualReplicas, "edsReplicas should return 0 when spec.replicas is nil and autoscaling is enabled")
915+
916+
// This allows the autoscaler to initialize spec.replicas properly
917+
// The autoscaler will see currentReplicas = 0 and can scale to minReplicas
918+
}
919+
732920
func systemUnderTest(eds *zv1.ElasticsearchDataSet, metricSet *zv1.ElasticsearchMetricSet, pods []v1.Pod) *AutoScaler {
733921
es := &ESResource{
734922
ElasticsearchDataSet: eds,

0 commit comments

Comments
 (0)