Hi Michael - Some follow-up/updates on items from my previous email inline below:
On Mon, Oct 31, 2016 at 6:13 PM, Tobias Thierer <tobi...@google.com> wrote: > > On Fri, Oct 28, 2016 at 12:28 PM, Michael McMahon < > michael.x.mcma...@oracle.com> wrote: > >> >> The intention behind those interfaces is primarily to provide a >> conversion between ByteBuffers and higher level objects that users >> are interested in. So, HttpRequest.BodyProcessor generates ByteBuffers >> for output and HttpResponse.ByteBuffer consumes them >> on input. For response bodies, there is this additional implicit >> requirement of needing to do something with the incoming data >> and that is why the option of discarding it exists (the details of that >> API are certainly open to discussion) >> >> On Publisher/Subscriber (Flow) versus InputStream/OutputStream, we wanted >> a non blocking API >> > > I can understand the desire to have a non blocking API, especially if > performance is seen as more important for the API than usability. Cronet > (the Chromium networking stack) API [documentation > <https://chromium.googlesource.com/chromium/src/+/lkgr/components/cronet>] > has a > > UrlRequest.read(ByteBuffer) <-> callback.onReadCompleted(UrlRequest, > UrlResponseInfo, ByteBuffer) > > call cycle that is not too different from OpenJDK 9 HTTP client's > > subscription.request(1) <-> BodyProcessor.onNext(ByteBuffer) > > so the general idea is shared by others (I'm meeting with some Cronet > folks later this week so I hope to gather their input / views soon). > I've meanwhile met with Cronet folks and now agree with your view that an async API is useful for applications that prioritize performance over simplicity. That said, I still think that many applications will prefer the simplicity of a blocking API - either provided as part of the platform or as a separate library. FYI as an experiment to see how easy this could be, I've prototyped a blocking API on top of the async API, specifically an adapter from OpenJDK 9's HttpResponse.BodyProcessor to an InputStream. The way I've prototyped it was to use a PipedInputStream internally whose buffer size is 32KBytes (2 * the size of the ByteBuffers used to deliver the response); the number of bytes free in that internal buffer (a) decreases when onResponseBodyChunk() copies bytes into the associated PipedOutputStream, (b) increases when the application reads bytes from the PipedInputStream. The PipedInputStream is wrapped in another InputStream that calls flowController.accept(1) every time (b) frees up enough bytes in the internal buffer to fit everything that may be delivered in the next call to onResponseBodyChunk(); this ensures that onResponseBodyChunk() never blocks. The InputStream wrapper methods also take care of throwing IOException after onResponseError(Throwable) occurred. All of this seemed fairly straightforward, but I did run into some problems: - I haven't succeeded in building OpenJDK 9 from source, following the instructions (I always run into a NullPointerException in XMLDTDProcessor during the BUILD_TOOLS_CORBA part of "make all"), so I've been unable to empirically try / benchmark my prototype code. - Obviously, copying data into/out of the PipedInputStream's internal buffer is overhead; this consciously trades performance for simplicity of the API. (I note that ResponseContent.copyBuffer() also performs copying but I haven't checked if this can be avoided). - flowController.accept(1) is only called when the underlying PipedInputStream.read() returns, and then there will still be additional latency before more data is delivered to onResponseBodyChunk(). These delays will impair performance more than should ideally be required but I haven't been able to quantify this through benchmarks because of my problems of building OpenJDK 9 from source. - Further contributing to this latency is the fact that the wrapper doesn't have good control over how many bytes will be delivered during the next onResponseBodyChunk(). Note that I've run into this same limitation on my previous prototype for the use case of parsing image data. Further note that Cronet's API *does* give the application control over how many bytes at most will be delivered during the next onReadCompleted(UrlRequest, UrlResponseInfo, ByteBuffer) - see my previous email. I previously thought that it was the BodyProcessor's job to construct the ByteBuffer objects but I may be mistaken - in the latest version of the API, this seems to be an implementation detail of HttpClientImpl and not part of the public API. On the question of push versus pull, there seems to be differing >> philosophies around this, but >> actually the publish/subscribe model provided by the Flow types has >> characteristics of both push and pull. >> It is primarily push() as in Subscriber.onNext() delivers the next buffer >> to the subscriber. But, the subscriber >> can effectively control it through the Subscription object. >> > > What does "effectively control" means? As far as I can see, the subscriber > can stop calls temporarily by not calling request(), but e.g. it can not > request a minimum number of bytes to deliver (if available before EOF) on > the next call. Is that correct or am I missing something? (See above > question about whether ByteBuffers are completely filled). > I'm still interested in the answer to this question :) 2.) HttpHeaders: I love that there is a type for this abstraction! But: >> >> - >> >> Why is the type an interface rather than a concrete, final class? >> Since this is a pure value type, there doesn’t seem to be much point in >> allowing alternative implementations? >> >> You could conceive of different implementations, with different tradeoffs >> between parsing early or lazily for instance. >> > > What parsing are you referring to? > FYI I've meanwhile investigated in more detail whether lazy parsing might be beneficial as part of HPACK decoding but I don't see how it could be: - A HTTP 2 implementation MUST (RFC 7540 section 4.3 <https://tools.ietf.org/html/rfc7540#section-4.3>) sufficiently process of header frames to update the decompression state and to signal a COMPRESSION_ERROR in the case of an error. I take this to mean that at least the HPACK decoder's Dynamic Table of header entries needs to be kept up to date. - In the HPACK decoder, decoding String literals involves negligible work beyond figuring out where they start/end so there doesn't seem to be a potential to do work lazily. - Not even the Huffman decoding seems to offer significant opportunity for lazy work because HPACK requires that "*A Huffman-encoded string literal containing the EOS symbol MUST be treated as a decoding error*" (RFC 7541 section 5.2 <https://tools.ietf.org/html/rfc7541#section-5.2>). Because Huffman codes are generally not self-synchronizing <https://en.wikipedia.org/wiki/Self-synchronizing_code>, the only way to figure out on what bit the last symbol starts seems to be to decode the entire sequence. What am I missing? I still don't see why HttpHeaders should be (or at least include) an immutable, final value type class. In general, do you disagree that ease of providing an alternative HttpClient implementation is important? Tobias