diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicBaseCongestionController.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicBaseCongestionController.java new file mode 100644 index 0000000000000..7dd3276f0d3e9 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicBaseCongestionController.java @@ -0,0 +1,318 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.quic; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.TimeLine; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.packets.QuicPacket; + +import java.util.Collection; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Implementation of the common parts of a QUIC congestion controller based on RFC 9002. + * + * This class implements the common parts of a congestion controller: + * - slow start + * - loss recovery + * - cooperation with pacer + * + * Subclasses implement congestion window growth in congestion avoidance phase. + * + * @spec https://www.rfc-editor.org/info/rfc9002 + * RFC 9002: QUIC Loss Detection and Congestion Control + */ +abstract class QuicBaseCongestionController implements QuicCongestionController { + // higher of 14720 and 2*maxDatagramSize; we use fixed maxDatagramSize + private static final int INITIAL_WINDOW = Math.max(14720, 2 * QuicConnectionImpl.DEFAULT_DATAGRAM_SIZE); + private static final int MAX_BYTES_IN_FLIGHT = Math.clamp( + Utils.getLongProperty("jdk.httpclient.quic.maxBytesInFlight", 1 << 24), + 1 << 14, 1 << 24); + final TimeLine timeSource; + final String dbgTag; + final Lock lock = new ReentrantLock(); + long congestionWindow = INITIAL_WINDOW; + int maxDatagramSize = QuicConnectionImpl.DEFAULT_DATAGRAM_SIZE; + int minimumWindow = 2 * maxDatagramSize; + long bytesInFlight; + // maximum bytes in flight seen since the last congestion event + long maxBytesInFlight; + Deadline congestionRecoveryStartTime; + long ssThresh = Long.MAX_VALUE; + + private final QuicPacer pacer; + + QuicBaseCongestionController(String dbgTag, QuicRttEstimator rttEstimator) { + this(dbgTag, TimeSource.source(), rttEstimator); + } + + // Allows to pass a custom timeline for testing + QuicBaseCongestionController(String dbgTag, TimeLine source, QuicRttEstimator rttEstimator) { + this.dbgTag = dbgTag; + this.timeSource = source; + this.pacer = new QuicPacer(rttEstimator, this); + } + + boolean inCongestionRecovery(Deadline sentTime) { + return (congestionRecoveryStartTime != null && + !sentTime.isAfter(congestionRecoveryStartTime)); + } + + abstract void onCongestionEvent(Deadline sentTime); + + private static boolean inFlight(QuicPacket packet) { + // packet is in flight if it contains anything other than a single ACK frame + // specifically, a packet containing padding is considered to be in flight. + return packet.frames().size() != 1 || + !(packet.frames().get(0) instanceof AckFrame); + } + + @Override + public boolean canSendPacket() { + lock.lock(); + try { + if (bytesInFlight >= MAX_BYTES_IN_FLIGHT) { + return false; + } + if (isCwndLimited() || isPacerLimited()) { + return false; + } + return true; + } finally { + lock.unlock(); + } + } + + @Override + public void updateMaxDatagramSize(int newSize) { + lock.lock(); + try { + if (minimumWindow != newSize * 2) { + minimumWindow = newSize * 2; + maxDatagramSize = newSize; + congestionWindow = Math.max(congestionWindow, minimumWindow); + } + } finally { + lock.unlock(); + } + } + + @Override + public void packetSent(int packetBytes) { + lock.lock(); + try { + bytesInFlight += packetBytes; + if (bytesInFlight > maxBytesInFlight) { + maxBytesInFlight = bytesInFlight; + } + pacer.packetSent(packetBytes); + } finally { + lock.unlock(); + } + } + + @Override + public void packetAcked(int packetBytes, Deadline sentTime) { + lock.lock(); + try { + long oldWindow = congestionWindow; + assert oldWindow >= minimumWindow : + "Congestion window lower than minimum: %s < %s".formatted(oldWindow, minimumWindow); + bytesInFlight -= packetBytes; + // RFC 9002 says we should not increase cwnd when application limited. + // The concept itself is poorly defined. + // Here we limit cwnd growth based on the maximum bytes in flight + // observed since the last congestion event + if (inCongestionRecovery(sentTime)) { + if (Log.quicCC() && Log.trace()) { + Log.logQuic(dbgTag + " Acked, in recovery: bytes: " + packetBytes + + ", in flight: " + bytesInFlight); + } + return; + } + boolean isAppLimited; + if (congestionWindow < ssThresh) { + isAppLimited = congestionWindow >= 2 * maxBytesInFlight; + if (!isAppLimited) { + congestionWindow += packetBytes; + } + } else { + isAppLimited = congestionAvoidanceAcked(packetBytes, sentTime); + } + if (Log.quicCC() && Log.trace()) { + if (isAppLimited) { + Log.logQuic(dbgTag + " Acked, not blocked: bytes: " + packetBytes + + ", in flight: " + bytesInFlight); + } else { + Log.logQuic(dbgTag + " Acked, increased: bytes: " + packetBytes + + ", in flight: " + bytesInFlight + + ", new cwnd:" + congestionWindow); + } + } + assert congestionWindow >= oldWindow : + "Window size decreased on ACK: %s to %s".formatted(oldWindow, congestionWindow); + } finally { + lock.unlock(); + } + } + + abstract boolean congestionAvoidanceAcked(int packetBytes, Deadline sentTime); + + @Override + public void packetLost(Collection lostPackets, Deadline sentTime, boolean persistent) { + lock.lock(); + try { + for (QuicPacket packet : lostPackets) { + if (inFlight(packet)) { + bytesInFlight -= packet.size(); + } + } + onCongestionEvent(sentTime); + if (persistent) { + congestionWindow = minimumWindow; + congestionRecoveryStartTime = null; + if (Log.quicCC()) { + Log.logQuic(dbgTag + " Persistent congestion: ssThresh: " + ssThresh + + ", in flight: " + bytesInFlight + + ", cwnd:" + congestionWindow); + } + } + assert congestionWindow >= minimumWindow : + "Congestion window lower than minimum: %s < %s".formatted(congestionWindow, minimumWindow); + } finally { + lock.unlock(); + } + } + + @Override + public void packetDiscarded(Collection discardedPackets) { + lock.lock(); + try { + for (QuicPacket packet : discardedPackets) { + if (inFlight(packet)) { + bytesInFlight -= packet.size(); + } + } + } finally { + lock.unlock(); + } + } + + @Override + public long congestionWindow() { + lock.lock(); + try { + return congestionWindow; + } finally { + lock.unlock(); + } + } + + @Override + public long initialWindow() { + lock.lock(); + try { + return Math.max(14720, 2 * maxDatagramSize); + } finally { + lock.unlock(); + } + } + + @Override + public long maxDatagramSize() { + lock.lock(); + try { + return maxDatagramSize; + } finally { + lock.unlock(); + } + } + + @Override + public boolean isSlowStart() { + lock.lock(); + try { + return congestionWindow < ssThresh; + } finally { + lock.unlock(); + } + } + + @Override + public void updatePacer(Deadline now) { + lock.lock(); + try { + pacer.updateQuota(now); + } finally { + lock.unlock(); + } + } + + @Override + public boolean isPacerLimited() { + lock.lock(); + try { + return !pacer.canSend(); + } finally { + lock.unlock(); + } + } + + @Override + public boolean isCwndLimited() { + lock.lock(); + try { + return congestionWindow - bytesInFlight < maxDatagramSize; + } finally { + lock.unlock(); + } + } + + @Override + public Deadline pacerDeadline() { + lock.lock(); + try { + return pacer.twoPacketDeadline(); + } finally { + lock.unlock(); + } + } + + @Override + public void appLimited() { + lock.lock(); + try { + pacer.appLimited(); + } finally { + lock.unlock(); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java index 7ebe09e008e39..d90ad1b217e08 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java @@ -334,7 +334,7 @@ protected QuicConnectionImpl(final QuicVersion firstFlightVersion, this.connectionId = this.endpoint.idFactory().newConnectionId(); this.logTag = logTagFormat.formatted(labelId); this.dbgTag = dbgTag(quicInstance, logTag); - this.congestionController = new QuicRenoCongestionController(dbgTag, rttEstimator); + this.congestionController = createCongestionController(dbgTag, rttEstimator); this.originalVersion = this.quicVersion = firstFlightVersion == null ? QuicVersion.firstFlightVersion(quicInstance.getAvailableVersions()) : firstFlightVersion; @@ -366,6 +366,16 @@ protected QuicConnectionImpl(final QuicVersion firstFlightVersion, if (debug.on()) debug.log("Quic Connection Created"); } + private static QuicCongestionController createCongestionController + (String dbgTag, QuicRttEstimator rttEstimator) { + String algo = System.getProperty("jdk.internal.httpclient.quic.congestionController", "cubic"); + if (algo.equalsIgnoreCase("reno")) { + return new QuicRenoCongestionController(dbgTag, rttEstimator); + } else { + return new QuicCubicCongestionController(dbgTag, rttEstimator); + } + } + @Override public final long uniqueId() { return labelId; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicCubicCongestionController.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicCubicCongestionController.java new file mode 100644 index 0000000000000..a7a1cd0c0bcdf --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicCubicCongestionController.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.quic; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.TimeLine; + +import java.util.concurrent.TimeUnit; + +/** + * Implementation of the CUBIC congestion controller + * based on RFC 9438. + * + * @spec https://www.rfc-editor.org/rfc/rfc9438.html + * RFC 9438: CUBIC for Fast and Long-Distance Networks + */ +public final class QuicCubicCongestionController extends QuicBaseCongestionController { + + public static final double BETA = 0.7; + public static final double ALPHA = 3 * (1 - BETA) / (1 + BETA); + private static final double C = 0.4; + private final QuicRttEstimator rttEstimator; + // Cubic curve inflection point, in bytes + private long wMaxBytes; + // cwnd before the most recent congestion event + private long cwndPriorBytes; + // "t" from RFC 9438 + private long timeNanos; + // "K" from RFC 9438 + private long kNanos; + // estimate for the Reno-friendly congestion window + private long wEstBytes; + // the most recent time when the congestion window was filled + private Deadline lastFullWindow; + + public QuicCubicCongestionController(String dbgTag, QuicRttEstimator rttEstimator) { + super(dbgTag, rttEstimator); + this.rttEstimator = rttEstimator; + } + + // for testing + public QuicCubicCongestionController(TimeLine source, QuicRttEstimator rttEstimator) { + super("TEST", source, rttEstimator); + this.rttEstimator = rttEstimator; + } + + @Override + public void packetSent(int packetBytes) { + lock.lock(); + try { + super.packetSent(packetBytes); + if (isCwndLimited()) { + Deadline now = timeSource.instant(); + if (lastFullWindow == null) { + lastFullWindow = now; + } else { + long timePassedNanos = Deadline.between(lastFullWindow, now).toNanos(); + if (timePassedNanos > 0) { + /* "The elapsed time MUST NOT include periods during which cwnd + has not been updated due to application-limited behavior" + "A flow is application limited if it is currently sending less + than what is allowed by the congestion window." + + We are sending asynchronously; one thread is sending data, + a separate thread is processing the acknowledgements. + We can't rely on cwnd being fully utilized when we process an ack, because + most of the time it won't be. + + Instead, we assume that if we filled the cwnd, we were not application-limited + in the last RTT (which is a pretty good approximation because of pacing), + and acknowledgements for all packets sent prior to filling the cwnd + count towards cwnd increase. + */ + long rttNanos = TimeUnit.MICROSECONDS.toNanos(rttEstimator.state().smoothedRttMicros()); + timeNanos += Math.min(timePassedNanos, rttNanos); + lastFullWindow = now; + } + } + } + } finally { + lock.unlock(); + } + } + + + boolean congestionAvoidanceAcked(int packetBytes, Deadline sentTime) { + boolean isAppLimited = sentTime.isAfter(lastFullWindow); + if (!isAppLimited) { + if (wEstBytes < cwndPriorBytes) { + wEstBytes += Math.max((long) (ALPHA * maxDatagramSize * packetBytes / congestionWindow), 1); + } else { + wEstBytes += Math.max((long)maxDatagramSize * packetBytes / congestionWindow, 1); + } + // target = Wcubic(t + RTT) + long rttNanos = TimeUnit.MICROSECONDS.toNanos(rttEstimator.state().smoothedRttMicros()); + double dblTargetBytes = wCubicBytes(timeNanos + rttNanos); + assert dblTargetBytes > 0 : "Unexpected negative target bytes"; + long targetBytes = (long) Math.min(dblTargetBytes, 1.5 * congestionWindow); + if (targetBytes > congestionWindow) { + long oldWindow = congestionWindow; + congestionWindow += Math.max((targetBytes - congestionWindow) * packetBytes / congestionWindow, 1L); + assert congestionWindow > oldWindow : + "Window size decreased: %s to %s".formatted(oldWindow, congestionWindow); + } + if (wEstBytes > congestionWindow) { + congestionWindow = wEstBytes; + } + } + return isAppLimited; + } + + // Wcubic(t) = C * (t-K [seconds])^3 + Wmax (segments) + private double wCubicBytes(long timeNanos) { + return (C * maxDatagramSize * Math.pow((timeNanos - kNanos) / 1e9, 3)) + wMaxBytes; + } + + void onCongestionEvent(Deadline sentTime) { + if (inCongestionRecovery(sentTime)) { + return; + } + if (congestionWindow < wMaxBytes) { + // fast convergence + wMaxBytes = (long) ((1 + BETA) * congestionWindow / 2); + } else { + wMaxBytes = congestionWindow; + } + cwndPriorBytes = congestionWindow; + congestionRecoveryStartTime = timeSource.instant(); + ssThresh = (long)(congestionWindow * BETA); + wEstBytes = congestionWindow = Math.max(minimumWindow, ssThresh); + maxBytesInFlight = 0; + timeNanos = 0; + // set lastFullWindow to prevent rapid timeNanos growth + lastFullWindow = congestionRecoveryStartTime; + // ((wmax_segments - cwnd_segments) / C) ^ (1/3) seconds + kNanos = (long)(Math.cbrt((wMaxBytes - congestionWindow) / C / maxDatagramSize) * 1_000_000_000); + // kNanos may be negative if we reduced the window below minimum, + // and fast convergence was used. This is acceptable. + if (Log.quicCC()) { + Log.logQuic(dbgTag + " Congestion: ssThresh: " + ssThresh + + ", in flight: " + bytesInFlight + + ", cwnd:" + congestionWindow + + ", K: " + TimeUnit.NANOSECONDS.toMillis(kNanos) + " ms"); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicPacer.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicPacer.java index 0ba7d78038bf6..50d8485785f4e 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicPacer.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicPacer.java @@ -30,10 +30,8 @@ import jdk.internal.net.http.common.Utils; import jdk.internal.util.OperatingSystem; -import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; /** * Implementation of pacing. diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRenoCongestionController.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRenoCongestionController.java index ff51aafc131d7..2594c00055f14 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRenoCongestionController.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRenoCongestionController.java @@ -27,15 +27,6 @@ import jdk.internal.net.http.common.Deadline; import jdk.internal.net.http.common.Log; -import jdk.internal.net.http.common.TimeLine; -import jdk.internal.net.http.common.TimeSource; -import jdk.internal.net.http.common.Utils; -import jdk.internal.net.http.quic.frames.AckFrame; -import jdk.internal.net.http.quic.packets.QuicPacket; - -import java.util.Collection; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; /** * Implementation of QUIC congestion controller based on RFC 9002. @@ -46,38 +37,20 @@ * @spec https://www.rfc-editor.org/info/rfc9002 * RFC 9002: QUIC Loss Detection and Congestion Control */ -class QuicRenoCongestionController implements QuicCongestionController { - // higher of 14720 and 2*maxDatagramSize; we use fixed maxDatagramSize - private static final int INITIAL_WINDOW = Math.max(14720, 2 * QuicConnectionImpl.DEFAULT_DATAGRAM_SIZE); - private static final int MAX_BYTES_IN_FLIGHT = Math.clamp( - Utils.getLongProperty("jdk.httpclient.quic.maxBytesInFlight", 1 << 24), - 1 << 14, 1 << 24); - private final TimeLine timeSource; - private final String dbgTag; - private final Lock lock = new ReentrantLock(); - private long congestionWindow = INITIAL_WINDOW; - private int maxDatagramSize = QuicConnectionImpl.DEFAULT_DATAGRAM_SIZE; - private int minimumWindow = 2 * maxDatagramSize; - private long bytesInFlight; - // maximum bytes in flight seen since the last congestion event - private long maxBytesInFlight; - private Deadline congestionRecoveryStartTime; - private long ssThresh = Long.MAX_VALUE; - - private final QuicPacer pacer; - +final class QuicRenoCongestionController extends QuicBaseCongestionController { public QuicRenoCongestionController(String dbgTag, QuicRttEstimator rttEstimator) { - this.dbgTag = dbgTag; - this.timeSource = TimeSource.source(); - this.pacer = new QuicPacer(rttEstimator, this); + super(dbgTag, rttEstimator); } - private boolean inCongestionRecovery(Deadline sentTime) { - return (congestionRecoveryStartTime != null && - !sentTime.isAfter(congestionRecoveryStartTime)); + boolean congestionAvoidanceAcked(int packetBytes, Deadline sentTime) { + boolean isAppLimited = congestionWindow > maxBytesInFlight + 2L * maxDatagramSize; + if (!isAppLimited) { + congestionWindow += Math.max((long) maxDatagramSize * packetBytes / congestionWindow, 1L); + } + return isAppLimited; } - private void onCongestionEvent(Deadline sentTime) { + void onCongestionEvent(Deadline sentTime) { if (inCongestionRecovery(sentTime)) { return; } @@ -91,226 +64,4 @@ private void onCongestionEvent(Deadline sentTime) { ", cwnd:" + congestionWindow); } } - - private static boolean inFlight(QuicPacket packet) { - // packet is in flight if it contains anything other than a single ACK frame - // specifically, a packet containing padding is considered to be in flight. - return packet.frames().size() != 1 || - !(packet.frames().get(0) instanceof AckFrame); - } - - @Override - public boolean canSendPacket() { - lock.lock(); - try { - if (bytesInFlight >= MAX_BYTES_IN_FLIGHT) { - return false; - } - if (isCwndLimited() || isPacerLimited()) { - return false; - } - return true; - } finally { - lock.unlock(); - } - } - - @Override - public void updateMaxDatagramSize(int newSize) { - lock.lock(); - try { - if (minimumWindow != newSize * 2) { - minimumWindow = newSize * 2; - maxDatagramSize = newSize; - congestionWindow = Math.max(congestionWindow, minimumWindow); - } - } finally { - lock.unlock(); - } - } - - @Override - public void packetSent(int packetBytes) { - lock.lock(); - try { - bytesInFlight += packetBytes; - if (bytesInFlight > maxBytesInFlight) { - maxBytesInFlight = bytesInFlight; - } - pacer.packetSent(packetBytes); - } finally { - lock.unlock(); - } - } - - @Override - public void packetAcked(int packetBytes, Deadline sentTime) { - lock.lock(); - try { - bytesInFlight -= packetBytes; - // RFC 9002 says we should not increase cwnd when application limited. - // The concept itself is poorly defined. - // Here we limit cwnd growth based on the maximum bytes in flight - // observed since the last congestion event - if (inCongestionRecovery(sentTime)) { - if (Log.quicCC() && Log.trace()) { - Log.logQuic(dbgTag + " Acked, in recovery: bytes: " + packetBytes + - ", in flight: " + bytesInFlight); - } - return; - } - boolean isAppLimited; - if (congestionWindow < ssThresh) { - isAppLimited = congestionWindow >= 2 * maxBytesInFlight; - if (!isAppLimited) { - congestionWindow += packetBytes; - } - } else { - isAppLimited = congestionWindow > maxBytesInFlight + 2L * maxDatagramSize; - if (!isAppLimited) { - congestionWindow += Math.max((long) maxDatagramSize * packetBytes / congestionWindow, 1L); - } - } - if (Log.quicCC() && Log.trace()) { - if (isAppLimited) { - Log.logQuic(dbgTag + " Acked, not blocked: bytes: " + packetBytes + - ", in flight: " + bytesInFlight); - } else { - Log.logQuic(dbgTag + " Acked, increased: bytes: " + packetBytes + - ", in flight: " + bytesInFlight + - ", new cwnd:" + congestionWindow); - } - } - } finally { - lock.unlock(); - } - } - - @Override - public void packetLost(Collection lostPackets, Deadline sentTime, boolean persistent) { - lock.lock(); - try { - for (QuicPacket packet : lostPackets) { - if (inFlight(packet)) { - bytesInFlight -= packet.size(); - } - } - onCongestionEvent(sentTime); - if (persistent) { - congestionWindow = minimumWindow; - congestionRecoveryStartTime = null; - if (Log.quicCC()) { - Log.logQuic(dbgTag + " Persistent congestion: ssThresh: " + ssThresh + - ", in flight: " + bytesInFlight + - ", cwnd:" + congestionWindow); - } - } - } finally { - lock.unlock(); - } - } - - @Override - public void packetDiscarded(Collection discardedPackets) { - lock.lock(); - try { - for (QuicPacket packet : discardedPackets) { - if (inFlight(packet)) { - bytesInFlight -= packet.size(); - } - } - } finally { - lock.unlock(); - } - } - - @Override - public long congestionWindow() { - lock.lock(); - try { - return congestionWindow; - } finally { - lock.unlock(); - } - } - - @Override - public long initialWindow() { - lock.lock(); - try { - return Math.max(14720, 2 * maxDatagramSize); - } finally { - lock.unlock(); - } - } - - @Override - public long maxDatagramSize() { - lock.lock(); - try { - return maxDatagramSize; - } finally { - lock.unlock(); - } - } - - @Override - public boolean isSlowStart() { - lock.lock(); - try { - return congestionWindow < ssThresh; - } finally { - lock.unlock(); - } - } - - @Override - public void updatePacer(Deadline now) { - lock.lock(); - try { - pacer.updateQuota(now); - } finally { - lock.unlock(); - } - } - - @Override - public boolean isPacerLimited() { - lock.lock(); - try { - return !pacer.canSend(); - } finally { - lock.unlock(); - } - } - - @Override - public boolean isCwndLimited() { - lock.lock(); - try { - return congestionWindow - bytesInFlight < maxDatagramSize; - } finally { - lock.unlock(); - } - } - - @Override - public Deadline pacerDeadline() { - lock.lock(); - try { - return pacer.twoPacketDeadline(); - } finally { - lock.unlock(); - } - } - - @Override - public void appLimited() { - lock.lock(); - try { - pacer.appLimited(); - } finally { - lock.unlock(); - } - } } diff --git a/test/jdk/java/net/httpclient/http3/H3MultipleConnectionsToSameHost.java b/test/jdk/java/net/httpclient/http3/H3MultipleConnectionsToSameHost.java index 45024c58e1f57..34d5163760f01 100644 --- a/test/jdk/java/net/httpclient/http3/H3MultipleConnectionsToSameHost.java +++ b/test/jdk/java/net/httpclient/http3/H3MultipleConnectionsToSameHost.java @@ -94,6 +94,31 @@ * limit is artificially low, in order to cause concurrent connections * to the same host to be created, with non-blocking IO and selector */ +/* + * @test id=reno-cc + * @bug 8087112 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm/timeout=360 -XX:+CrashOnOutOfMemoryError + * -Djdk.httpclient.quic.idleTimeout=120 + * -Djdk.httpclient.keepalive.timeout.h3=120 + * -Djdk.test.server.quic.idleTimeout=90 + * -Djdk.httpclient.quic.minPtoBackoffTime=60 + * -Djdk.httpclient.quic.maxPtoBackoffTime=120 + * -Djdk.httpclient.quic.maxPtoBackoff=9 + * -Djdk.httpclient.http3.maxStreamLimitTimeout=0 + * -Djdk.httpclient.quic.maxEndpoints=1 + * -Djdk.httpclient.quic.maxBidiStreams=2 + * -Djdk.httpclient.retryOnStreamlimit=50 + * -Djdk.httpclient.HttpClient.log=errors,http3,quic:hs:retransmit + * -Dsimpleget.requests=100 + * -Djdk.internal.httpclient.quic.congestionController=reno + * H3MultipleConnectionsToSameHost + * @summary Send 100 large concurrent requests, with connections whose max stream + * limit is artificially low, in order to cause concurrent connections + * to the same host to be created, with Reno congestion controller + */ // Interesting additional settings for debugging and manual testing: // ----------------------------------------------------------------- diff --git a/test/jdk/java/net/httpclient/http3/H3SimpleGet.java b/test/jdk/java/net/httpclient/http3/H3SimpleGet.java index e75ad04263a55..fe0217e64b600 100644 --- a/test/jdk/java/net/httpclient/http3/H3SimpleGet.java +++ b/test/jdk/java/net/httpclient/http3/H3SimpleGet.java @@ -148,6 +148,18 @@ * H3SimpleGet */ +/* + * @test id=reno-cc + * @bug 8087112 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestUtil + * jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm/timeout=480 -Djdk.internal.httpclient.quic.congestionController=reno + * H3SimpleGet + * @summary send multiple GET requests using Reno congestion controller + */ + + // Interesting additional settings for debugging and manual testing: // ----------------------------------------------------------------- // -Djdk.httpclient.HttpClient.log=requests,errors,quic:retransmit:control,http3 diff --git a/test/jdk/java/net/httpclient/http3/H3SimpleTest.java b/test/jdk/java/net/httpclient/http3/H3SimpleTest.java index 73c766f1eab25..8531f10bea7dd 100644 --- a/test/jdk/java/net/httpclient/http3/H3SimpleTest.java +++ b/test/jdk/java/net/httpclient/http3/H3SimpleTest.java @@ -46,7 +46,7 @@ * @test * @summary Basic test to verify that simple GET/POST/HEAD * requests work as expected with HTTP/3, using IPv4 - * or IPv6 + * or IPv6, using CUBIC or Reno * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.test.lib.net.SimpleSSLContext * jdk.httpclient.test.lib.common.HttpServerAdapters @@ -64,6 +64,11 @@ * -Djdk.httpclient.HttpClient.log=requests,responses,errors * -Djava.net.preferIPv4Stack=true * H3SimpleTest + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * -Djdk.internal.httpclient.quic.congestionController=reno + * H3SimpleTest */ // -Djava.security.debug=all public class H3SimpleTest implements HttpServerAdapters { diff --git a/test/jdk/java/net/httpclient/quic/CubicTest.java b/test/jdk/java/net/httpclient/quic/CubicTest.java new file mode 100644 index 0000000000000..f4b08e6d9b7eb --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/CubicTest.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.TimeLine; +import jdk.internal.net.http.quic.*; +import jdk.internal.net.http.quic.frames.PaddingFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.packets.QuicPacket; +import org.junit.jupiter.api.Test; + +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalUnit; +import java.util.List; + +import static jdk.internal.net.http.quic.QuicCubicCongestionController.ALPHA; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/* + * @test + * @run junit/othervm -Djdk.httpclient.HttpClient.log=trace,quic:cc CubicTest + */ +public class CubicTest { + static class TimeSource implements TimeLine { + final Deadline first = jdk.internal.net.http.common.TimeSource.now(); + volatile Deadline current = first; + public synchronized Deadline advance(long duration, TemporalUnit unit) { + return current = current.plus(duration, unit); + } + public Deadline advanceMillis(long millis) { + return advance(millis, ChronoUnit.MILLIS); + } + @Override + public Deadline instant() { + return current; + } + } + + private final TimeSource timeSource = new TimeSource(); + + private class TestQuicPacket implements QuicPacket { + private final int size; + + public TestQuicPacket(int size) { + this.size = size; + } + + @Override + public List frames() { + // fool congestion controller that this packet is in flight + return List.of(new PaddingFrame(1)); + } + + @Override + public QuicConnectionId destinationId() { + throw new AssertionError("Should not come here"); + } + + @Override + public PacketNumberSpace numberSpace() { + throw new AssertionError("Should not come here"); + } + + @Override + public int size() { + return size; + } + + @Override + public HeadersType headersType() { + throw new AssertionError("Should not come here"); + } + + @Override + public PacketType packetType() { + throw new AssertionError("Should not come here"); + } + } + + @Test + public void testReduction() { + System.err.println("***** testReduction *****"); + QuicRttEstimator rtt = new QuicRttEstimator(); + rtt.consumeRttSample(1, 0, Deadline.MIN); + QuicCongestionController cc = new QuicCubicCongestionController(timeSource, rtt); + int packetSize = (int) cc.maxDatagramSize(); + assertEquals(cc.initialWindow(), cc.congestionWindow(), "Unexpected starting congestion window"); + do { + cc.packetSent(packetSize); + // reduce to 70% of the last value, but not below 2*SMSS + long newCongestionWindow = Math.max((long) (QuicCubicCongestionController.BETA * cc.congestionWindow()), 2 * packetSize); + cc.packetLost(List.of(new TestQuicPacket(packetSize)), Deadline.MAX, false); + assertEquals(newCongestionWindow, cc.congestionWindow(), "Unexpected reduced congestion window"); + } while (cc.congestionWindow() > 2 * packetSize); + } + + @Test + public void testAppLimited() { + System.err.println("***** testAppLimited *****"); + QuicRttEstimator rtt = new QuicRttEstimator(); + rtt.consumeRttSample(1, 0, Deadline.MIN); + QuicCongestionController cc = new QuicCubicCongestionController(timeSource, rtt); + int packetSize = (int) cc.maxDatagramSize(); + assertEquals(cc.initialWindow(), cc.congestionWindow(), "Unexpected starting congestion window"); + cc.packetSent(packetSize); + long newCongestionWindow = (long) (QuicCubicCongestionController.BETA * cc.congestionWindow()); + // lose packet to exit slow start + cc.packetLost(List.of(new TestQuicPacket(packetSize)), Deadline.MAX, false); + assertEquals(newCongestionWindow, cc.congestionWindow(), "Unexpected reduced congestion window"); + Deadline sentTime = timeSource.instant().plus(1, ChronoUnit.NANOS); + // congestion window should not increase when sender is app-limited + cc.packetSent(packetSize); + cc.packetAcked(packetSize, sentTime); + assertEquals(newCongestionWindow, cc.congestionWindow(), "Unexpected congestion window change"); + } + + @Test + public void testRenoFriendly() { + System.err.println("***** testRenoFriendly *****"); + QuicRttEstimator rtt = new QuicRttEstimator(); + rtt.consumeRttSample(1, 0, Deadline.MIN); + QuicCongestionController cc = new QuicCubicCongestionController(timeSource, rtt); + int packetSize = (int) cc.maxDatagramSize(); + assertEquals(cc.initialWindow(), cc.congestionWindow(), "Unexpected starting congestion window"); + int startingWindow = (int) cc.congestionWindow(); + // lose packet to exit slow start + cc.packetSent(packetSize); + long newCongestionWindow = (long) (QuicCubicCongestionController.BETA * cc.congestionWindow()); + cc.packetLost(List.of(new TestQuicPacket(packetSize)), timeSource.instant(), false); + assertEquals(newCongestionWindow, cc.congestionWindow(), "Unexpected reduced congestion window"); + // exit loss recovery to start increasing cwnd + Deadline sentTime = timeSource.advanceMillis(1); + do { + // test that the window increases roughly by ALPHA * maxDatagramSize every RTT + int startingCwnd = (int) cc.congestionWindow(); + cc.packetSent(startingCwnd); + // we ack the entire window in one call; in practice the increase will be slower + // because cwnd increases (and increase rate reduces) after every call to packetAcked + cc.packetAcked(startingCwnd, sentTime); + long expectedCwnd = (long) (startingCwnd + ALPHA * packetSize); + long actualCwnd = cc.congestionWindow(); + assertEquals(expectedCwnd, actualCwnd, 1.0, + "actual cwnd not within the expected range"); + } while (cc.congestionWindow() < startingWindow); + // test that the window increases roughly by maxDatagramSize every RTT after passing cwndPrior + int startingCwnd = (int) cc.congestionWindow(); + cc.packetSent(startingCwnd); + cc.packetAcked(startingCwnd, sentTime); + int expectedCwnd = startingCwnd + packetSize; + long actualCwnd = cc.congestionWindow(); + assertEquals(expectedCwnd, actualCwnd, 1.0, + "actual cwnd not within the expected range"); + } + + @Test + public void testCubic() { + /* + Manually created test vector: + - ramp up the congestion window to 36 packets + - trigger congestion; window will be reduced to 25.2 packets, K=3 seconds + - set RTT = 1.5 seconds, advance "t" to 1.5 seconds, + send and acknowledge a whole cwnd of data + - cwnd should be back to 36 packets, give or take a few bytes. + */ + System.err.println("***** testCubic *****"); + QuicRttEstimator rtt = new QuicRttEstimator(); + rtt.consumeRttSample(1_500_000, 0, Deadline.MIN); + QuicCongestionController cc = new QuicCubicCongestionController(timeSource, rtt); + int packetSize = (int) cc.maxDatagramSize(); + long cwnd = cc.congestionWindow(); + // ramp up the congestion window to 36 packets + int tmp = (int) (36 * packetSize - cwnd); + cc.packetSent(tmp + packetSize); + cc.packetAcked(tmp, timeSource.instant()); + assertEquals(36*packetSize, cc.congestionWindow(), "Unexpected congestion window"); + long newCongestionWindow = (long) (QuicCubicCongestionController.BETA * cc.congestionWindow()); + // trigger congestion; window will be reduced to 25.2 packets, K=3 seconds + cc.packetLost(List.of(new TestQuicPacket(packetSize)), timeSource.instant(), false); + assertEquals(newCongestionWindow, cc.congestionWindow(), "Unexpected reduced congestion window"); + // advance "t" to 1.5 seconds, + Deadline sentTime = timeSource.advanceMillis(1500); + // send and acknowledge a whole cwnd of data + tmp = (int) cc.congestionWindow(); + cc.packetSent(tmp); + // we ack the entire window in one call; in practice the increase will be slower + // because cwnd increases (and increase rate reduces) after every call to packetAcked + cc.packetAcked(tmp, sentTime); + long expectedCwnd = 36 * packetSize; + long actualCwnd = cc.congestionWindow(); + assertEquals(expectedCwnd, actualCwnd, 1.0, + "actual cwnd not within the expected range"); + } +}