This is an automated email from the ASF dual-hosted git repository.

1996fanrui pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git


The following commit(s) were added to refs/heads/master by this push:
     new 8ec1b0b9828 [FLINK-40326][core] Do not over-grant capacity in 
GatedRateLimiter
8ec1b0b9828 is described below

commit 8ec1b0b9828f1d4caf66885005dd7c1ca5b97cc9
Author: Aleksandr Savonin <[email protected]>
AuthorDate: Mon Aug 3 18:44:08 2026 +0200

    [FLINK-40326][core] Do not over-grant capacity in GatedRateLimiter
    
    The decrement in acquire() was generalized from a constant 1 to
    numberOfEvents as part of FLIP-535, but the gate was left closing only at
    capacityLeft <= 0, so a cycle can admit more events than capacityPerCycle.
    The gate is now closed unless the capacity left covers the whole request.
---
 .../source/util/ratelimit/GatedRateLimiter.java    | 16 ++---
 .../source/util/ratelimit/RateLimiter.java         |  2 +-
 .../source/lib/util/GatedRateLimiterTest.java      | 76 +++++++++++++++++++++-
 3 files changed, 84 insertions(+), 10 deletions(-)

diff --git 
a/flink-core/src/main/java/org/apache/flink/api/connector/source/util/ratelimit/GatedRateLimiter.java
 
b/flink-core/src/main/java/org/apache/flink/api/connector/source/util/ratelimit/GatedRateLimiter.java
index 2fa991af013..75fb0b9303a 100644
--- 
a/flink-core/src/main/java/org/apache/flink/api/connector/source/util/ratelimit/GatedRateLimiter.java
+++ 
b/flink-core/src/main/java/org/apache/flink/api/connector/source/util/ratelimit/GatedRateLimiter.java
@@ -28,8 +28,9 @@ import static 
org.apache.flink.util.Preconditions.checkArgument;
 
 /**
  * An implementation of {@link RateLimiter} that completes defined number of 
futures in-between the
- * external notification events. The first cycle completes immediately, 
without waiting for the
- * external notifications.
+ * external notification events. The first cycle does not wait for an external 
notification: its
+ * capacity is available from the start, so requests complete immediately for 
as long as the
+ * capacity left covers them.
  */
 @Internal
 public class GatedRateLimiter<Split extends SourceSplit> implements 
