This is an automated email from the ASF dual-hosted git repository.
gyfora pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-kubernetes-operator.git
The following commit(s) were added to refs/heads/main by this push:
new c305a7ce [FLINK-40389] Source parallelism is not capped at the
partition count (#1179)
c305a7ce is described below
commit c305a7ce554e8dd1a2381867824a260d43e70bbc
Author: Dennis-Mircea Ciupitu <[email protected]>
AuthorDate: Fri Aug 14 16:54:03 2026 +0300
[FLINK-40389] Source parallelism is not capped at the partition count
(#1179)
---
.../autoscaler/alignment/BuiltInAlignmentMode.java | 7 +-
.../autoscaler/alignment/ParallelismAligner.java | 5 ++
.../flink/autoscaler/JobVertexScalerTest.java | 43 ++++++++++
.../autoscaler/alignment/AlignmentModeTest.java | 99 ++++++++++++++++++++++
4 files changed, 153 insertions(+), 1 deletion(-)
diff --git
a/flink-autoscaler/src/main/java/org/apache/flink/autoscaler/alignment/BuiltInAlignmentMode.java
b/flink-autoscaler/src/main/java/org/apache/flink/autoscaler/alignment/BuiltInAlignmentMode.java
index 37dcc731..a85deab4 100644
---
a/flink-autoscaler/src/main/java/org/apache/flink/autoscaler/alignment/BuiltInAlignmentMode.java
+++
b/flink-autoscaler/src/main/java/org/apache/flink/autoscaler/alignment/BuiltInAlignmentMode.java
@@ -69,7 +69,12 @@ public enum BuiltInAlignmentMode implements
ParallelismAlignmentMode, DescribedE
*/
private static int alignOrKeepTarget(Context<?> ctx, boolean
acceptLoadReducing) {
int aligned = ParallelismAligner.firstAlignedInRegion(ctx,
acceptLoadReducing);
- return aligned > 0 ? aligned : ctx.getNewParallelism();
+ if (aligned > 0) {
+ return aligned;
+ }
+ // Nothing aligned in the region: keep the target, but never above the
cap, since
+ // subtasks past the source partition count are assigned no split.
+ return Math.min(ctx.getNewParallelism(),
ParallelismAligner.upperBoundForAlignment(ctx));
}
private final InlineElement description;
diff --git
a/flink-autoscaler/src/main/java/org/apache/flink/autoscaler/alignment/ParallelismAligner.java
b/flink-autoscaler/src/main/java/org/apache/flink/autoscaler/alignment/ParallelismAligner.java
index b58da779..65c02d68 100644
---
a/flink-autoscaler/src/main/java/org/apache/flink/autoscaler/alignment/ParallelismAligner.java
+++
b/flink-autoscaler/src/main/java/org/apache/flink/autoscaler/alignment/ParallelismAligner.java
@@ -280,6 +280,11 @@ public final class ParallelismAligner {
private static boolean invertsDirection(
ParallelismAlignmentMode.Context<?> ctx, int candidate) {
+ // Exception to the direction check below: the cap is the most
subtasks the vertex can
+ // use, so dropping onto it corrects an over-provisioned vertex rather
than inverting it.
+ if (candidate == upperBoundForAlignment(ctx) && candidate <
ctx.getCurrentParallelism()) {
+ return false;
+ }
return (isScaleUp(ctx) && candidate <= ctx.getCurrentParallelism())
|| (!isScaleUp(ctx) && candidate >=
ctx.getCurrentParallelism());
}
diff --git
a/flink-autoscaler/src/test/java/org/apache/flink/autoscaler/JobVertexScalerTest.java
b/flink-autoscaler/src/test/java/org/apache/flink/autoscaler/JobVertexScalerTest.java
index 8cba8221..eb7013b3 100644
---
a/flink-autoscaler/src/test/java/org/apache/flink/autoscaler/JobVertexScalerTest.java
+++
b/flink-autoscaler/src/test/java/org/apache/flink/autoscaler/JobVertexScalerTest.java
@@ -20,6 +20,7 @@ package org.apache.flink.autoscaler;
import org.apache.flink.api.common.JobID;
import org.apache.flink.api.common.JobStatus;
import org.apache.flink.autoscaler.JobVertexScaler.ParallelismChange;
+import org.apache.flink.autoscaler.alignment.BuiltInAlignmentMode;
import org.apache.flink.autoscaler.alignment.KeyGroupOrPartitionsAdjustMode;
import org.apache.flink.autoscaler.config.AutoScalerOptions;
import org.apache.flink.autoscaler.event.TestingEventCollector;
@@ -1210,6 +1211,48 @@ public class JobVertexScalerTest {
context));
}
+ @Test
+ public void testSourceCappedAtPartitionCountWhenOverProvisioned() {
+ final var vertex = new JobVertexID();
+ // 15 partitions, running at 100: both a requested scale-up and
scale-down land on 15.
+ // setup() pins the legacy EVENLY_SPREAD, so that one is covered first.
+ assertEquals(15, scaleOverProvisionedSource(vertex, 1.8));
+ assertEquals(15, scaleOverProvisionedSource(vertex, 0.5));
+
+ conf.set(
+ AutoScalerOptions.SCALING_KEY_GROUP_PARTITIONS_ADJUST_MODE,
+ KeyGroupOrPartitionsAdjustMode.MAXIMIZE_UTILISATION);
+ assertEquals(15, scaleOverProvisionedSource(vertex, 1.8));
+ assertEquals(15, scaleOverProvisionedSource(vertex, 0.5));
+
+ // The new key takes precedence over the deprecated one set above.
+ for (var mode :
+ List.of(BuiltInAlignmentMode.BALANCED,
BuiltInAlignmentMode.EVENLY_SPREAD)) {
+ conf.set(AutoScalerOptions.ALIGNMENT_MODE, mode.name());
+ assertEquals(15, scaleOverProvisionedSource(vertex, 1.8),
mode.name());
+ assertEquals(15, scaleOverProvisionedSource(vertex, 0.5),
mode.name());
+ }
+
+ // OFF opts out of alignment, so the computed target is used as-is.
+ conf.set(AutoScalerOptions.ALIGNMENT_MODE,
BuiltInAlignmentMode.OFF.name());
+ assertEquals(180, scaleOverProvisionedSource(vertex, 1.8));
+ }
+
+ private int scaleOverProvisionedSource(JobVertexID vertex, double
scaleFactor) {
+ return vertexScaler.scale(
+ vertex,
+ 100,
+ List.of(),
+ 15,
+ 180,
+ scaleFactor,
+ 1,
+ Integer.MAX_VALUE,
+ Map.of(),
+ null,
+ context);
+ }
+
@Test
public void testSendingScalingLimitedEvents() {
var jobVertexID = new JobVertexID();
diff --git
a/flink-autoscaler/src/test/java/org/apache/flink/autoscaler/alignment/AlignmentModeTest.java
b/flink-autoscaler/src/test/java/org/apache/flink/autoscaler/alignment/AlignmentModeTest.java
index 90cd0f4b..33aa4223 100644
---
a/flink-autoscaler/src/test/java/org/apache/flink/autoscaler/alignment/AlignmentModeTest.java
+++
b/flink-autoscaler/src/test/java/org/apache/flink/autoscaler/alignment/AlignmentModeTest.java
@@ -23,14 +23,19 @@ import org.apache.flink.autoscaler.topology.ShipStrategy;
import org.apache.flink.configuration.Configuration;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Stream;
import static
org.apache.flink.autoscaler.TestingAutoscalerUtils.createDefaultJobAutoScalerContext;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
/**
* Tests for the built-in and legacy {@link ParallelismAlignmentMode}s and
{@link
@@ -123,6 +128,100 @@ class AlignmentModeTest {
assertThat(emitted).hasValue(1);
}
+ @SuppressWarnings("deprecation")
+ static Stream<ParallelismAlignmentMode> aligningModes() {
+ return Stream.of(
+ BuiltInAlignmentMode.BALANCED,
+ BuiltInAlignmentMode.EVENLY_SPREAD,
+ KeyGroupOrPartitionsAdjustMode.EVENLY_SPREAD,
+ KeyGroupOrPartitionsAdjustMode.MAXIMIZE_UTILISATION);
+ }
+
+ @SuppressWarnings("deprecation")
+ static Stream<KeyGroupOrPartitionsAdjustMode> legacyModes() {
+ return Stream.of(
+ KeyGroupOrPartitionsAdjustMode.EVENLY_SPREAD,
+ KeyGroupOrPartitionsAdjustMode.MAXIMIZE_UTILISATION);
+ }
+
+ /** Every aligning mode against a target above the cap, on both sides of
the current value. */
+ static Stream<Arguments> cappedTargets() {
+ return aligningModes()
+ .flatMap(mode -> Stream.of(180, 50, 16, 15).map(target ->
arguments(mode, target)));
+ }
+
+ @ParameterizedTest(name = "{0}, target {1}")
+ @MethodSource("cappedTargets")
+ void targetAbovePartitionCountIsCappedInEitherDirection(
+ ParallelismAlignmentMode mode, int target) {
+ // 15 partitions, running at 100: 15 is the ceiling whether the target
is above it (180),
+ // below it (50, 16), or already on it (15).
+ assertThat(mode.alignParallelism(ctx(100, target, 15,
180))).isEqualTo(15);
+ }
+
+ @Test
+ void offKeepsTheTargetAbovePartitionCount() {
+ // OFF opts out of alignment altogether, so the over-provisioning is
the user's choice.
+ assertThat(BuiltInAlignmentMode.OFF.alignParallelism(ctx(100, 180, 15,
180)))
+ .isEqualTo(180);
+ assertThat(BuiltInAlignmentMode.OFF.alignParallelism(ctx(100, 50, 15,
180))).isEqualTo(50);
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("aligningModes")
+ void keyedVertexIsCappedAtItsKeyGroupCount(ParallelismAlignmentMode mode) {
+ // Without source partitions the cap is the key group count. scale()
already clamps the
+ // target to maxParallelism, so only sources reach this in practice.
+ assertThat(mode.alignParallelism(ctx(20, 18, 0, 12))).isEqualTo(12);
+ }
+
+ /** Legacy modes only, and only targets that deviate from the cap, so an
event is expected. */
+ static Stream<Arguments> legacyCappedTargets() {
+ return legacyModes()
+ .flatMap(mode -> Stream.of(180, 50, 16).map(target ->
arguments(mode, target)));
+ }
+
+ @ParameterizedTest(name = "{0}, target {1}")
+ @MethodSource("legacyCappedTargets")
+ @SuppressWarnings("deprecation")
+ void legacyEmitsWheneverCappedAtPartitionCount(
+ KeyGroupOrPartitionsAdjustMode mode, int target) {
+ // Capping is not an inversion, but it still deviates from the target,
so the event stays.
+ assertThat(mode.alignParallelism(ctx(100, target, 15, 180),
emitter)).isEqualTo(15);
+ assertThat(emitted).hasValue(1);
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("legacyModes")
+ @SuppressWarnings("deprecation")
+ void
alreadyAtThePartitionCountBlocksAScaleUp(KeyGroupOrPartitionsAdjustMode mode) {
+ // Already on the cap: the candidate is not below current, so the
scale-up stays blocked.
+ assertThat(mode.alignParallelism(ctx(15, 20, 15, 180),
emitter)).isEqualTo(15);
+ assertThat(emitted).hasValue(1);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ void movingUpToTheCapOnAScaleDownIsStillAnInversion() {
+ // The lower limit clamps the fallback up onto the cap (12), above the
current 10, so
+ // taking it would invert the scale-down.
+ var c = ctx(10, 8, 12, 128, 12, 12, List.of(ShipStrategy.HASH));
+
assertThat(KeyGroupOrPartitionsAdjustMode.EVENLY_SPREAD.alignParallelism(c,
emitter))
+ .isEqualTo(10);
+ assertThat(emitted).hasValue(1);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ void lowerLimitClampAboveCurrentStillBlocksAScaleDown() {
+ // The same inversion, but below the cap (candidate 11, cap 30): the
exemption must not
+ // reach it.
+ var c = ctx(10, 8, 35, 128, 11, 30, List.of(ShipStrategy.HASH));
+
assertThat(KeyGroupOrPartitionsAdjustMode.EVENLY_SPREAD.alignParallelism(c,
emitter))
+ .isEqualTo(10);
+ assertThat(emitted).hasValue(1);
+ }
+
@Test
void modesDoNotApplyToNonSourceNonHashVertices() {
var rebalance = ctx(16, 24, 0, 128, 1, Integer.MAX_VALUE,
List.of(ShipStrategy.REBALANCE));