Skip to content

Conversation

@djelinski
Copy link
Member

@djelinski djelinski commented Nov 7, 2025

CUBIC is a standard TCP congestion control algorithm that uses a cubic function instead of a linear congestion window increase function to improve scalability and stability over fast and long-distance networks. CUBIC has been adopted as the default TCP congestion control algorithm by the Linux, Windows, and Apple stacks.

This PR adds a new congestion controller algorithm. It reuses a large part of the QuicRenoCongestionController, which was refactored to two classes - QuicBaseCongestionController, containing the shared code, and QuicRenoCongestionController, containing only the code that is unique to Reno.

CUBIC is now the default congestion controller. Reno can still be selected by setting the system property jdk.httpclient.quic.congestionController to reno.

A new test was added to exercise the new congestion controller. Existing tests continue to pass.


Progress

  • Change must be properly reviewed (1 review required, with at least 1 Reviewer)
  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue

Issue

  • JDK-8371475: HttpClient: Implement CUBIC congestion controller (Enhancement - P4)

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/28195/head:pull/28195
$ git checkout pull/28195

Update a local copy of the PR:
$ git checkout pull/28195
$ git pull https://git.openjdk.org/jdk.git pull/28195/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 28195

View PR using the GUI difftool:
$ git pr show -t 28195

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/28195.diff

Using Webrev

Link to Webrev Comment

@bridgekeeper
Copy link

bridgekeeper bot commented Nov 7, 2025

👋 Welcome back djelinski! A progress list of the required criteria for merging this PR into pr/28156 will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk
Copy link

openjdk bot commented Nov 7, 2025

❗ This change is not yet ready to be integrated.
See the Progress checklist in the description for automated requirements.

@openjdk openjdk bot changed the title 8371475 8371475: HttpClient: Implement CUBIC congestion controller Nov 7, 2025
@openjdk openjdk bot added the net net-dev@openjdk.org label Nov 7, 2025
@openjdk
Copy link

openjdk bot commented Nov 7, 2025

@djelinski The following label will be automatically applied to this pull request:

  • net

When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing list. If you would like to change these labels, use the /label pull request command.

@djelinski djelinski marked this pull request as ready for review November 7, 2025 13:56
@openjdk openjdk bot added the rfr Pull request is ready for review label Nov 7, 2025
@mlbridge
Copy link

mlbridge bot commented Nov 7, 2025

Webrevs

@AlanBateman
Copy link
Contributor

If jdk.httpclient.quic.congestionController is exposed then we'll need to track this with a CSR.

Copy link
Contributor

@vy vy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really neat work @djelinski! I liked the surgery you carried out in QuicBaseCC. 💯

I've dropped some minor remarks. I guess we will need some time to wrap our mind around the math involved in the PR.


private final QuicPacer pacer;

