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

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git


The following commit(s) were added to refs/heads/master by this push:
     new 8159dfe66 ThresholdCircuitBreaker accepts negative increments and 
overflows its accumulator, silently keeping the breaker closed (f027).
8159dfe66 is described below

commit 8159dfe661a064707e9b4de92aba1b952f3370d7
Author: Gary Gregory <[email protected]>
AuthorDate: Sat Sep 5 15:39:48 2026 -0400

    ThresholdCircuitBreaker accepts negative increments and overflows its
    accumulator, silently keeping the breaker closed (f027).
---
 src/changes/changes.xml                            |  1 +
 .../lang3/concurrent/EventCountCircuitBreaker.java | 10 +++++++++
 .../lang3/concurrent/ThresholdCircuitBreaker.java  | 26 +++++++++++++++++-----
 .../concurrent/EventCountCircuitBreakerTest.java   | 11 +++++++++
 .../concurrent/ThresholdCircuitBreakerTest.java    | 23 +++++++++++++++++++
 5 files changed, 66 insertions(+), 5 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 70dc9d021..be119519d 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -276,6 +276,7 @@ java.lang.NullPointerException: Cannot invoke
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">FormattableUtils.append pads right-justified output with insert(0) per 
char; O(width^2); '%500000s' costs ~1.25e11 char moves (f024).</action>
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">Attacker-controlled exception message forges frames in 
getRootCauseStackTrace output AND suppresses all real frames, beyond cosmetic 
log spoofing (f025).</action>
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">StrBuilder.indexOf materializes the whole builder as a String per 
call; deleteAll/replaceAll multiply it into ~750 GB churn on a 1 MB builder 
(f026).</action>
+    <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">ThresholdCircuitBreaker accepts negative increments and overflows its 
accumulator, silently keeping the breaker closed (f027).</action>
     <!-- ADD -->
     <action                   type="add" dev="ggregory" due-to="Gary 
Gregory">Add JavaVersion.JAVA_27.</action>
     <action                   type="add" dev="ggregory" due-to="Gary 
Gregory">Add SystemUtils.IS_JAVA_27.</action>
diff --git 
a/src/main/java/org/apache/commons/lang3/concurrent/EventCountCircuitBreaker.java
 