RateLimiter<Split> {
@@ -37,6 +38,9 @@ public class GatedRateLimiter<Split extends SourceSplit> 
implements RateLimiter<
     private final int capacityPerCycle;
     private int capacityLeft;
 
+    /** Completed while the current cycle has capacity left, incomplete once 
it has run out. */
+    private CompletableFuture<Void> gatingFuture = 
CompletableFuture.completedFuture(null);
+
     /**
      * Instantiates a new GatedRateLimiter.
      *
@@ -48,14 +52,10 @@ public class GatedRateLimiter<Split extends SourceSplit> 
implements RateLimiter<
         this.capacityLeft = capacityPerCycle;
     }
 
-    transient CompletableFuture<Void> gatingFuture = null;
-
     @Override
     public CompletionStage<Void> acquire(int numberOfEvents) {
-        if (gatingFuture == null) {
-            gatingFuture = CompletableFuture.completedFuture(null);
-        }
-        if (capacityLeft <= 0) {
+        checkArgument(numberOfEvents > 0, "Number of events has to be a 
positive number.");
+        if (capacityLeft < numberOfEvents) {
             gatingFuture = new CompletableFuture<>();
         }
         return gatingFuture.thenRun(() -> capacityLeft -= numberOfEvents);
diff --git 
a/flink-core/src/main/java/org/apache/flink/api/connector/source/util/ratelimit/RateLimiter.java
 
b/flink-core/src/main/java/org/apache/flink/api/connector/source/util/ratelimit/RateLimiter.java
index 06de7bc7269..bc2a14ab191 100644
--- 
a/flink-core/src/main/java/org/apache/flink/api/connector/source/util/ratelimit/RateLimiter.java
+++ 
b/flink-core/src/main/java/org/apache/flink/api/connector/source/util/ratelimit/RateLimiter.java
@@ -48,7 +48,7 @@ public interface RateLimiter<SplitT extends SourceSplit> {
      * correct functioning, the next invocation of this method should only 
happen after the
      * previously returned future has been completed.
      *
-     * @param numberOfEvents The number of events.
+     * @param numberOfEvents The number of events, which has to be a positive 
number.
      */
     CompletionStage<Void> acquire(int numberOfEvents);
 
diff --git 
a/flink-tests/src/test/java/org/apache/flink/api/connector/source/lib/util/GatedRateLimiterTest.java
 
b/flink-tests/src/test/java/org/apache/flink/api/connector/source/lib/util/GatedRateLimiterTest.java
index 15a034e0f7d..21c905a6374 100644
--- 
a/flink-tests/src/test/java/org/apache/flink/api/connector/source/lib/util/GatedRateLimiterTest.java
+++ 
b/flink-tests/src/test/java/org/apache/flink/api/connector/source/lib/util/GatedRateLimiterTest.java
@@ -18,6 +18,7 @@
 
 package org.apache.flink.api.connector.source.lib.util;
 
+import 
org.apache.flink.api.connector.source.lib.NumberSequenceSource.NumberSequenceSplit;
 import org.apache.flink.api.connector.source.util.ratelimit.GatedRateLimiter;
 
 import org.junit.jupiter.api.Test;
@@ -25,6 +26,7 @@ import org.junit.jupiter.api.Test;
 import java.util.concurrent.CompletionStage;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 class GatedRateLimiterTest {
 
@@ -32,7 +34,8 @@ class GatedRateLimiterTest {
     void testCapacityNotExceededOnCheckpoint() {
         int capacityPerCycle = 5;
 
-        final GatedRateLimiter gatedRateLimiter = new 
GatedRateLimiter(capacityPerCycle);
+        final GatedRateLimiter<NumberSequenceSplit> gatedRateLimiter =
+                new GatedRateLimiter<>(capacityPerCycle);
         for (int x = 0; x < capacityPerCycle; x++) {
             assertThat(gatedRateLimiter.acquire()).isCompleted();
         }
@@ -50,4 +53,75 @@ class GatedRateLimiterTest {
         CompletionStage<Void> postCheckpoint = gatedRateLimiter.acquire();
         assertThat(postCheckpoint).isNotCompleted();
     }
+
+    @Test
+    void testCapacityNotExceededWhenAcquiringMultipleEvents() {
+        int capacityPerCycle = 5;
+
+        final GatedRateLimiter<NumberSequenceSplit> gatedRateLimiter =
+                new GatedRateLimiter<>(capacityPerCycle);
+        assertThat(gatedRateLimiter.acquire(3)).isCompleted();
+
+        // Only two permits are left in this cycle, so a request for three 
events has to wait even
+        // though the remaining capacity is still greater than zero.
+        CompletionStage<Void> exceedsRemainingCapacity = 
gatedRateLimiter.acquire(3);
+        assertThat(exceedsRemainingCapacity).isNotCompleted();
+
+        gatedRateLimiter.notifyCheckpointComplete(0);
+
+        assertThat(exceedsRemainingCapacity).isCompleted();
+    }
+
+    @Test
+    void testRequestLargerThanCapacityIsReleasedByNextCycle() {
+        final GatedRateLimiter<NumberSequenceSplit> gatedRateLimiter = new 
GatedRateLimiter<>(2);
+
+        // A single request may legitimately exceed the capacity of an entire 
cycle. It must not
+        // deadlock: resetting the capacity on the next completed checkpoint 
releases it.
+        CompletionStage<Void> exceedsWholeCycle = gatedRateLimiter.acquire(3);
+        assertThat(exceedsWholeCycle).isNotCompleted();
+
+        gatedRateLimiter.notifyCheckpointComplete(0);
+
+        assertThat(exceedsWholeCycle).isCompleted();
+
+        // Because completing it took 3 events from a cycle that only had 2, a 
further checkpoint is
+        // needed before requests are allowed again.
+        CompletionStage<Void> followingRequest = gatedRateLimiter.acquire(1);
+        assertThat(followingRequest).isNotCompleted();
+
+        gatedRateLimiter.notifyCheckpointComplete(1);
+
+        assertThat(followingRequest).isCompleted();
+        assertThat(gatedRateLimiter.acquire(1)).isCompleted();
+    }
+
+    @Test
+    void testCheckpointCompleteBeforeFirstAcquire() {
+        int capacityPerCycle = 5;
+
+        final GatedRateLimiter<NumberSequenceSplit> gatedRateLimiter =
+                new GatedRateLimiter<>(capacityPerCycle);
+
+        // A checkpoint can complete before the reader has emitted anything, 
for instance while it
+        // is still waiting for its first split assignment.
+        gatedRateLimiter.notifyCheckpointComplete(0);
+
+        for (int x = 0; x < capacityPerCycle; x++) {
+            assertThat(gatedRateLimiter.acquire()).isCompleted();
+        }
+        assertThat(gatedRateLimiter.acquire()).isNotCompleted();
+    }
+
+    @Test
+    void testNonPositiveNumberOfEventsIsRejected() {
+        final GatedRateLimiter<NumberSequenceSplit> gatedRateLimiter = new 
GatedRateLimiter<>(5);
+
+        assertThatThrownBy(() -> gatedRateLimiter.acquire(0))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("positive");
+        assertThatThrownBy(() -> gatedRateLimiter.acquire(-1))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("positive");
+    }
 }

Reply via email to