class QuicRenoCongestionController extends QuicBaseCongestionController {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess thins can be final and consequently protected modifier can be removed from the implemented methods.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed the class to final. The protected modifier is still needed on overridden methods.

Comment on lines 46 to 47
boolean isAppLimited;
isAppLimited = congestionWindow > maxBytesInFlight + 2L * maxDatagramSize;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
boolean isAppLimited;
isAppLimited = congestionWindow > maxBytesInFlight + 2L * maxDatagramSize;
boolean isAppLimited = congestionWindow > maxBytesInFlight + 2L * maxDatagramSize;

@djelinski, I see you verbatim copied these lines – which is fine, and makes things easier to review. Nevertheless, I want to double-check: do we need to guard against any arithmetic overflows here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion applied.

MaxBytesInFlight is limited by the amount of available memory, and soft-limited to 16M (see MAX_BYTES_IN_FLIGHT in base congestion controller). While it can cross the limit briefly, I don't expect it to get anywhere near the arithmetic limits.

@openjdk-notifier openjdk-notifier bot changed the base branch from pr/28156 to master November 12, 2025 12:40
@openjdk-notifier
Copy link

The parent pull request that this pull request depends on has now been integrated and the target branch of this pull request has been updated. This means that changes from the dependent pull request can start to show up as belonging to this pull request, which may be confusing for reviewers. To remedy this situation, simply merge the latest changes from the new target branch into this pull request by running commands similar to these in the local repository for your personal fork:

git checkout quic-cubic
git fetch https://git.openjdk.org/jdk.git master
git merge FETCH_HEAD
# if there are conflicts, follow the instructions given by git merge
git commit -m "Merge master"
git push

@openjdk
Copy link

openjdk bot commented Nov 12, 2025

@djelinski this pull request can not be integrated into master due to one or more merge conflicts. To resolve these merge conflicts and update this pull request you can run the following commands in the local repository for your personal fork:

git checkout quic-cubic
git fetch https://git.openjdk.org/jdk.git master
git merge FETCH_HEAD
# resolve conflicts and follow the instructions given by git merge
git commit -m "Merge master"
git push

@openjdk openjdk bot added the merge-conflict Pull request has merge conflict with target branch label Nov 12, 2025
@openjdk openjdk bot removed the merge-conflict Pull request has merge conflict with target branch label Nov 12, 2025
Copy link
Member Author

@djelinski djelinski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @AlanBateman @vy for the reviews.

The property was not meant to be exposed; IMO CUBIC is superior to Reno in all aspects that matter. I renamed it to include internal.

We will probably need to revisit the configuration if and when we implement BBR, but we aren't quite there yet.


private final QuicPacer pacer;

class QuicRenoCongestionController extends QuicBaseCongestionController {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed the class to final. The protected modifier is still needed on overridden methods.

Comment on lines 46 to 47
boolean isAppLimited;
isAppLimited = congestionWindow > maxBytesInFlight + 2L * maxDatagramSize;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion applied.

MaxBytesInFlight is limited by the amount of available memory, and soft-limited to 16M (see MAX_BYTES_IN_FLIGHT in base congestion controller). While it can cross the limit briefly, I don't expect it to get anywhere near the arithmetic limits.

Comment on lines +62 to +69
protected long congestionWindow = INITIAL_WINDOW;
protected int maxDatagramSize = QuicConnectionImpl.DEFAULT_DATAGRAM_SIZE;
protected int minimumWindow = 2 * maxDatagramSize;
protected long bytesInFlight;
// maximum bytes in flight seen since the last congestion event
protected long maxBytesInFlight;
protected Deadline congestionRecoveryStartTime;
protected long ssThresh = Long.MAX_VALUE;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove the protected keyword on the mutable fields? All the subclasses are in the same package.

Comment on lines +73 to +84
protected QuicBaseCongestionController(String dbgTag, QuicRttEstimator rttEstimator) {
this.dbgTag = dbgTag;
this.timeSource = TimeSource.source();
this.pacer = new QuicPacer(rttEstimator, this);
}

// for testing
protected QuicBaseCongestionController(TimeLine source, QuicRttEstimator rttEstimator) {
this.dbgTag = "TEST";
this.timeSource = source;
this.pacer = new QuicPacer(rttEstimator, this);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
protected QuicBaseCongestionController(String dbgTag, QuicRttEstimator rttEstimator) {
this.dbgTag = dbgTag;
this.timeSource = TimeSource.source();
this.pacer = new QuicPacer(rttEstimator, this);
}
// for testing
protected QuicBaseCongestionController(TimeLine source, QuicRttEstimator rttEstimator) {
this.dbgTag = "TEST";
this.timeSource = source;
this.pacer = new QuicPacer(rttEstimator, this);
}
protected QuicBaseCongestionController(String dbgTag, QuicRttEstimator rttEstimator) {
this(dbgTag, TimeSource.source(), rttEstimator);
}
// Allows to pass a custom TimeLine when testing
QuicBaseCongestionController(String dbgTag, TimeLine source, QuicRttEstimator rttEstimator) {
this.dbgTag = dbgTag;
this.timeSource = source;
this.pacer = new QuicPacer(rttEstimator, this);
}


// for testing
public QuicCubicCongestionController(TimeLine source, QuicRttEstimator rttEstimator) {
super(source, rttEstimator);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
super(source, rttEstimator);
super("TEST", source, rttEstimator);

boolean isAppLimited = sentTime.isAfter(lastFullWindow);
if (!isAppLimited) {
if (wEstBytes < cwndPriorBytes) {
wEstBytes += Math.max((long) (ALPHA * maxDatagramSize * packetBytes / congestionWindow), 1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we assert that congestionWindow is > 2 ?

Comment on lines +123 to +124
if (dblTargetBytes > 1.5 * congestionWindow) {
targetBytes = (long) (1.5 * congestionWindow);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (dblTargetBytes > 1.5 * congestionWindow) {
targetBytes = (long) (1.5 * congestionWindow);
// targetLimit is 1.5 * congestionWindow
long targetLimit = congestionWindow + (congestionWindow >> 1)
if (dblTargetBytes > targetLimit) {
targetBytes = targetLimit;

targetBytes = (long)dblTargetBytes;
}
if (targetBytes > congestionWindow) {
congestionWindow += Math.max((targetBytes - congestionWindow) * packetBytes / congestionWindow, 1L);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can (targetBytes - congestionWindow) * packetBytes / congestionWindow overflow?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we assert that congestionWindow is >= minimumWindow after this operation?

// 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we assert that wMaxBytes >= congestionWindow ? It is not immediately obvious to me that this is guaranteed.

In particular:

        if (congestionWindow < wMaxBytes) {
            // fast convergence
            wMaxBytes = (long) ((1 + BETA) * congestionWindow / 2);

seems to imply that wMaxBytes will be less than congestion window since 1.7 < 2

So we would end up with a negative value for kNanos, which is defined as:

The time period in seconds it takes to increase the congestion window size at the beginning of the current congestion avoidance stage to Wmax.

so presumably negative values for kNanos are not expected?

@dfuch
Copy link
Member

dfuch commented Nov 14, 2025

And another question: after this change - do we still have tests for the Reno congestion controller? Or is everthing using Cubic?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

net net-dev@openjdk.org rfr Pull request is ready for review

Development

Successfully merging this pull request may close these issues.

4 participants