b/src/main/java/org/apache/commons/lang3/concurrent/EventCountCircuitBreaker.java
index 5c4386713..3741f56fa 100644
--- 
a/src/main/java/org/apache/commons/lang3/concurrent/EventCountCircuitBreaker.java
+++ 
b/src/main/java/org/apache/commons/lang3/concurrent/EventCountCircuitBreaker.java
@@ -478,9 +478,19 @@ public boolean incrementAndCheckState() {
 
     /**
      * {@inheritDoc}
+     *
+     * <p>
+     * The event count is a protective counter and only moves toward the 
opening threshold:
+     * negative increments are rejected.
+     * </p>
+     *
+     * @throws IllegalArgumentException if the increment is negative.
      */
     @Override
     public boolean incrementAndCheckState(final Integer increment) {
+        if (increment.intValue() < 0) {
+            throw new IllegalArgumentException("Increment must not be 
negative: " + increment);
+        }
         return performStateCheck(increment);
     }
 
diff --git 
a/src/main/java/org/apache/commons/lang3/concurrent/ThresholdCircuitBreaker.java
 
b/src/main/java/org/apache/commons/lang3/concurrent/ThresholdCircuitBreaker.java
index 235b4d144..a7a213e6f 100644
--- 
a/src/main/java/org/apache/commons/lang3/concurrent/ThresholdCircuitBreaker.java
+++ 
b/src/main/java/org/apache/commons/lang3/concurrent/ThresholdCircuitBreaker.java
@@ -108,19 +108,35 @@ public long getThreshold() {
     /**
      * {@inheritDoc}
      *
-     * <p>If the threshold is zero, the circuit breaker will be in a permanent 
<em>open</em> state.</p>
+     * <p>
+     * If the threshold is zero, the circuit breaker will be in a permanent 
<em>open</em> state.
+     * </p>
+     * <p>
+     * The internal counter is a protective counter and only moves toward the 
threshold: negative
+     * increments are rejected, and an increment that would overflow {@link 
Long#MAX_VALUE} saturates
+     * the counter at {@link Long#MAX_VALUE} and opens the circuit breaker 
instead of silently wrapping
+     * negative (which would disable the trip condition).
+     * </p>
+     *
+     * @throws IllegalArgumentException if the increment is negative.
      */
     @Override
     public boolean incrementAndCheckState(final Long increment) {
         if (threshold == 0) {
             open();
         }
-
-        final long used = this.used.addAndGet(increment);
-        if (used > threshold) {
+        final long delta = increment.longValue();
+        if (delta < 0) {
+            throw new IllegalArgumentException("Increment must not be 
negative: " + delta);
+        }
+        final long used = this.used.accumulateAndGet(delta, (current, add) -> {
+            final long next = current + add;
+            // Both operands are non-negative, so overflow shows up as a 
decrease: saturate.
+            return next < current ? Long.MAX_VALUE : next;
+        });
+        if (used > threshold || used == Long.MAX_VALUE) {
             open();
         }
-
         return checkState();
     }
 
diff --git 
a/src/test/java/org/apache/commons/lang3/concurrent/EventCountCircuitBreakerTest.java
 
b/src/test/java/org/apache/commons/lang3/concurrent/EventCountCircuitBreakerTest.java
index aab088648..8bbe62e72 100644
--- 
a/src/test/java/org/apache/commons/lang3/concurrent/EventCountCircuitBreakerTest.java
+++ 
b/src/test/java/org/apache/commons/lang3/concurrent/EventCountCircuitBreakerTest.java
@@ -20,6 +20,7 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.beans.PropertyChangeEvent;
@@ -256,6 +257,16 @@ void testIntervalCalculation() {
         assertEquals(2 * NANO_FACTOR / 1000, breaker.getClosingInterval(), 
"Wrong closing interval");
     }
 
+    /**
+     * Tests that a negative increment is rejected: the event count must only 
move toward
+     * the opening threshold.
+     */
+    @Test
+    void testNegativeIncrementRejected() {
+        final EventCountCircuitBreaker breaker = new 
EventCountCircuitBreaker(OPENING_THRESHOLD, 1, TimeUnit.SECONDS);
+        assertThrows(IllegalArgumentException.class, () -> 
breaker.incrementAndCheckState(-1), "Negative increments must be rejected");
+    }
+
     /**
      * Tests that an open circuit breaker does not close itself when the 
number of events
      * received is over the threshold.
diff --git 
a/src/test/java/org/apache/commons/lang3/concurrent/ThresholdCircuitBreakerTest.java
 
b/src/test/java/org/apache/commons/lang3/concurrent/ThresholdCircuitBreakerTest.java
index 7159f2816..8e90b4ae7 100644
--- 
a/src/test/java/org/apache/commons/lang3/concurrent/ThresholdCircuitBreakerTest.java
+++ 
b/src/test/java/org/apache/commons/lang3/concurrent/ThresholdCircuitBreakerTest.java
@@ -18,6 +18,7 @@
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import org.apache.commons.lang3.AbstractLangTest;
@@ -56,6 +57,28 @@ void testGettingThreshold() {
         assertEquals(Long.valueOf(threshold), 
Long.valueOf(circuit.getThreshold()), "Wrong value of threshold");
     }
 
+    /**
+     * Tests that a negative increment is rejected: the counter must only move 
toward the threshold.
+     */
+    @Test
+    void testNegativeIncrementRejected() {
+        final ThresholdCircuitBreaker circuit = new 
ThresholdCircuitBreaker(threshold);
+        assertThrows(IllegalArgumentException.class, () -> 
circuit.incrementAndCheckState(-1L), "Negative increments must be rejected");
+    }
+
+    /**
+     * Tests that overflowing the internal counter saturates and opens the 
circuit breaker instead of wrapping negative and silently disabling the trip
+     * condition.
+     */
+    @Test
+    void testOverflowSaturatesAndOpens() {
+        final ThresholdCircuitBreaker circuit = new 
ThresholdCircuitBreaker(Long.MAX_VALUE);
+        circuit.incrementAndCheckState(Long.MAX_VALUE);
+        // Without saturation this would wrap negative and report the circuit 
closed forever.
+        assertFalse(circuit.incrementAndCheckState(1L), "Overflow must open 
the circuit, not wrap the counter");
+        assertFalse(circuit.checkState(), "Circuit must stay open after 
overflow");
+    }
+
     /**
      * Tests that the threshold is working as expected when incremented and no 
exception is thrown.
      */

Reply via email to