async-http-client icon indicating copy to clipboard operation
async-http-client copied to clipboard

unclear docs: How to signal to StreamWriter that body is done?

Open weissi opened this issue 5 years ago • 28 comments

StreamWriter's complete docs are

        /// Chunk provider.
        public struct StreamWriter {
            /// Write data to server.
            ///
            /// - parameters:
            ///     - data: `IOData` to write.
            public func write(_ data: IOData) -> EventLoopFuture<Void>
        }

I understand how I can provide chunks: by calling write but how do I tell the StreamWriter that I'm done sending request body bytes?

weissi avatar Apr 05 '20 16:04 weissi

@weissi You provide it with a length at the start and once you've supplied that data, it completes. I'm not a great fan of this interface. It is really confusing what is happening. Here is my attempt at implementing one https://github.com/swift-aws/aws-sdk-swift-core/blob/bdbac3768398b8387e2a81be193b6c42da75979e/Sources/AWSSDKSwiftCore/HTTP/AsyncHTTPClient.swift#L23

adam-fowler avatar Apr 22 '20 16:04 adam-fowler

@adam-fowler but how do I do streaming? Ie. if I don't know the content length? Then chunked encoding should be selected...

weissi avatar Apr 22 '20 17:04 weissi

CC @artemredkin

weissi avatar Apr 22 '20 17:04 weissi

@adam-fowler but how do I do streaming? Ie. if I don't know the content length? Then chunked encoding should be selected...

Yes that's a good question. That would be very useful.

adam-fowler avatar Apr 24 '20 11:04 adam-fowler

@weissi is this something that could be resolved for 1.2. It doesn't look like that complex a thing to do. Return an empty ByteBuffer() to indicate end or something like that and add a chunked header.

adam-fowler avatar May 15 '20 13:05 adam-fowler

Oh shit just thought this might be a breaking change. Although you could just add another version of the stream function.

adam-fowler avatar May 15 '20 13:05 adam-fowler

@weissi answering your question, you just return succeeded future from write method:

.stream() { writer in
    writer.write()
    writer.write()
    ...
    writer.write()
    return eventLoop.makeSucceededFuture(())
}

I'm not a fan of this interface, it was designed mainly around how NonBlockingFileIO works...

artemredkin avatar May 15 '20 13:05 artemredkin

Obviously, this example has absolutely no backpressure, so another alternative will be something like:

.stream() { writer in
    writer.write().flatMap {
        writer.write()
    }
}

artemredkin avatar May 15 '20 13:05 artemredkin

Sorry @artem I completely missed that. For some reason I had got it into my head you were required to pass in a content length

adam-fowler avatar May 15 '20 15:05 adam-fowler

No worries, our docs in this area are not good, and API is not very clear at all. I'll try to improve streaming docs/API's after 1.2.0 release.

artemredkin avatar May 15 '20 15:05 artemredkin

@artemredkin Hmm, I don't get it. A user gets handed a StreamWriter, they then call .write for every bit of data they have, how do they communicate that that's everything? So that there's nothing more coming?

weissi avatar May 15 '20 17:05 weissi

user sets a body and passes it a closure, that has a singature:

(StreamWriter) -> EventLoopFuture<Void>

as soon as this future completes, we send .end

artemredkin avatar May 15 '20 17:05 artemredkin

@weissi I found it is a little confusing initially. I have a working version here, where you supply a stream function that returns ByteBuffers until you have finished streaming or have hit a specified size. https://github.com/swift-aws/aws-sdk-swift-core/blob/16cce9fe9fbc0f5949fb48a6fd7dce90b3645357/Sources/AWSSDKSwiftCore/HTTP/AsyncHTTPClient.swift#L78

adam-fowler avatar May 15 '20 17:05 adam-fowler

@artemredkin oooh, so the user's meant to allocate a promise and finish it when they're done with the body? That seems super dangerous, they need to then also detect all the possible error cases and funnel them into this promise, or else they'll crash.

I think we should create a new API that just gives you a done method on StreamWriter, no?

Or was there a use-case that this was designed for?

weissi avatar May 15 '20 17:05 weissi

@weissi they need to return a future. I allocated the promise so I wouldnt have to chain all the futures

adam-fowler avatar May 15 '20 17:05 adam-fowler

I built it around how NonBlockingFileIO works, readChunked specifically

artemredkin avatar May 15 '20 17:05 artemredkin

@adam-fowler yeah but to return a future that they can fulfil when they're done, they need to allocate a promise that they hold elsewhere.

So basically users need to keep track of two things:

  • a promise (that they MUST fulfil in all error cases too)
  • a stream writer

Confusingly, the feed new data through the stream writer but send end through the promise.

weissi avatar May 15 '20 17:05 weissi

@artemredkin readChunked is an API that gives you data, ie. a source. write is an API that you give data to, ie. a sink. They aren't related, are they?

The future you return in readChunked is to propagate backpressure and I think the API makes sense. The future you return in the stream writer providing closure is to indicate the end of a HTTP request body, I think that's quite confusing, no?

weissi avatar May 15 '20 17:05 weissi

it totally is super confusing, yeah, it was like a mirror API, so it turned out quite strange

artemredkin avatar May 15 '20 17:05 artemredkin

Yeah it is confusing. Took me a while to work out how to get it working. Personally I would prefer, an API where a stream closure returning an EventLoopFuture<ByteBuffer> is called repeatedly until it returns an empty ByteBuffer.

adam-fowler avatar May 15 '20 17:05 adam-fowler

How about a an enum:

enum Part {
    case next(ByteBuffer)
    case end
}

wdyt?

artemredkin avatar May 15 '20 17:05 artemredkin

So it returns an EventLoopFuture<Part>? Yeah that'd work

adam-fowler avatar May 15 '20 17:05 adam-fowler

@artemredkin we already got the StreamWriter type, why not just use methods?

streamWriter.write(...)
[...]
streamWriter.end(...)

I mean I'm happy with an enum too but then the API would become something like

streamWriter.do(.next(buffer))
[...]
streamWriter.do(.end(...))

I'd just use methods but don't mind. Either is a big step up from where we are right now.

One other thing that I found very inconvenient was that I had to wait for a callback to get the StreamWriter. Why can't I get it synchronously off the Task?

weissi avatar May 15 '20 17:05 weissi

Methods seem a nicer API, moving to 2.0.0 to introduce a new API

artemredkin avatar Jun 13 '20 11:06 artemredkin

@artemredkin we need to do this before 2.0.0. We can do this with deprecations, pretty sure.

weissi avatar Jun 14 '20 17:06 weissi