3030
3131import java .time .Instant ;
3232import java .util .HashMap ;
33+ import java .util .LinkedList ;
3334import java .util .Map ;
3435
36+ import static org .apache .flink .util .Preconditions .checkState ;
37+
3538/** All delayed scale down requests. */
3639public class DelayedScaleDown {
3740
41+ @ Data
42+ public static class RecommendedParallelism {
43+ @ Nonnull private final Instant triggerTime ;
44+ private final int parallelism ;
45+ private final boolean outsideUtilizationBound ;
46+
47+ @ JsonCreator
48+ public RecommendedParallelism (
49+ @ Nonnull @ JsonProperty ("triggerTime" ) Instant triggerTime ,
50+ @ JsonProperty ("parallelism" ) int parallelism ,
51+ @ JsonProperty ("outsideUtilizationBound" ) boolean outsideUtilizationBound ) {
52+ this .triggerTime = triggerTime ;
53+ this .parallelism = parallelism ;
54+ this .outsideUtilizationBound = outsideUtilizationBound ;
55+ }
56+ }
57+
3858 /** The delayed scale down info for vertex. */
3959 @ Data
4060 public static class VertexDelayedScaleDownInfo {
4161 private final Instant firstTriggerTime ;
42- private int maxRecommendedParallelism ;
62+
63+ /**
64+ * In theory, it maintains all recommended parallelisms at each time within the past
65+ * `scale-down.interval` window period, so all recommended parallelisms before the window
66+ * start time will be evicted.
67+ *
68+ * <p>Also, if latest parallelism is greater than the past parallelism, all smaller
69+ * parallelism in the past never be the max recommended parallelism, so we could evict all
70+ * smaller parallelism in the past. It's a general optimization for calculating max value
71+ * for sliding window. So we only need to maintain a list with monotonically decreasing
72+ * parallelism within the past window, and the first parallelism will be the max recommended
73+ * parallelism within the past `scale-down.interval` window period.
74+ */
75+ private final LinkedList <RecommendedParallelism > recommendedParallelisms ;
76+
77+ public VertexDelayedScaleDownInfo (Instant firstTriggerTime ) {
78+ this .firstTriggerTime = firstTriggerTime ;
79+ this .recommendedParallelisms = new LinkedList <>();
80+ }
4381
4482 @ JsonCreator
4583 public VertexDelayedScaleDownInfo (
4684 @ JsonProperty ("firstTriggerTime" ) Instant firstTriggerTime ,
47- @ JsonProperty ("maxRecommendedParallelism" ) int maxRecommendedParallelism ) {
85+ @ JsonProperty ("recommendedParallelisms" )
86+ LinkedList <RecommendedParallelism > recommendedParallelisms ) {
4887 this .firstTriggerTime = firstTriggerTime ;
49- this .maxRecommendedParallelism = maxRecommendedParallelism ;
88+ this .recommendedParallelisms = recommendedParallelisms ;
89+ }
90+
91+ /** Record current recommended parallelism. */
92+ public void recordRecommendedParallelism (
93+ Instant triggerTime , int parallelism , boolean outsideUtilizationBound ) {
94+ // Evict all recommended parallelisms that are lower than or equal to the latest
95+ // parallelism. When the past parallelism is equal to the latest parallelism,
96+ // triggerTime needs to be updated, so it also needs to be evicted.
97+ while (!recommendedParallelisms .isEmpty ()
98+ && recommendedParallelisms .peekLast ().getParallelism () <= parallelism ) {
99+ recommendedParallelisms .pollLast ();
100+ }
101+
102+ recommendedParallelisms .addLast (
103+ new RecommendedParallelism (triggerTime , parallelism , outsideUtilizationBound ));
104+ }
105+
106+ @ JsonIgnore
107+ public RecommendedParallelism getMaxRecommendedParallelism (Instant windowStartTime ) {
108+ // Evict all recommended parallelisms before the window start time.
109+ while (!recommendedParallelisms .isEmpty ()
110+ && recommendedParallelisms
111+ .peekFirst ()
112+ .getTriggerTime ()
113+ .isBefore (windowStartTime )) {
114+ recommendedParallelisms .pollFirst ();
115+ }
116+
117+ var maxRecommendedParallelism = recommendedParallelisms .peekFirst ();
118+ checkState (
119+ maxRecommendedParallelism != null ,
120+ "The getMaxRecommendedParallelism should be called after triggering a scale down, it may be a bug." );
121+ return maxRecommendedParallelism ;
50122 }
51123 }
52124
@@ -60,21 +132,28 @@ public DelayedScaleDown() {
60132 this .delayedVertices = new HashMap <>();
61133 }
62134
135+ // TODO : remove this and refactor tests;
136+ public VertexDelayedScaleDownInfo triggerScaleDown (
137+ JobVertexID vertex , Instant triggerTime , int parallelism ) {
138+ return triggerScaleDown (vertex , triggerTime , parallelism , false );
139+ }
140+
63141 /** Trigger a scale down, and return the corresponding {@link VertexDelayedScaleDownInfo}. */
64142 @ Nonnull
65143 public VertexDelayedScaleDownInfo triggerScaleDown (
66- JobVertexID vertex , Instant triggerTime , int parallelism ) {
67- var vertexDelayedScaleDownInfo = delayedVertices .get (vertex );
68- if (vertexDelayedScaleDownInfo == null ) {
69- // It's the first trigger
70- vertexDelayedScaleDownInfo = new VertexDelayedScaleDownInfo (triggerTime , parallelism );
71- delayedVertices .put (vertex , vertexDelayedScaleDownInfo );
72- updated = true ;
73- } else if (parallelism > vertexDelayedScaleDownInfo .getMaxRecommendedParallelism ()) {
74- // Not the first trigger, but the maxRecommendedParallelism needs to be updated.
75- vertexDelayedScaleDownInfo .setMaxRecommendedParallelism (parallelism );
76- updated = true ;
77- }
144+ JobVertexID vertex ,
145+ Instant triggerTime ,
146+ int parallelism ,
147+ boolean outsideUtilizationBound ) {
148+ // The vertexDelayedScaleDownInfo is updated once scale down is triggered due to we need
149+ // update the triggerTime each time.
150+ updated = true ;
151+
152+ var vertexDelayedScaleDownInfo =
153+ delayedVertices .computeIfAbsent (
154+ vertex , k -> new VertexDelayedScaleDownInfo (triggerTime ));
155+ vertexDelayedScaleDownInfo .recordRecommendedParallelism (
156+ triggerTime , parallelism , outsideUtilizationBound );
78157
79158 return vertexDelayedScaleDownInfo ;
80159 }
0 commit comments