-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Properly adhere to Flow spec when converting flows
To map the JavaHttpClient's Flow.Publisher<List<ByteBuffer>> to our Flow.Publisher<ByteBuffer> we have to buffer byte buffers until downstream subscribers request them. We were previously just calling onNext with each individual ByteBuffer for as many buffers as we were handed by the client's publisher. In addition to now queueing these byte buffers when necessary, this commit also adds a dedicated DataStream for the JavaHttpClientTransport to avoid a bunch of intermediate conversions we were doing previously to convert the publisher to a ByteBuffer or to an InputStream. I added some basic test cases and also tested manually calling some services, which is how I discovered we needed to update system properties for the client statically too.
- Loading branch information
Showing
3 changed files
with
268 additions
and
40 deletions.
There are no files selected for viewing
129 changes: 129 additions & 0 deletions
129
client-http/src/main/java/software/amazon/smithy/java/client/http/HttpClientDataStream.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
/* | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package software.amazon.smithy.java.client.http; | ||
|
||
import java.io.InputStream; | ||
import java.nio.ByteBuffer; | ||
import java.util.List; | ||
import java.util.Queue; | ||
import java.util.concurrent.CompletableFuture; | ||
import java.util.concurrent.ConcurrentLinkedQueue; | ||
import java.util.concurrent.Flow; | ||
import java.util.concurrent.atomic.AtomicBoolean; | ||
import java.util.concurrent.atomic.AtomicLong; | ||
import software.amazon.smithy.java.io.datastream.DataStream; | ||
|
||
/** | ||
* This class defers turning the HTTP client's response publisher into an adapted publisher unless required. | ||
* | ||
* <p>This class avoids needing to use multiple intermediate adapters to go from a flow that publishes a list of | ||
* byte buffers to a flow that publishes single byte buffer. Instead, it directly implements asByteBuffer and | ||
* asInputStream to use a more direct integration from the HTTP client library. | ||
*/ | ||
record HttpClientDataStream( | ||
Flow.Publisher<List<ByteBuffer>> httpPublisher, | ||
long contentLength, | ||
String contentType) implements DataStream { | ||
@Override | ||
public boolean isReplayable() { | ||
return false; | ||
} | ||
|
||
@Override | ||
public CompletableFuture<ByteBuffer> asByteBuffer() { | ||
var p = java.net.http.HttpResponse.BodySubscribers.ofByteArray(); | ||
httpPublisher.subscribe(p); | ||
return p.getBody().thenApply(ByteBuffer::wrap).toCompletableFuture(); | ||
} | ||
|
||
@Override | ||
public CompletableFuture<InputStream> asInputStream() { | ||
var p = java.net.http.HttpResponse.BodySubscribers.ofInputStream(); | ||
httpPublisher.subscribe(p); | ||
return p.getBody().toCompletableFuture(); | ||
} | ||
|
||
@Override | ||
public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) { | ||
// Adapt the "Flow.Subscriber<List<ByteBuffer>" to "Flow.Subscriber<ByteBuffer>". | ||
httpPublisher.subscribe(new BbListToBbSubscriber(subscriber)); | ||
} | ||
|
||
private static final class BbListToBbSubscriber implements Flow.Subscriber<List<ByteBuffer>> { | ||
private final Flow.Subscriber<? super ByteBuffer> subscriber; | ||
|
||
BbListToBbSubscriber(Flow.Subscriber<? super ByteBuffer> subscriber) { | ||
this.subscriber = subscriber; | ||
} | ||
|
||
private Flow.Subscription upstreamSubscription; | ||
private final Queue<ByteBuffer> queue = new ConcurrentLinkedQueue<>(); | ||
private final AtomicLong demand = new AtomicLong(0); | ||
private final AtomicBoolean senderFinished = new AtomicBoolean(false); | ||
|
||
@Override | ||
public void onSubscribe(Flow.Subscription subscription) { | ||
upstreamSubscription = subscription; | ||
|
||
subscriber.onSubscribe(new Flow.Subscription() { | ||
@Override | ||
public void request(long n) { | ||
demand.addAndGet(n); | ||
drainAndRequest(); | ||
} | ||
|
||
@Override | ||
public void cancel() { | ||
upstreamSubscription.cancel(); | ||
} | ||
}); | ||
} | ||
|
||
@Override | ||
public void onError(Throwable throwable) { | ||
subscriber.onError(throwable); | ||
} | ||
|
||
@Override | ||
public void onNext(List<ByteBuffer> item) { | ||
queue.addAll(item); | ||
drainAndRequest(); | ||
} | ||
|
||
@Override | ||
public void onComplete() { | ||
// The sender is done sending us bytes, so when our queue is empty, emit onComplete downstream. | ||
senderFinished.set(true); | ||
drain(); | ||
} | ||
|
||
private void drain() { | ||
try { | ||
while (!queue.isEmpty() && demand.get() > 0) { | ||
ByteBuffer buffer = queue.poll(); | ||
if (buffer != null) { | ||
subscriber.onNext(buffer); | ||
demand.decrementAndGet(); | ||
} | ||
} | ||
// When we have no more buffered BBs and the sender has signaled they're done, then complete downstream. | ||
if (queue.isEmpty() && senderFinished.get()) { | ||
subscriber.onComplete(); | ||
} | ||
} catch (Exception e) { | ||
subscriber.onError(e); | ||
} | ||
} | ||
|
||
private void drainAndRequest() { | ||
drain(); | ||
|
||
if (queue.isEmpty() && !senderFinished.get()) { | ||
upstreamSubscription.request(demand.get()); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
114 changes: 114 additions & 0 deletions
114
...-http/src/test/java/software/amazon/smithy/java/client/http/HttpClientDataStreamTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
/* | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package software.amazon.smithy.java.client.http; | ||
|
||
import static org.hamcrest.MatcherAssert.assertThat; | ||
import static org.hamcrest.Matchers.equalTo; | ||
|
||
import java.nio.ByteBuffer; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.concurrent.CompletableFuture; | ||
import java.util.concurrent.Flow; | ||
import java.util.concurrent.SubmissionPublisher; | ||
import org.junit.jupiter.api.Test; | ||
|
||
public class HttpClientDataStreamTest { | ||
|
||
private static List<List<ByteBuffer>> createCannedBuffers() { | ||
return List.of( | ||
List.of(ByteBuffer.wrap("{\"hi\":".getBytes(StandardCharsets.UTF_8))), | ||
List.of( | ||
ByteBuffer.wrap("[1, ".getBytes(StandardCharsets.UTF_8)), | ||
ByteBuffer.wrap("2]".getBytes(StandardCharsets.UTF_8)) | ||
), | ||
List.of(ByteBuffer.wrap("}".getBytes(StandardCharsets.UTF_8))) | ||
); | ||
} | ||
|
||
private static final class CannedPublisher extends SubmissionPublisher<List<ByteBuffer>> { | ||
void pushData(List<List<ByteBuffer>> data) { | ||
data.forEach(this::submit); | ||
close(); | ||
} | ||
} | ||
|
||
@Test | ||
public void convertsToBb() throws Exception { | ||
var httpPublisher = new CannedPublisher(); | ||
var ds = new HttpClientDataStream(httpPublisher, 13, "application/json"); | ||
assertThat(ds.contentType(), equalTo("application/json")); | ||
assertThat(ds.contentLength(), equalTo(13L)); | ||
|
||
var cf = ds.asByteBuffer(); | ||
httpPublisher.pushData(createCannedBuffers()); | ||
|
||
var bb = cf.get(); | ||
assertThat(bb.remaining(), equalTo(13)); | ||
assertThat(new String(bb.array(), StandardCharsets.UTF_8), equalTo("{\"hi\":[1, 2]}")); | ||
} | ||
|
||
@Test | ||
public void convertsToInputStream() throws Exception { | ||
var httpPublisher = new CannedPublisher(); | ||
var ds = new HttpClientDataStream(httpPublisher, 13, "application/json"); | ||
var cf = ds.asInputStream(); | ||
httpPublisher.pushData(createCannedBuffers()); | ||
|
||
var is = cf.get(); | ||
assertThat(new String(is.readAllBytes(), StandardCharsets.UTF_8), equalTo("{\"hi\":[1, 2]}")); | ||
} | ||
|
||
@Test | ||
public void convertsToPublisher() throws Exception { | ||
var httpPublisher = new CannedPublisher(); | ||
var ds = new HttpClientDataStream(httpPublisher, 13, "application/json"); | ||
|
||
var collector = new CollectingSubscriber(); | ||
ds.subscribe(collector); | ||
httpPublisher.pushData(createCannedBuffers()); | ||
var results = collector.getResult().get(); | ||
|
||
assertThat(results, equalTo("{\"hi\":[1, 2]}")); | ||
} | ||
|
||
public static final class CollectingSubscriber implements Flow.Subscriber<ByteBuffer> { | ||
private final List<String> buffers = Collections.synchronizedList(new ArrayList<>()); | ||
private final CompletableFuture<String> result = new CompletableFuture<>(); | ||
private Flow.Subscription subscription; | ||
|
||
@Override | ||
public void onSubscribe(Flow.Subscription subscription) { | ||
this.subscription = subscription; | ||
subscription.request(Long.MAX_VALUE); | ||
} | ||
|
||
@Override | ||
public void onNext(ByteBuffer item) { | ||
buffers.add(new String(item.array(), StandardCharsets.UTF_8)); | ||
} | ||
|
||
@Override | ||
public void onError(Throwable throwable) { | ||
result.completeExceptionally(throwable); | ||
} | ||
|
||
@Override | ||
public void onComplete() { | ||
StringBuilder builder = new StringBuilder(); | ||
for (String buffer : buffers) { | ||
builder.append(buffer); | ||
} | ||
result.complete(builder.toString()); | ||
} | ||
|
||
public CompletableFuture<String> getResult() { | ||
return result; | ||
} | ||
} | ||
} |