Skip to content
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

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

Open
weissi opened this issue Apr 5, 2020 · 25 comments · May be fixed by #291
Open

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

weissi opened this issue Apr 5, 2020 · 25 comments · May be fixed by #291
Labels
enhancement New feature or request
Milestone

Comments

@weissi
Copy link
Contributor

weissi commented Apr 5, 2020

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 weissi added the bug Something isn't working label Apr 6, 2020
@adam-fowler
Copy link
Member

@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

@weissi
Copy link
Contributor Author

weissi commented Apr 22, 2020

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

@weissi
Copy link
Contributor Author

weissi commented Apr 22, 2020

CC @artemredkin

@adam-fowler
Copy link
Member

@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
Copy link
Member

@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
Copy link
Member

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

@artemredkin
Copy link
Collaborator

@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
Copy link
Collaborator

artemredkin commented May 15, 2020

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

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

@adam-fowler
Copy link
Member

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

@artemredkin
Copy link
Collaborator

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.

@weissi
Copy link
Contributor Author

weissi commented May 15, 2020

@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?

@artemredkin
Copy link
Collaborator

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

@adam-fowler
Copy link
Member

@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

@weissi
Copy link
Contributor Author

weissi commented May 15, 2020

@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?

@adam-fowler
Copy link
Member

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

@artemredkin
Copy link
Collaborator

I built it around how NonBlockingFileIO works, readChunked specifically

@weissi
Copy link
Contributor Author

weissi commented May 15, 2020

@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
Copy link
Contributor Author

weissi commented May 15, 2020

@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?

@artemredkin
Copy link
Collaborator

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

@adam-fowler
Copy link
Member

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.

@artemredkin
Copy link
Collaborator

How about a an enum:

enum Part {
    case next(ByteBuffer)
    case end
}

wdyt?

@adam-fowler
Copy link
Member

adam-fowler commented May 15, 2020

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

@weissi
Copy link
Contributor Author

weissi commented May 15, 2020

@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?

@artemredkin artemredkin added this to the 1.2.0 milestone May 20, 2020
@artemredkin
Copy link
Collaborator

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

@artemredkin artemredkin modified the milestones: 1.2.0, 2.0.0 Jun 13, 2020
@artemredkin artemredkin removed the bug Something isn't working label Jun 13, 2020
@artemredkin artemredkin added the enhancement New feature or request label Jun 13, 2020
@weissi
Copy link
Contributor Author

weissi commented Jun 14, 2020

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants