-
Notifications
You must be signed in to change notification settings - Fork 227
Optimize how SSE data is written #826
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
Merged
TBlueF
merged 4 commits into
BlueMap-Minecraft:master
from
pR0Ps:refactor/sseconnection-write
Aug 5, 2026
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
cf84824
Optimize how SSE data is written
pR0Ps 73df152
Encapsulate body+streamwriter differences within HttpResponse
pR0Ps c2bfb54
Always convert HttpResponse.body to a HttpResponseStreamWriter
pR0Ps 2cd1806
Add a soft size limit to how much data ChunkedOutputStream will buffer
pR0Ps File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
136 changes: 136 additions & 0 deletions
136
common/src/main/java/de/bluecolored/bluemap/common/web/http/ChunkedOutputStream.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| /* | ||
| * This file is part of BlueMap, licensed under the MIT License (MIT). | ||
| * | ||
| * Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de> | ||
| * Copyright (c) contributors | ||
| * | ||
| * Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| * of this software and associated documentation files (the "Software"), to deal | ||
| * in the Software without restriction, including without limitation the rights | ||
| * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| * copies of the Software, and to permit persons to whom the Software is | ||
| * furnished to do so, subject to the following conditions: | ||
| * | ||
| * The above copyright notice and this permission notice shall be included in | ||
| * all copies or substantial portions of the Software. | ||
| * | ||
| * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
| * THE SOFTWARE. | ||
| */ | ||
| package de.bluecolored.bluemap.common.web.http; | ||
|
|
||
| import java.io.ByteArrayOutputStream; | ||
| import java.io.IOException; | ||
| import java.io.OutputStream; | ||
| import java.nio.charset.StandardCharsets; | ||
|
|
||
| /** | ||
| * Wraps an {@link OutputStream}, buffering writes and framing them as HTTP/1.1 chunks. | ||
| * <p> | ||
| * {@link #endChunk()} ends the current chunk without flushing the wrapped stream. | ||
| * {@link #flush()} ends the current chunk *and* flushes the wrapped stream. | ||
| * <p> | ||
| * Closing this stream ends the current chunk and writes the terminating zero-length chunk, but | ||
| * doesn't close the wrapped stream since it's expected to outlive an individual chunked response. | ||
| * <p> | ||
| * Any write made after this stream has been closed throws an {@link IOException} to avoid bytes | ||
| * being written to the wrapped stream outside the required chunk framing. | ||
| */ | ||
| public class ChunkedOutputStream extends OutputStream { | ||
|
|
||
| private static final int AUTO_FLUSH_CHUNK_SIZE = 1024; | ||
| private static final byte[] CRLF = "\r\n".getBytes(StandardCharsets.UTF_8); | ||
|
|
||
| private final OutputStream out; | ||
| private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); | ||
| private boolean closed = false; | ||
|
|
||
| public ChunkedOutputStream(OutputStream out) { | ||
| this.out = out; | ||
| } | ||
|
|
||
| @Override | ||
| public void write(int b) throws IOException { | ||
| ensureOpen(); | ||
| buffer.write(b); | ||
| if (buffer.size() >= AUTO_FLUSH_CHUNK_SIZE) endChunk(); | ||
| } | ||
|
|
||
| @Override | ||
| public void write(byte[] b, int off, int len) throws IOException { | ||
| ensureOpen(); | ||
| buffer.write(b, off, len); | ||
| if (buffer.size() >= AUTO_FLUSH_CHUNK_SIZE) endChunk(); | ||
| } | ||
|
|
||
| /** | ||
| * Writes out any currently buffered bytes as one HTTP chunk. | ||
| */ | ||
| public void endChunk() throws IOException { | ||
| ensureOpen(); | ||
| if (buffer.size() > 0) { | ||
| writeChunkHeader(buffer.size()); | ||
| buffer.writeTo(out); | ||
| out.write(CRLF); | ||
| buffer.reset(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Writes {@code len} bytes from {@code b} starting at {@code off} as single chunk. | ||
| * This avoids the buffering overhead incurred by using {@code write(...)}. | ||
| * <p> | ||
| * Any currently buffered bytes are written with {@link #endChunk()} first. | ||
| */ | ||
| public void writeChunk(byte[] b, int off, int len) throws IOException { | ||
| endChunk(); | ||
| if (len > 0) { | ||
| writeChunkHeader(len); | ||
| out.write(b, off, len); | ||
| out.write(CRLF); | ||
| } | ||
| } | ||
|
|
||
| private void writeChunkHeader(int len) throws IOException { | ||
| out.write(Integer.toHexString(len).getBytes(StandardCharsets.UTF_8)); | ||
| out.write(CRLF); | ||
| } | ||
|
|
||
| /** | ||
| * Ends the current chunk and flushes the wrapped stream to push all the buffered | ||
| * data to the client. | ||
| */ | ||
| @Override | ||
| public void flush() throws IOException { | ||
| endChunk(); | ||
| out.flush(); | ||
| } | ||
|
|
||
| /** | ||
| * Ends the current chunk, writes the terminating zero-length chunk, and flushes the | ||
| * wrapped stream (without closing it). | ||
| */ | ||
| @Override | ||
| public void close() throws IOException { | ||
| if (closed) return; | ||
| endChunk(); | ||
| closed = true; | ||
| out.write('0'); | ||
| out.write(CRLF); | ||
| out.write(CRLF); | ||
| out.flush(); | ||
| } | ||
|
|
||
| /** | ||
| * @throws IOException if this stream has already been closed. | ||
| */ | ||
| private void ensureOpen() throws IOException { | ||
| if (closed) throw new IOException("stream closed"); | ||
| } | ||
|
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the buffer have some threshold or max size that will flush it or end the chunk automatically if exceeded?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't add one because the
HttpResponseStreamWriterandSseConnectioneither write an entire chunk (doesn't use the buffer at all) or explicitly flush the stream after every event is written so the buffer will never grow out of control.With that being said, I do see how this could be an issue in the future if something else starts using this and doesn't make sure to periodically flush it.
I have 2 ideas for how I could solve this:
writemethods if the internal buffer gets larger than that.HttpResponseStreamWriteralways writes an entire chunk and theSseConnectioncould be pretty trivially refactored to internally buffer its events, I could remove the internal buffer andwritemethods from theChunkedOutputStreamentirely and enforce that an entire chunk must be written at once.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's true that it is not an issue with how the class is being used right now, but I like having classes like this as individually "safe" as possible. It makes potential future changes safer and there is also potential use by addons :D
I think i personally would go with 1, but i am also totally fine with 2 👍
So i'll leave that up to you :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've pushed a commit that does 1