-
Notifications
You must be signed in to change notification settings - Fork 6.1k
8308310: HttpClient: Avoid logging or locking from within synchronized blocks #14038
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
👋 Welcome back dfuchs! A progress list of the required criteria for merging this PR into |
Webrevs
|
/label remove security |
@dfuch |
src/java.net.http/share/classes/jdk/internal/net/http/ConnectionPool.java
Outdated
Show resolved
Hide resolved
src/java.net.http/share/classes/jdk/internal/net/http/Http1Response.java
Outdated
Show resolved
Hide resolved
Co-authored-by: Andrey Turbanov <turbanoff@gmail.com>
…onPool.java Co-authored-by: Andrey Turbanov <turbanoff@gmail.com>
…ponse.java Co-authored-by: Andrey Turbanov <turbanoff@gmail.com>
src/java.net.http/share/classes/jdk/internal/net/http/ConnectionPool.java
Outdated
Show resolved
Hide resolved
src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java
Outdated
Show resolved
Hide resolved
src/java.net.http/share/classes/jdk/internal/net/http/Stream.java
Outdated
Show resolved
Hide resolved
src/java.net.http/share/classes/jdk/internal/net/http/Http1Response.java
Show resolved
Hide resolved
@dfuch this pull request can not be integrated into git checkout HttpClient-Logging-8308310
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 |
java.util.stream.Stream<ExpiryEntry> stream() { | ||
return list.stream(); | ||
} | ||
|
||
// should only be called while holding a synchronization | ||
// lock on the ConnectionPool | ||
// should only be called while holding the ConnectionPool stateLock. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello Daniel, all these methods which say a lock needs to be held when they are called, should we add a assert stateLock.isHeldByCurrentThread();
to make it verifiable? I understand we didn't have a similar assert when the comment said the method needs to be called while holding the monitor, but since we are changing this part now, perhaps we can add those asserts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The stateLock
variable is not available from within the method:
Non-static field 'stateLock' cannot be referenced from a static context
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, that wasn't clearly noticable in the diff.
@@ -342,8 +349,11 @@ CompletableFuture<ExchangeImpl<T>> sendHeadersAsync() { | |||
|
|||
if (debug.on()) debug.log("requestAction.headers"); | |||
List<ByteBuffer> data = requestAction.headers(); | |||
synchronized (lock) { | |||
lock.lock(); | |||
try { | |||
state = State.HEADERS; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this code be replaced by a call to switchState(...)
to be consistent with the rest of the places in this class which do a similar call?
} else if ((state & 0x02) == 0x02) { | ||
if (debug.on()) | ||
debug.log("ref count for %s already released", client); | ||
} | ||
state |= 0x02; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like previously (before the changes in this PR), the tryRelease
could incorrectly set this state
to 0x02
even when the state
was 0
. From what I see in this change, that seems to have been addressed now.
public synchronized void tryRelease() { | ||
if (state == 0x01) { | ||
public void tryRelease() { | ||
if (STATE.compareAndSet(this, (byte)0x01, (byte)0x03)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit - a space is missing between the cast and the value for the second and third parameters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK - will do - though I usually don't put a space there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's fine with me in the current form if this wasn't an oversight.
|
||
private void unlock() { | ||
lock.unlock(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any specific reason for creating private wrapper methods instead of just using lock.lock()
and lock.unlock()
at the call sites?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed private methods
boolean finalStream() { | ||
stateLock.lock(); | ||
try { | ||
return finalStream; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we can make finalStream
volatile and remove the use of locks when accessing and setting it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks doable
Exchange<T> previousExchange; | ||
synchronized (this) { | ||
previousExchange = this.exchange; | ||
this.exchange = exchange; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a behavioural change here as compared to before this change. Previously, we used to first call released()
on the old exchange and cancel()
the new exchange, before setting the this.exchange
to the new exchange. Now, with this change, we first switch the this.exchange
to the new exchange and then call released()
and cancel()
. Do you think that could have any (odd) issues?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good observation. When we reach here the previous exchange is practically terminated. Looking at the implementation of released(), and at the place where setExchange
is called, I don't think it matters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you Daniel for checking.
@@ -168,13 +178,20 @@ void testAsString(String uri) throws Exception { | |||
.map(cf -> cf.thenCompose(response -> assertbody(response, requests.get(response.request())))) | |||
.toArray(CompletableFuture<?>[]::new)) | |||
.join(); | |||
client.close(); | |||
virtualExecutor.close(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we do this in a try/finally block? Same for the other test method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Note that I avoided try-with-resource in order to ensure that the client would be closed first.
@@ -260,13 +271,13 @@ static void test(HttpsServer server, HttpClient client) throws Exception { | |||
done = true; | |||
} catch (CompletionException e) { | |||
if (!Platform.isWindows()) throw e; | |||
if (LIMIT.get() < REQUESTS) throw e; | |||
if (LIMIT.get() < MAX_LIMIT) throw e; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously, LIMIT.get()
was being compared against MAX_COUNT
, because REQUEST
was initialized (as final
) to that value. Now it's being compared to MAX_LIMIT
. Is this intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. I think the previous behaviour was buggy. The idea is to reduce concurrency if the underlying OS has trouble with too many concurrent connections.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What it says here is that if we already reduced the LIMIT once and it fails again we should fail.
The tests that have been selectively changed to use virtual threads for the HttpClient (and have been set with |
Yes - these were the bigger offenders causing virtual threads to get pinned. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The latest changes in f5986f93
look fine to me. There's just one suggestion that's remaining in the ConcurrentResponses
test about moving the closing of the client and virtual thread executor in a finally block, but other than that this looks good to me.
@dfuch This change now passes all automated pre-integration checks. ℹ️ This project also has non-automated pre-integration requirements. Please see the file CONTRIBUTING.md for details. After integration, the commit message for the final commit will be:
You can use pull request commands such as /summary, /contributor and /issue to adjust it as needed. At the time when this comment was updated there had been 20 new commits pushed to the
As there are no conflicts, your changes will automatically be rebased on top of these commits when integrating. If you prefer to avoid this automatic rebasing, please check the documentation for the /integrate command for further details. ➡️ To integrate this PR with the above commit message to the |
/integrate |
Going to push as commit 736b90d.
Your commit was automatically rebased without conflicts. |
Please find here a change that revisits usage of monitors in the HttpClient.
With Virtual Threads now part of the platform it should be possible to pass a newVirtualThreadPerTaskExecutor to the HttpClient. Logging, when enabled, and when called from a synchronized block, can cause the carrier thread to get pinned in case of contention when printing through the underlying PrintStream.
This change aims at avoiding situations where the carrier threads might get pinned.
Progress
Issue
Reviewers
Reviewing
Using
git
Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/14038/head:pull/14038
$ git checkout pull/14038
Update a local copy of the PR:
$ git checkout pull/14038
$ git pull https://git.openjdk.org/jdk.git pull/14038/head
Using Skara CLI tools
Checkout this PR locally:
$ git pr checkout 14038
View PR using the GUI difftool:
$ git pr show -t 14038
Using diff file
Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/14038.diff
Webrev
Link to Webrev Comment