-
Notifications
You must be signed in to change notification settings - Fork 396
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
Server streams request bodies forever when not completed normally #2297
Comments
/bounty $250 |
💎 $250 bounty • ZIOSteps to solve:
Thank you for contributing to zio/zio-http! Add a bounty • Share on socials
|
Hi, I've done some debugging and I think I've located the place where this behavior is being caused. I'll try to fix it /attempt #2297 Options |
@FVelasquezM: Reminder that in 7 days the bounty will become up for grabs, so please submit a pull request before then 🙏 |
I've been working on this issue for the last couple of days, but unfortunately I've been unable to solve it, I'll leave some of my findings here so, hopefully, someone more knowledgeable can pick it up from there and solve this issue: The gist of the issue seems to be that neither There could be a workaround by overriding adding a boolean var
There's an issue with this, however, as
When a client drops the listener is interrupting the fiber before As a temporary workaround, I've introduced a sleep before the fiber is interrupted. This seems to work, but only twice, the first two requests dropped by the client fail as expected, generating the "test stream failed" message described in the issue, however, any further drops hang (but small requests made with |
The bounty is up for grabs! Everyone is welcome to |
/attempt #2297 I'll try for a few days to do this. Options |
Hm, the crux of the issue is that the request handler thread is interrupted (by something, which is probably correct anyway). But the mystery remains is why "foldCauseZIO" doesn't catch that? Here's a comment from the ZIO code:
This is in conflict with the doc, which says: "They can recover from any error, even fiber interruptions." Any ZIO guru out there can explain to me how "fiber interruption" is different from "external interruption"? Thanks. And here's the actual cause of failure:
|
@lackhoa External interruption is when a fiber is interrupted by another fiber using |
So this is my summary of this issue so far (possibly conclusive?): Starting from the original description of the issue: from the test it appears that when the client disconnects after having sent a large request body, the server continues to iterate on the body stream and does not get interrupted. Two points:
There is a way to run code upon external interruption: using
Question remains: should the request handler fiber be interrupted when client disconnects like that? I don't know, would like input from maintainers to proceed. (UPDATE: well on second thought, obviously we can't return a response when the client disconnects, so the API is correct unless I'm missing something.) So tldr: the only bug is found in #2402. |
It seems indeed the stream is interrupted externally not running forever, and I was also unclear on the fact that such interruption is not caught by
I would expect that behavior to be random, no? For me it reliably does nothing on the first call and reliably does something on subsequent calls. Maybe classloading on the initial call is slow enough that the buffer gets filled but on subsequent calls they're already loaded so it's fast enough. What that suggests is the channel is marked active and read from too early. Demand should probably be signaled from downstream before data is read. So maybe
A response certainly cannot be sent over the wire, but I was expecting the streaming body to fail upon disconnect. That scenario might build up an error response, and that response would never be sent, but all other expected error handling could occur, like metrics and logging. I think it's reasonable to fail the streaming body instead of interrupting it on the grounds that client disconnects are expected cases, e.g. with TCP FIN/RST packets. I expect interrupts in situations like the server is shutting down, not normal per-connection handling. The programming model for that is a little friendlier I think. I already have to translate all expected error cases into a response in order to serve the app, it might be nice if I didn't have to think about every handler getting interrupted too. Put another way, it's one thing to have silent interruption when I opt into it with something like Also, interruptions not due to client disconnections could still send an error response over an active channel. That's not to say that interrupting in this case is wrong in any sense. It's a valid way to handle the scenario. It's just not what I was expected and not what I personally desire. |
The buffer is 16 chunk. In fact I think it must be filled every time. Tbh I don't know why sometimes it works either. I could look into it... but hey! It's broken code, and there's little incentive to investigate why broken code breaks the way it does. I'd advise you to test from myazinn's PR, and tell me if you can find a bug with it (I find that it works 100% of the time).
Personally I agree, allowing the handler fiber to run to completion and then discarding the response would be fair game. |
I'm certain that PR is functionally correct in as much as forking the fiber will always allow the stream to process chunks that get emitted to it during the callback registration. The fact that callback registration can and does emit chunks to the stream at all is what feels off to me. I don't really know - perhaps the buffering that's happening is inherently necessary for some reason. In any case, I guess this issue can be closed as not a bug. Perhaps a new issue is warranted to decide/design how client disconnections should be surfaced, if the maintainers are so inclined to reconsider the current interruption approach |
@lackhoa: Reminder that in 7 days the bounty will become up for grabs, so please submit a pull request before then 🙏 |
Actually seems I'm still having the issue which motivated this report, even incorporating the forked fiber from the other MR, and it turns out I had not isolated the cause as I thought. It seems related to I'm now trying to diagnose from this request handler ZIO.scoped {
req.body
.asStream
// using .toInputStream.flatMap deadlocks or something
// it never reaches the "interrupted" message
// after enough non-interruptions, new requests are never handled
.toInputStream
.flatMap { in =>
ZIO.logInfo("transferring") *>
ZIO.attemptBlocking(in.transferTo(new java.io.OutputStream() {
def write(byte: Int): Unit = {}
})) <*
ZIO.logInfo("transferred")
}
// not using .toInputStream.flatMap and just using .runDrain reliably completes and prints "interrupted"
// .runDrain
.foldCauseZIO(
c => ZIO.logWarningCause("test stream failed", Cause.fail(c)).as(Response.status(Status.InternalServerError)),
_ => ZIO.logInfo("test stream done").as(Response.status(Status.NoContent))
)
.onInterrupt(ZIO.logWarning("interrupted"))
} |
@jgulotta The reason why "onInterrupt" isn't called is because what you're doing can't be interrupted. This is the docs of "transferTo"
However, even then, I think there's still a bug in the body stream (it should stop the stream when the client disconnects, but I can't figure out how to do it yet). I think you should file a different issue with that program. We're getting quite far off with this issue already :) It should be put to rest. |
In the general case where one can pass in an arbitrary input and/or output stream, you're right, the behavior through the interface is unspecified. In this specific case that is not the reason because the classes involved are well defined: The trouble seems to be the effect from I do intend to file another issue when I feel I've actually isolated it enough. But I also think we're still in the spirit of this one as the only difference is instead of |
@jgulotta Ok you’re right. The doc I quoted above did say that the behavior is implementation-specific. In that case I have no idea why it can’t be interrupted. I’ll have to look into the implementation a bit more… |
The bounty is up for grabs! Everyone is welcome to |
Is this issue still open? I'd like to take a look. |
Trust me, you don't :) |
Don't underestimate my willingness to humiliate myself. :) |
Well if what you're after is the bounty, you're never gonna get it no matter how good you are. This issue is like a hydra, you cut off one head, it grows back two other. So it's worse if you manage to do something, because you won't get any reward for it. Read the previous comments if you don't believe me. |
💡 @kyri-petrou submitted a pull request that claims the bounty. You can visit your bounty board to reward. |
🎉🎈 @kyri-petrou has been awarded $250! 🎈🎊 |
Describe the bug
When a request is not completed successfully, e.g. the connection drops, the streaming body does not terminate
To Reproduce
yes hello | http --chunked POST localhost:8080/forever
The output
read chunk size 5
will print continuallyCtrl+C
The output
read chunk size 5
will still print continually, until the buffer is cleared, and then nothing.Expected behaviour
The stream from
req.body.asStream
interrupts or dies, printing the final messages infoldCauseZIO
Curiously, I have to make one request, cancel it, and then make a second to actually get any output at all. I also expect the initial request would trigger the streaming.
The text was updated successfully, but these errors were encountered: