-
-
Notifications
You must be signed in to change notification settings - Fork 957
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
flush() after writing to gzip_file #2753
base: master
Are you sure you want to change the base?
Conversation
Can we add a test to prove your point? |
I'll work on that today. I have a small repro case I'll share but I need to make it run as a test |
I've added a test, but it's a bit complicated. Without the flush, the entire contents of the response are correct, but to show that they are received iteratively rather than all at once, I use a wrapping middleware to assert that GZipMiddleware isn't sending empty message bodies, which is what it does without the flush. |
5f8af49
to
2450b70
Compare
@Kludex could you take another look or recommend a good reviewer? Thanks! |
Not related. |
Any concerns here, or how can we best move this forward? |
The best way to move forward would be to present the problem first, with an MRE, and references to other issues where other people had the same problem. I think the current behavior is intentional, so I need to get more references around before reviewing this. |
If the behavior is intentional, we really need to update the documentation here:
to indicate that this will cause streaming responses to be buffered 32KiB at a time; this was certainly a surprising result for us, and one that caused our users to report that our app appeared broken as realtime status updates stopped working. |
This seems a valid PR. It seems other web frameworks have the same issue. I'm confused as to how no one noticed... It's even hard to find references about people having issues with this. 🤔 |
Okay. I look around, and based on what you said, I understand we have a real problem with SSEs. It seems we should either:
I'm not so sure about all the streaming responses. Reference to my future self: https://www.npmjs.com/package/compression#server-sent-events |
In order to better support streaming responses where the chunks are smaller than the file buffer size, we flush after writing. Without the explicit flush, the writes are buffered and the subsequent reads see an empty self.gzip_buffer until the file automatically flushes due to either (1) the write buffer fills, probably at 8kiB, or (2) the file is closed because the streaming response is complete. Without flushing, the GZipMiddleware doesn't work as expected for streaming responses, especially not for Server-Sent Events which are expected to be delivered immediately to clients. The code as written appears to intend to flush immediately rather than buffering, as it does immediately call `await self.send(message)`, but in practice that `message` is often empty.
I think having both GZip + SSE should be supportable. I think the most correct behavior is to send each message as it becomes available, even if this is not the most optimal compression. But still, enabling compression for streaming responses (including SSE) can be a reasonable choice; those individual messages could theoretically be larger than 32k and may benefit from compression the same as non-streaming responses might. It seems the current implementation (without the flush) favors optimization of the compression at the expense of timely delivery. Enabling the flush allows the developer to accept the suboptimal compression and get the expected timely delivery, or to choose to trade off timely delivery for potential compression improvements by implementing their own buffering. |
Similar conversation happening at tuffnatty/zstd-asgi#5. I argue that the least-surprising behavior of the middleware is to deliver small messages immediately rather than holding them in a buffer for an indefinite amount of time. As a user of the middleware, I'd rather read "compression won't be as effective for small messages; buffer them into larger chunks if you wish to maximize compression" vs "small messages will be held in a buffer and delivered 32kB at a time; if you don't want this, don't use this middleware" |
Is there any web framework in any language that does this right in the middleware level? |
As I suggested in tuffnatty/zstd-asgi#5, why not allow both behaviours selecting the desired one with a parameter? |
Because that seems to be taken responsibility from our side and give to the developers. I'm still trying to understand the implications of flushing it on every message. Seems a lot. But for SSE, we need to send the ping messages, so there's no need for the user to configure it. |
I agree that adding a parameter should be avoided if possible; I'd rather the middleware figure out the correct behavior and implement it, transparently and consistently. Do we have any evidence the current implementation was even intentional? Read-after-write without flushing is a common, easy-to-make programming error. Calling |
Summary
In order to better support streaming responses where the chunks are smaller than the file buffer size, we flush after writing.
Without the explicit flush, the writes are buffered and the subsequent reads see an empty self.gzip_buffer until the file automatically flushes due to either (1) the 32KiB write buffer1 fills or (2) the file is closed because the streaming response is complete.
Without flushing, the GZipMiddleware doesn't work as expected for streaming responses, especially not for Server-Sent Events which are expected to be delivered immediately to clients. The code as written appears to intend to flush immediately rather than buffering, as it does immediately call
await self.send(message)
, but in practice thatmessage
is often empty.Checklist
Footnotes
https://github.com/python/cpython/blob/main/Lib/gzip.py#L26 ↩