Skip to content

Commit 723588a

Browse files
committed
8338569: HTTP/1.1 CleanupTrigger may be triggerred after the next exchange started
Reviewed-by: jpai
1 parent 362f9ce commit 723588a

File tree

6 files changed

+167
-64
lines changed

6 files changed

+167
-64
lines changed

src/java.net.http/share/classes/jdk/internal/net/http/ConnectionPool.java

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -44,6 +44,7 @@
4444

4545
import jdk.internal.net.http.common.Deadline;
4646
import jdk.internal.net.http.common.FlowTube;
47+
import jdk.internal.net.http.common.Log;
4748
import jdk.internal.net.http.common.Logger;
4849
import jdk.internal.net.http.common.TimeLine;
4950
import jdk.internal.net.http.common.TimeSource;
@@ -492,13 +493,13 @@ void clear() {
492493

493494
// Remove a connection from the pool.
494495
// should only be called while holding the ConnectionPool stateLock.
495-
private void removeFromPool(HttpConnection c) {
496+
private boolean removeFromPool(HttpConnection c) {
496497
assert stateLock.isHeldByCurrentThread();
497498
if (c instanceof PlainHttpConnection) {
498-
removeFromPool(c, plainPool);
499+
return removeFromPool(c, plainPool);
499500
} else {
500501
assert c.isSecure() : "connection " + c + " is not secure!";
501-
removeFromPool(c, sslPool);
502+
return removeFromPool(c, sslPool);
502503
}
503504
}
504505

@@ -524,18 +525,36 @@ private boolean contains0(HttpConnection c) {
524525
return false;
525526
}
526527

527-
void cleanup(HttpConnection c, Throwable error) {
528+
void cleanup(HttpConnection c, long pendingData, Throwable error) {
528529
if (debug.on())
529530
debug.log("%s : ConnectionPool.cleanup(%s)",
530531
String.valueOf(c.getConnectionFlow()), error);
531532
stateLock.lock();
533+
boolean removed;
532534
try {
533-
removeFromPool(c);
535+
removed = removeFromPool(c);
534536
expiryList.remove(c);
535537
} finally {
536538
stateLock.unlock();
537539
}
538-
c.close();
540+
if (!removed && pendingData != 0) {
541+
// this should not happen; the cleanup may have consumed
542+
// some data that wasn't supposed to be consumed, so
543+
// the only thing we can do is log it and close the
544+
// connection.
545+
if (Log.errors()) {
546+
Log.logError("WARNING: CleanupTrigger triggered for" +
547+
" a connection not found in the pool: closing {0}", c.dbgString());
548+
}
549+
if (debug.on()) {
550+
debug.log("WARNING: CleanupTrigger triggered for" +
551+
" a connection not found in the pool: closing %s", c.dbgString());
552+
}
553+
Throwable cause = new IOException("Unexpected cleanup triggered for non pooled connection", error);
554+
c.close(cause);
555+
} else {
556+
c.close();
557+
}
539558
}
540559

541560
/**
@@ -549,32 +568,37 @@ private final class CleanupTrigger implements
549568

550569
private final HttpConnection connection;
551570
private volatile boolean done;
571+
private volatile boolean dropped;
552572

553573
public CleanupTrigger(HttpConnection connection) {
554574
this.connection = connection;
555575
}
556576

557577
public boolean isDone() { return done;}
558578

559-
private void triggerCleanup(Throwable error) {
579+
private void triggerCleanup(long pendingData, Throwable error) {
560580
done = true;
561-
cleanup(connection, error);
581+
if (debug.on()) {
582+
debug.log("Cleanup triggered for %s: pendingData:%s error:%s", this, pendingData, error);
583+
}
584+
cleanup(connection, pendingData, error);
562585
}
563586

564587
@Override public void request(long n) {}
565588
@Override public void cancel() {}
566589

567590
@Override
568591
public void onSubscribe(Flow.Subscription subscription) {
592+
if (dropped || done) return;
569593
subscription.request(1);
570594
}
571595
@Override
572-
public void onError(Throwable error) { triggerCleanup(error); }
596+
public void onError(Throwable error) { triggerCleanup(0, error); }
573597
@Override
574-
public void onComplete() { triggerCleanup(null); }
598+
public void onComplete() { triggerCleanup(0, null); }
575599
@Override
576600
public void onNext(List<ByteBuffer> item) {
577-
triggerCleanup(new IOException("Data received while in pool"));
601+
triggerCleanup(Utils.remaining(item), new IOException("Data received while in pool"));
578602
}
579603

580604
@Override
@@ -586,5 +610,10 @@ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
586610
public String toString() {
587611
return "CleanupTrigger(" + connection.getConnectionFlow() + ")";
588612
}
613+
614+
@Override
615+
public void dropSubscription() {
616+
dropped = true;
617+
}
589618
}
590619
}

src/java.net.http/share/classes/jdk/internal/net/http/SocketTube.java

Lines changed: 71 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -29,6 +29,7 @@
2929
import java.nio.ByteBuffer;
3030
import java.util.List;
3131
import java.util.Objects;
32+
import java.util.concurrent.ConcurrentLinkedQueue;
3233
import java.util.concurrent.Flow;
3334
import java.util.concurrent.atomic.AtomicLong;
3435
import java.util.concurrent.atomic.AtomicReference;
@@ -557,31 +558,33 @@ private final class InternalReadPublisher
557558
implements Flow.Publisher<List<ByteBuffer>> {
558559
private final InternalReadSubscription subscriptionImpl
559560
= new InternalReadSubscription();
560-
AtomicReference<ReadSubscription> pendingSubscription = new AtomicReference<>();
561+
ConcurrentLinkedQueue<ReadSubscription> pendingSubscriptions = new ConcurrentLinkedQueue<>();
561562
private volatile ReadSubscription subscription;
562563

563564
@Override
564565
public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
565566
Objects.requireNonNull(s);
566567

567568
TubeSubscriber sub = FlowTube.asTubeSubscriber(s);
568-
ReadSubscription target = new ReadSubscription(subscriptionImpl, sub);
569-
ReadSubscription previous = pendingSubscription.getAndSet(target);
570-
571-
if (previous != null && previous != target) {
569+
ReadSubscription previous;
570+
while ((previous = pendingSubscriptions.poll()) != null) {
572571
if (debug.on())
573572
debug.log("read publisher: dropping pending subscriber: "
574573
+ previous.subscriber);
575574
previous.errorRef.compareAndSet(null, errorRef.get());
575+
// make sure no data will be routed to the old subscriber.
576+
previous.stopReading();
576577
previous.signalOnSubscribe();
577578
if (subscriptionImpl.completed) {
578579
previous.signalCompletion();
579580
} else {
580581
previous.subscriber.dropSubscription();
581582
}
582583
}
584+
ReadSubscription target = new ReadSubscription(subscriptionImpl, sub);
585+
pendingSubscriptions.offer(target);
583586

584-
if (debug.on()) debug.log("read publisher got subscriber");
587+
if (debug.on()) debug.log("read publisher got new subscriber: " + s);
585588
subscriptionImpl.signalSubscribe();
586589
debugState("leaving read.subscribe: ");
587590
}
@@ -606,6 +609,7 @@ final class ReadSubscription implements Flow.Subscription {
606609
volatile boolean subscribed;
607610
volatile boolean cancelled;
608611
volatile boolean completed;
612+
private volatile boolean stopped;
609613

610614
public ReadSubscription(InternalReadSubscription impl,
611615
TubeSubscriber subscriber) {
@@ -623,11 +627,12 @@ public void cancel() {
623627

624628
@Override
625629
public void request(long n) {
626-
if (!cancelled) {
630+
if (!cancelled && !stopped) {
631+
// should be safe to not synchronize here.
627632
impl.request(n);
628633
} else {
629634
if (debug.on())
630-
debug.log("subscription cancelled, ignoring request %d", n);
635+
debug.log("subscription stopped or cancelled, ignoring request %d", n);
631636
}
632637
}
633638

@@ -661,6 +666,32 @@ void signalOnSubscribe() {
661666
signalCompletion();
662667
}
663668
}
669+
670+
/**
671+
* Called when switching subscriber on the {@link InternalReadSubscription}.
672+
* This subscriber is the old subscriber. Demand on the internal
673+
* subscription will be reset and reading will be paused until the
674+
* new subscriber is subscribed.
675+
* This should ensure that no data is routed to this subscriber
676+
* until the new subscriber is subscribed.
677+
*/
678+
synchronized void stopReading() {
679+
stopped = true;
680+
impl.demand.reset();
681+
}
682+
683+
synchronized boolean tryDecrementDemand() {
684+
if (stopped) return false;
685+
return impl.demand.tryDecrement();
686+
}
687+
688+
synchronized boolean isStopped() {
689+
return stopped;
690+
}
691+
692+
synchronized void increaseDemand(long n) {
693+
if (!stopped) impl.demand.increase(n);
694+
}
664695
}
665696

666697
final class InternalReadSubscription implements Flow.Subscription {
@@ -835,7 +866,7 @@ final void read() {
835866

836867
// If we reach here then we must be in the selector thread.
837868
assert client.isSelectorThread();
838-
if (demand.tryDecrement()) {
869+
if (current.tryDecrementDemand()) {
839870
// we have demand.
840871
try {
841872
List<ByteBuffer> bytes = readAvailable(current.bufferSource);
@@ -881,8 +912,10 @@ final void read() {
881912
// event. This ensures that this loop is
882913
// executed again when the socket becomes
883914
// readable again.
884-
demand.increase(1);
885-
resumeReadEvent();
915+
if (!current.isStopped()) {
916+
current.increaseDemand(1);
917+
resumeReadEvent();
918+
}
886919
if (errorRef.get() != null) continue;
887920
debugState("leaving read() loop with no bytes");
888921
return;
@@ -922,30 +955,35 @@ final void read() {
922955
}
923956

924957
boolean handlePending() {
925-
ReadSubscription pending = pendingSubscription.getAndSet(null);
926-
if (pending == null) return false;
927-
if (debug.on())
928-
debug.log("handling pending subscription for %s",
958+
ReadSubscription pending;
959+
boolean subscribed = false;
960+
while ((pending = pendingSubscriptions.poll()) != null) {
961+
subscribed = true;
962+
if (debug.on())
963+
debug.log("handling pending subscription for %s",
929964
pending.subscriber);
930-
ReadSubscription current = subscription;
931-
if (current != null && current != pending && !completed) {
932-
current.subscriber.dropSubscription();
933-
}
934-
if (debug.on()) debug.log("read demand reset to 0");
935-
subscriptionImpl.demand.reset(); // subscriber will increase demand if it needs to.
936-
pending.errorRef.compareAndSet(null, errorRef.get());
937-
if (!readScheduler.isStopped()) {
938-
subscription = pending;
939-
} else {
940-
if (debug.on()) debug.log("socket tube is already stopped");
941-
}
942-
if (debug.on()) debug.log("calling onSubscribe");
943-
pending.signalOnSubscribe();
944-
if (completed) {
965+
ReadSubscription current = subscription;
966+
if (current != null && current != pending && !completed) {
967+
debug.log("dropping pending subscription for current %s",
968+
current.subscriber);
969+
current.subscriber.dropSubscription();
970+
}
971+
if (debug.on()) debug.log("read demand reset to 0");
972+
subscriptionImpl.demand.reset(); // subscriber will increase demand if it needs to.
945973
pending.errorRef.compareAndSet(null, errorRef.get());
946-
pending.signalCompletion();
974+
if (!readScheduler.isStopped()) {
975+
subscription = pending;
976+
} else {
977+
if (debug.on()) debug.log("socket tube is already stopped");
978+
}
979+
if (debug.on()) debug.log("calling onSubscribe on " + pending.subscriber);
980+
pending.signalOnSubscribe();
981+
if (completed) {
982+
pending.errorRef.compareAndSet(null, errorRef.get());
983+
pending.signalCompletion();
984+
}
947985
}
948-
return true;
986+
return subscribed;
949987
}
950988
}
951989

src/java.net.http/share/classes/jdk/internal/net/http/common/FlowTube.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -180,6 +180,10 @@ public void onError(Throwable throwable) {
180180
public void onComplete() {
181181
delegate.onComplete();
182182
}
183+
@Override
184+
public String toString() {
185+
return "TubeSubscriberWrapper("+delegate.toString()+")";
186+
}
183187
}
184188

185189
}

test/jdk/java/net/httpclient/DigestEchoClient.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -64,7 +64,7 @@
6464
* @test
6565
* @summary this test verifies that a client may provides authorization
6666
* headers directly when connecting with a server.
67-
* @bug 8087112
67+
* @bug 8087112 8336655 8338569
6868
* @library /test/lib /test/jdk/java/net/httpclient/lib
6969
* @build jdk.httpclient.test.lib.common.HttpServerAdapters jdk.test.lib.net.SimpleSSLContext
7070
* DigestEchoServer ReferenceTracker DigestEchoClient

test/jdk/java/net/httpclient/ShutdownNow.java

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -210,12 +210,16 @@ void testConcurrent(String uriString) throws Exception {
210210
}
211211
CompletableFuture.allOf(responses.toArray(new CompletableFuture<?>[0])).get();
212212
} finally {
213-
if (client.awaitTermination(Duration.ofMillis(2000))) {
213+
if (client.awaitTermination(Duration.ofMillis(2500))) {
214214
out.println("Client terminated within expected delay");
215+
assertTrue(client.isTerminated());
215216
} else {
216-
throw new AssertionError("client still running");
217+
client = null;
218+
var error = TRACKER.check(500);
219+
if (error != null) throw error;
220+
throw new AssertionError("client was still running, but exited after further delay: "
221+
+ "timeout should be adjusted");
217222
}
218-
assertTrue(client.isTerminated());
219223
}
220224
}
221225

@@ -272,12 +276,16 @@ void testSequential(String uriString) throws Exception {
272276
}).thenCompose((c) -> c).get();
273277
}
274278
} finally {
275-
if (client.awaitTermination(Duration.ofMillis(2000))) {
279+
if (client.awaitTermination(Duration.ofMillis(2500))) {
276280
out.println("Client terminated within expected delay");
281+
assertTrue(client.isTerminated());
277282
} else {
278-
throw new AssertionError("client still running");
283+
client = null;
284+
var error = TRACKER.check(500);
285+
if (error != null) throw error;
286+
throw new AssertionError("client was still running, but exited after further delay: "
287+
+ "timeout should be adjusted");
279288
}
280-
assertTrue(client.isTerminated());
281289
}
282290
}
283291

0 commit comments

Comments
 (0)