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

Abacn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new 08e0efcfebf feat(firestore): add option to disable write ramp-up 
throttling (#40390)
08e0efcfebf is described below

commit 08e0efcfebf1e8b7eca1a222be11bb964c4e64bb
Author: Balázs Németh <[email protected]>
AuthorDate: Thu Oct 15 16:13:46 2026 +0200

    feat(firestore): add option to disable write ramp-up throttling (#40390)
    
    Add RpcQosOptions.Builder.withRampupThrottlingDisabled(), mirroring
    DatastoreV1.Write.withRampupThrottlingDisabled(). When set, writes no
    longer wait for the 500/50/5 ramp-up budget and batch sizes are no longer
    capped by it; adaptive throttling on failures still applies.
    
    Addresses #27170.
    
    
    Claude-Session: https://claude.ai/code/session_01BofSrVTSEGJvEzsSNXfU2o
    
    Co-authored-by: Claude <[email protected]>
    Co-authored-by: Yi Hu <[email protected]>
---
 CHANGES.md                                         |  2 +
 .../beam/sdk/io/gcp/firestore/RpcQosImpl.java      |  8 ++-
 .../beam/sdk/io/gcp/firestore/RpcQosOptions.java   | 61 ++++++++++++++++++++--
 .../sdk/io/gcp/firestore/RpcQosOptionsTest.java    | 16 ++++++
 .../beam/sdk/io/gcp/firestore/RpcQosTest.java      | 39 ++++++++++++++
 5 files changed, 122 insertions(+), 4 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index c787b83dde7..bb5ab431e05 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -65,6 +65,8 @@
 ## I/Os
 
 * Support for X source added (Java/Python) 
([#X](https://github.com/apache/beam/issues/X)).
+* (Java) FirestoreIO: added 
`RpcQosOptions.Builder.withRampupThrottlingDisabled()`, mirroring the existing 
DatastoreIO option, to opt out of the 500/50/5 write ramp-up 
([#27170](https://github.com/apache/beam/issues/27170)).
+
 
 ## New Features / Improvements
 
diff --git 
a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/firestore/RpcQosImpl.java
 
b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/firestore/RpcQosImpl.java
index 06b7dbe3801..c647b3d084b 100644
--- 
a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/firestore/RpcQosImpl.java
+++ 
b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/firestore/RpcQosImpl.java
@@ -376,6 +376,9 @@ final class RpcQosImpl implements RpcQos {
     @Override
     public boolean awaitSafeToProceed(Instant instant) throws 
InterruptedException {
       state.checkActive();
+      if (!options.isShouldThrottleRampup()) {
+        return super.awaitSafeToProceed(instant);
+      }
       Optional<Duration> shouldThrottle = writeRampUp.shouldThrottle(instant);
       if (shouldThrottle.isPresent()) {
         Duration throttleDuration = shouldThrottle.get();
@@ -392,7 +395,10 @@ final class RpcQosImpl implements RpcQos {
     public <ElementT extends Element<?>> FlushBufferImpl<ElementT> 
newFlushBuffer(
         Instant instantSinceEpoch) {
       state.checkActive();
-      int availableWriteCountBudget = 
writeRampUp.getAvailableWriteCountBudget(instantSinceEpoch);
+      int availableWriteCountBudget =
+          options.isShouldThrottleRampup()
+              ? writeRampUp.getAvailableWriteCountBudget(instantSinceEpoch)
+              : Integer.MAX_VALUE;
       int nextBatchMaxCount = wb.nextBatchMaxCount(instantSinceEpoch);
       int batchMaxCount =
           Ints.min(
diff --git 
a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/firestore/RpcQosOptions.java
 
b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/firestore/RpcQosOptions.java
index 950dbe9374b..247f1d42cb1 100644
--- 
a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/firestore/RpcQosOptions.java
+++ 
b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/firestore/RpcQosOptions.java
@@ -76,6 +76,7 @@ public final class RpcQosOptions implements Serializable, 
HasDisplayData {
   private final long batchMaxBytes;
   private final Duration batchTargetLatency;
   private final int hintMaxNumWorkers;
+  private final boolean shouldThrottleRampup;
   private final boolean shouldReportDiagnosticMetrics;
 
   private RpcQosOptions(
@@ -90,6 +91,7 @@ public final class RpcQosOptions implements Serializable, 
HasDisplayData {
       long batchMaxBytes,
       Duration batchTargetLatency,
       int hintMaxNumWorkers,
+      boolean shouldThrottleRampup,
       boolean shouldReportDiagnosticMetrics) {
     this.maxAttempts = maxAttempts;
     this.initialBackoff = initialBackoff;
@@ -102,6 +104,7 @@ public final class RpcQosOptions implements Serializable, 
HasDisplayData {
     this.batchMaxBytes = batchMaxBytes;
     this.batchTargetLatency = batchTargetLatency;
     this.hintMaxNumWorkers = hintMaxNumWorkers;
+    this.shouldThrottleRampup = shouldThrottleRampup;
     this.shouldReportDiagnosticMetrics = shouldReportDiagnosticMetrics;
   }
 
@@ -125,6 +128,9 @@ public final class RpcQosOptions implements Serializable, 
HasDisplayData {
                 .withLabel("batchTargetLatency"))
         .add(
             DisplayData.item("hintMaxNumWorkers", 
hintMaxNumWorkers).withLabel("hintMaxNumWorkers"))
+        .add(
+            DisplayData.item("shouldThrottleRampup", shouldThrottleRampup)
+                .withLabel("shouldThrottleRampup"))
         .add(
             DisplayData.item("shouldReportDiagnosticMetrics", 
shouldReportDiagnosticMetrics)
                 .withLabel("shouldReportDiagnosticMetrics"));
@@ -274,6 +280,23 @@ public final class RpcQosOptions implements Serializable, 
HasDisplayData {
     return hintMaxNumWorkers;
   }
 
+  /**
+   * Whether writes are throttled to follow the <a target="_blank" 
rel="noopener noreferrer"
+   * 
href="https://cloud.google.com/firestore/docs/best-practices#ramping_up_traffic";>500/50/5
+   * ramp-up</a> strategy.
+   *
+   * <p>When {@code false}, the ramp-up budget no longer delays writes nor 
limits the size of a
+   * batch. Writes are still subject to adaptive throttling in response to 
failed requests.
+   *
+   * <p><i>Default Value:</i> {@code true}
+   *
+   * @see RpcQosOptions.Builder#withRampupThrottlingDisabled()
+   * @see #getHintMaxNumWorkers()
+   */
+  public boolean isShouldThrottleRampup() {
+    return shouldThrottleRampup;
+  }
+
   /**
    * Whether additional diagnostic metrics should be reported for a Transform.
    *
@@ -314,11 +337,13 @@ public final class RpcQosOptions implements Serializable, 
HasDisplayData {
             .withBatchMaxBytes(batchMaxBytes)
             .withBatchTargetLatency(batchTargetLatency)
             .withHintMaxNumWorkers(hintMaxNumWorkers);
+    if (!shouldThrottleRampup) {
+      builder = builder.withRampupThrottlingDisabled();
+    }
     if (shouldReportDiagnosticMetrics) {
-      return builder.withReportDiagnosticMetrics();
-    } else {
-      return builder;
+      builder = builder.withReportDiagnosticMetrics();
     }
+    return builder;
   }
 
   @Override
@@ -336,6 +361,7 @@ public final class RpcQosOptions implements Serializable, 
HasDisplayData {
         && batchMaxCount == that.batchMaxCount
         && batchMaxBytes == that.batchMaxBytes
         && hintMaxNumWorkers == that.hintMaxNumWorkers
+        && shouldThrottleRampup == that.shouldThrottleRampup
         && shouldReportDiagnosticMetrics == that.shouldReportDiagnosticMetrics
         && initialBackoff.equals(that.initialBackoff)
         && samplePeriod.equals(that.samplePeriod)
@@ -358,6 +384,7 @@ public final class RpcQosOptions implements Serializable, 
HasDisplayData {
         batchMaxBytes,
         batchTargetLatency,
         hintMaxNumWorkers,
+        shouldThrottleRampup,
         shouldReportDiagnosticMetrics);
   }
 
@@ -386,6 +413,8 @@ public final class RpcQosOptions implements Serializable, 
HasDisplayData {
         + batchTargetLatency
         + ", hintMaxNumWorkers="
         + hintMaxNumWorkers
+        + ", shouldThrottleRampup="
+        + shouldThrottleRampup
         + ", shouldReportDiagnosticMetrics="
         + shouldReportDiagnosticMetrics
         + '}';
@@ -444,6 +473,7 @@ public final class RpcQosOptions implements Serializable, 
HasDisplayData {
     private long batchMaxBytes;
     private Duration batchTargetLatency;
     private int hintMaxNumWorkers;
+    private boolean shouldThrottleRampup;
     private boolean shouldReportDiagnosticMetrics;
 
     private Builder() {
@@ -458,6 +488,7 @@ public final class RpcQosOptions implements Serializable, 
HasDisplayData {
       batchMaxBytes = FIRESTORE_RPC_BYTES_MAX;
       batchTargetLatency = Duration.standardSeconds(5);
       hintMaxNumWorkers = 500;
+      shouldThrottleRampup = true;
       shouldReportDiagnosticMetrics = false;
     }
 
@@ -646,6 +677,29 @@ public final class RpcQosOptions implements Serializable, 
HasDisplayData {
       return this;
     }
 
+    /**
+     * Disable the <a target="_blank" rel="noopener noreferrer"
+     * 
href="https://cloud.google.com/firestore/docs/best-practices#ramping_up_traffic";>500/50/5
+     * ramp-up</a> throttling of writes.
+     *
+     * <p>By default, each worker starts with a budget of {@code 500 / 
hintMaxNumWorkers} writes per
+     * second, which grows by 50% every 5 minutes. With the default {@code 
hintMaxNumWorkers} this
+     * is 1 write per second, which can severely limit the throughput of 
pipelines running on fewer
+     * workers. Disabling ramp-up throttling is appropriate when the target 
collection is already
+     * serving traffic, or its documents are written with well distributed IDs.
+     *
+     * <p>Writes remain subject to adaptive throttling in response to failed 
requests.
+     *
+     * <p>This mirrors {@code 
DatastoreIO.v1().write().withRampupThrottlingDisabled()}.
+     *
+     * @return this builder
+     * @see RpcQosOptions#isShouldThrottleRampup()
+     */
+    public Builder withRampupThrottlingDisabled() {
+      this.shouldThrottleRampup = false;
+      return this;
+    }
+
     /**
      * Whether additional diagnostic metrics should be reported for a 
Transform.
      *
@@ -698,6 +752,7 @@ public final class RpcQosOptions implements Serializable, 
HasDisplayData {
           batchMaxBytes,
           batchTargetLatency,
           hintMaxNumWorkers,
+          shouldThrottleRampup,
           shouldReportDiagnosticMetrics);
     }
 
diff --git 
a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/firestore/RpcQosOptionsTest.java
 
b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/firestore/RpcQosOptionsTest.java
index 997d94b3d2b..601525c02ac 100644
--- 
a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/firestore/RpcQosOptionsTest.java
+++ 
b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/firestore/RpcQosOptionsTest.java
@@ -19,6 +19,7 @@ package org.apache.beam.sdk.io.gcp.firestore;
 
 import static 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists.newArrayList;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
@@ -59,6 +60,20 @@ public final class RpcQosOptionsTest {
     assertEquals(newMaxAttempts, build.getMaxAttempts());
   }
 
+  @Test
+  public void rampupThrottlingEnabledByDefault() {
+    assertTrue(RpcQosOptions.defaultOptions().isShouldThrottleRampup());
+  }
+
+  @Test
+  public void withRampupThrottlingDisabled_survivesToBuilder() {
+    RpcQosOptions disabled = 
RpcQosOptions.newBuilder().withRampupThrottlingDisabled().build();
+
+    assertFalse(disabled.isShouldThrottleRampup());
+    assertNotEquals(RpcQosOptions.defaultOptions(), disabled);
+    assertEquals(disabled, disabled.toBuilder().build());
+  }
+
   @Test
   public void populateDisplayData() {
     //noinspection unchecked
@@ -85,6 +100,7 @@ public final class RpcQosOptionsTest {
             "samplePeriod",
             "samplePeriodBucketSize",
             "shouldReportDiagnosticMetrics",
+            "shouldThrottleRampup",
             "throttleDuration");
 
     assertEquals(expectedKeys, actualKeys);
diff --git 
a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/firestore/RpcQosTest.java
 
b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/firestore/RpcQosTest.java
index 8910d491c4f..367677055e8 100644
--- 
a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/firestore/RpcQosTest.java
+++ 
b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/firestore/RpcQosTest.java
@@ -562,6 +562,45 @@ public final class RpcQosTest {
     doTest_initialBatchSizeRelativeToWorkerCount(1, 500);
   }
 
+  @Test
+  public void rampupThrottlingDisabled_initialBatchSizeIgnoresWorkerCount() {
+    RpcQosOptions options =
+        RpcQosOptions.newBuilder()
+            .withHintMaxNumWorkers(10000)
+            .withBatchInitialCount(500)
+            .withRampupThrottlingDisabled()
+            .build();
+    RpcQosImpl qos = new RpcQosImpl(options, random, sleeper, counterFactory, 
distributionFactory);
+    RpcWriteAttemptImpl attempt = qos.newWriteAttempt(RPC_ATTEMPT_CONTEXT);
+    FlushBufferImpl<Element<Object>> buffer = 
attempt.newFlushBuffer(Instant.EPOCH);
+    assertEquals(500, buffer.nextBatchMaxCount);
+  }
+
+  @Test
+  public void rampupThrottlingDisabled_writesDoNotWaitForRampUpBudget()
+      throws InterruptedException {
+    RpcQosOptions options =
+        RpcQosOptions.newBuilder()
+            .withHintMaxNumWorkers(10000)
+            .withRampupThrottlingDisabled()
+            .build();
+    RpcQos qos = new RpcQosImpl(options, random, sleeper, counterFactory, 
distributionFactory);
+
+    // with ramp-up throttling enabled, the budget of 1 write per second would 
be exhausted after
+    // the first successful request within the same second
+    Instant now = Instant.EPOCH;
+    for (int i = 0; i < 10; i++) {
+      RpcWriteAttempt attempt = qos.newWriteAttempt(RPC_ATTEMPT_CONTEXT);
+      assertTrue(attempt.awaitSafeToProceed(now));
+      attempt.recordRequestStart(now, 500);
+      attempt.recordWriteCounts(now, 500, 0);
+      attempt.recordRequestSuccessful(now);
+    }
+
+    verify(sleeper, times(0)).sleep(anyLong());
+    verify(counterThrottlingMs, times(0)).inc(anyLong());
+  }
+
   @Test
   public void isCodeRetryable() {
     doTest_isCodeRetryable(Code.ABORTED, true);

Reply via email to