diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/IteratingCallback.java b/jetty-util/src/main/java/org/eclipse/jetty/util/IteratingCallback.java index 634dc52a0335..af0583187bba 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/IteratingCallback.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/IteratingCallback.java @@ -275,6 +275,7 @@ private void processing() } case SCHEDULED: { + _iterate = false; // we won the race against the callback, so the callback has to process and we can break processing _state = State.PENDING; break processing; @@ -300,6 +301,7 @@ private void processing() { if (action != Action.SCHEDULED) throw new IllegalStateException(String.format("%s[action=%s]", this, action)); + _iterate = false; // we lost the race, so we have to keep processing _state = State.PROCESSING; continue; @@ -459,6 +461,14 @@ public void close() onCompleteFailure(new IOException(failure)); } + boolean isPending() + { + try (AutoLock ignored = _lock.lock()) + { + return _state == State.PENDING; + } + } + /** * @return whether this callback is idle, and {@link #iterate()} needs to be called */ diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/IteratingCallbackTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/IteratingCallbackTest.java index d588ab518bc1..0ade4d4ee37b 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/IteratingCallbackTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/IteratingCallbackTest.java @@ -22,7 +22,11 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -44,6 +48,45 @@ public void dispose() throws Exception scheduler.stop(); } + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testIterateWhileProcessingLoopCount(boolean succeededWinsRace) + { + var icb = new IteratingCallback() + { + int counter = 0; + + @Override + protected Action process() + { + int counter = this.counter++; + if (counter == 0) + { + iterate(); + if (succeededWinsRace) + { + succeeded(); + } + else + { + new Thread(() -> + { + await().atMost(5, TimeUnit.SECONDS).until(this::isPending, is(true)); + succeeded(); + }).start(); + } + return Action.SCHEDULED; + } + return Action.IDLE; + } + }; + + icb.iterate(); + + await().atMost(10, TimeUnit.SECONDS).until(icb::isIdle, is(true)); + assertEquals(2, icb.counter); + } + @Test public void testNonWaitingProcess() throws Exception {