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

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


The following commit(s) were added to refs/heads/master by this push:
     new e60a8b2e2f0 correctly update for only partial change on "before 
serving query" value and regular value (#18693)
e60a8b2e2f0 is described below

commit e60a8b2e2f0b3f4b9ba85cf40d74ac6a824fcbf1
Author: Jhow <[email protected]>
AuthorDate: Sat Jun 6 01:52:54 2026 -0700

    correctly update for only partial change on "before serving query" value 
and regular value (#18693)
---
 .../local/utils/SegmentOperationsThrottler.java    |  17 ++++
 .../local/utils/SegmentOperationsThrottlerSet.java |  37 +++++---
 .../utils/SegmentOperationsThrottlerSetTest.java   | 101 +++++++++++++++++++++
 3 files changed, 141 insertions(+), 14 deletions(-)

diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/SegmentOperationsThrottler.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/SegmentOperationsThrottler.java
index b09d9c8b064..894b37c017d 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/SegmentOperationsThrottler.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/SegmentOperationsThrottler.java
@@ -197,6 +197,23 @@ public class SegmentOperationsThrottler {
     LOGGER.info("Throttler {}: Updated total permits: {}", _throttlerName, 
totalPermits());
   }
 
+  /**
+   * Partial variant of {@link #updatePermits(int, int)}: a {@code null} 
argument leaves the corresponding value
+   * unchanged. Resolving the current value and writing the new permits happen 
atomically under this throttler's
+   * lock, so a partial update cannot interleave with another concurrent 
permit change. This allows a single
+   * changed config to be applied without resetting the other (unchanged) 
config.
+   *
+   * @param maxConcurrency new max concurrency value, or {@code null} to keep 
the current value
+   * @param maxConcurrencyBeforeServingQueries new max concurrency before 
serving queries value, or {@code null} to
+   *     keep the current value
+   */
+  public synchronized void updatePermits(@Nullable Integer maxConcurrency,
+      @Nullable Integer maxConcurrencyBeforeServingQueries) {
+    updatePermits(maxConcurrency == null ? _maxConcurrency : maxConcurrency,
+        maxConcurrencyBeforeServingQueries == null ? 
_maxConcurrencyBeforeServingQueries
+            : maxConcurrencyBeforeServingQueries);
+  }
+
   /**
    * Block trying to acquire the semaphore to perform the segment operation 
steps unless interrupted.
    * <p>
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/SegmentOperationsThrottlerSet.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/SegmentOperationsThrottlerSet.java
index 2416e8a1a84..9aa29b89449 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/SegmentOperationsThrottlerSet.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/SegmentOperationsThrottlerSet.java
@@ -155,24 +155,33 @@ public class SegmentOperationsThrottlerSet implements 
PinotClusterConfigChangeLi
       return;
     }
 
-    // Parse maxConcurrency. 0 is a valid kill-switch value; negative values 
are treated as misconfiguration.
-    int maxConcurrency = parseConfigValue(clusterConfigs, 
maxConcurrencyConfigKey, maxConcurrencyDefault);
-    if (maxConcurrency < 0) {
-      return;
+    // Only parse the config that actually changed; pass null for the 
unchanged one so the throttler keeps its
+    // current value. This prevents a single changed config from inadvertently 
resetting the other. 0 is a valid
+    // kill-switch value; negative values are treated as misconfiguration.
+    Integer maxConcurrency = null;
+    if (maxConcurrencyChanged) {
+      int parsed = parseConfigValue(clusterConfigs, maxConcurrencyConfigKey, 
maxConcurrencyDefault);
+      if (parsed < 0) {
+        return;
+      }
+      maxConcurrency = parsed;
     }
 
-    // Parse maxConcurrencyBeforeServingQueries. Same handling as 
maxConcurrency.
-    int maxConcurrencyBeforeServingQueries =
-        parseConfigValue(clusterConfigs, 
maxConcurrencyBeforeServingQueriesConfigKey,
-            maxConcurrencyBeforeServingQueriesDefault);
-    if (maxConcurrencyBeforeServingQueries < 0) {
-      return;
+    Integer maxConcurrencyBeforeServingQueries = null;
+    if (maxConcurrencyBeforeServingQueriesChanged) {
+      int parsed = parseConfigValue(clusterConfigs, 
maxConcurrencyBeforeServingQueriesConfigKey,
+          maxConcurrencyBeforeServingQueriesDefault);
+      if (parsed < 0) {
+        return;
+      }
+      maxConcurrencyBeforeServingQueries = parsed;
     }
 
-    // Update throttler
-    LOGGER.info("Updating {} throttler with maxConcurrency: {}, 
maxConcurrencyBeforeServingQueries: {}",
-        throttlerName, maxConcurrency, maxConcurrencyBeforeServingQueries);
-    if (maxConcurrency == 0 || maxConcurrencyBeforeServingQueries == 0) {
+    // Update throttler (null = unchanged)
+    LOGGER.info("Updating {} throttler with maxConcurrency: {}, 
maxConcurrencyBeforeServingQueries: {} "
+            + "(null = unchanged)", throttlerName, maxConcurrency, 
maxConcurrencyBeforeServingQueries);
+    if ((maxConcurrency != null && maxConcurrency == 0)
+        || (maxConcurrencyBeforeServingQueries != null && 
maxConcurrencyBeforeServingQueries == 0)) {
       LOGGER.warn("Throttler {} configured with 0 permits (maxConcurrency: {}, 
maxConcurrencyBeforeServingQueries: "
               + "{}) — all operations will block until value is raised",
           throttlerName, maxConcurrency, maxConcurrencyBeforeServingQueries);
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/SegmentOperationsThrottlerSetTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/SegmentOperationsThrottlerSetTest.java
index 98610462c4a..00c8098df64 100644
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/SegmentOperationsThrottlerSetTest.java
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/SegmentOperationsThrottlerSetTest.java
@@ -387,6 +387,107 @@ public class SegmentOperationsThrottlerSetTest {
     }
   }
 
+  @Test
+  public void testChangeOnlyMaxConcurrencyPreservesBeforeServingQueries() {
+    // Regression: changing only maxConcurrency must not reset 
maxConcurrencyBeforeServingQueries (which defaults to
+    // Integer.MAX_VALUE and would have been clobbered before the fix).
+    int initialMaxConcurrency = 4;
+    int initialBeforeServing = 8;
+    SegmentOperationsThrottler[] throttlers = 
createThrottlers(initialMaxConcurrency, initialBeforeServing, false);
+    SegmentOperationsThrottlerSet sot = 
wrapInSegmentOperationsThrottler(throttlers);
+    for (int i = 0; i < NUM_THROTTLERS; i++) {
+      SegmentOperationsThrottler t = throttlers[i];
+      // Not serving queries yet, so permits reflect the before-serving value.
+      Assert.assertEquals(t.totalPermits(), initialBeforeServing);
+
+      // Change only maxConcurrency.
+      Map<String, String> updatedClusterConfigs = new HashMap<>();
+      updatedClusterConfigs.put(PARALLELISM_KEYS[i], 
String.valueOf(initialMaxConcurrency * 2));
+      sot.onChange(updatedClusterConfigs.keySet(), updatedClusterConfigs);
+
+      // Before-serving value must be preserved (permits unchanged while not 
serving).
+      Assert.assertEquals(t.totalPermits(), initialBeforeServing);
+
+      // After serving begins, permits reflect the newly-applied 
maxConcurrency.
+      t.startServingQueries();
+      Assert.assertEquals(t.totalPermits(), initialMaxConcurrency * 2);
+    }
+  }
+
+  @Test
+  public void testChangeOnlyBeforeServingQueriesPreservesMaxConcurrency() {
+    // Regression: changing only maxConcurrencyBeforeServingQueries must not 
reset maxConcurrency (which defaults to
+    // Integer.MAX_VALUE and would have been clobbered before the fix).
+    int initialMaxConcurrency = 4;
+    int initialBeforeServing = 8;
+    SegmentOperationsThrottler[] throttlers = 
createThrottlers(initialMaxConcurrency, initialBeforeServing, false);
+    SegmentOperationsThrottlerSet sot = 
wrapInSegmentOperationsThrottler(throttlers);
+    for (int i = 0; i < NUM_THROTTLERS; i++) {
+      SegmentOperationsThrottler t = throttlers[i];
+      Assert.assertEquals(t.totalPermits(), initialBeforeServing);
+
+      // Change only maxConcurrencyBeforeServingQueries.
+      Map<String, String> updatedClusterConfigs = new HashMap<>();
+      updatedClusterConfigs.put(PARALLELISM_BEFORE_SERVING_KEYS[i], 
String.valueOf(initialBeforeServing * 2));
+      sot.onChange(updatedClusterConfigs.keySet(), updatedClusterConfigs);
+
+      // Before-serving value updated.
+      Assert.assertEquals(t.totalPermits(), initialBeforeServing * 2);
+
+      // After serving begins, the preserved maxConcurrency is applied.
+      t.startServingQueries();
+      Assert.assertEquals(t.totalPermits(), initialMaxConcurrency);
+    }
+  }
+
+  @Test
+  public void testInvalidChangedConfigLeavesBothValuesUntouched() {
+    // An unparseable changed config triggers the early return before 
updatePermits; neither the changed nor the
+    // unchanged value should be modified.
+    int initialMaxConcurrency = 4;
+    int initialBeforeServing = 8;
+    SegmentOperationsThrottler[] throttlers = 
createThrottlers(initialMaxConcurrency, initialBeforeServing, false);
+    SegmentOperationsThrottlerSet sot = 
wrapInSegmentOperationsThrottler(throttlers);
+    for (int i = 0; i < NUM_THROTTLERS; i++) {
+      SegmentOperationsThrottler t = throttlers[i];
+      Assert.assertEquals(t.totalPermits(), initialBeforeServing);
+
+      // Change only maxConcurrency to a non-numeric value.
+      Map<String, String> updatedClusterConfigs = new HashMap<>();
+      updatedClusterConfigs.put(PARALLELISM_KEYS[i], "not-a-number");
+      sot.onChange(updatedClusterConfigs.keySet(), updatedClusterConfigs);
+
+      // Before-serving value untouched (still not serving).
+      Assert.assertEquals(t.totalPermits(), initialBeforeServing);
+      // maxConcurrency untouched: after serving begins, the original value 
applies.
+      t.startServingQueries();
+      Assert.assertEquals(t.totalPermits(), initialMaxConcurrency);
+    }
+  }
+
+  @Test
+  public void 
testChangeOnlyMaxConcurrencyToZeroAppliesKillSwitchAndPreservesBeforeServing() {
+    // 0 is a valid kill-switch for maxConcurrency; changing only it must not 
disturb the before-serving value.
+    int initialMaxConcurrency = 4;
+    int initialBeforeServing = 8;
+    SegmentOperationsThrottler[] throttlers = 
createThrottlers(initialMaxConcurrency, initialBeforeServing, false);
+    SegmentOperationsThrottlerSet sot = 
wrapInSegmentOperationsThrottler(throttlers);
+    for (int i = 0; i < NUM_THROTTLERS; i++) {
+      SegmentOperationsThrottler t = throttlers[i];
+      Assert.assertEquals(t.totalPermits(), initialBeforeServing);
+
+      Map<String, String> updatedClusterConfigs = new HashMap<>();
+      updatedClusterConfigs.put(PARALLELISM_KEYS[i], "0");
+      sot.onChange(updatedClusterConfigs.keySet(), updatedClusterConfigs);
+
+      // Before-serving value preserved while not serving.
+      Assert.assertEquals(t.totalPermits(), initialBeforeServing);
+      // Kill switch takes effect once serving begins.
+      t.startServingQueries();
+      Assert.assertEquals(t.totalPermits(), 0);
+    }
+  }
+
   @Test
   public void testDecreaseSegmentPreprocessParallelism()
       throws Exception {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to