Repository navigation
KAFKA-21280: Do not send stale bytes after a failed file read in SslTransportLayer.transferFrom #23832
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
Merged
KAFKA-21280: Do not send stale bytes after a failed file read in SslTransportLayer.transferFrom #23832
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,6 +51,7 @@ | |
| import java.net.Socket; | ||
| import java.nio.ByteBuffer; | ||
| import java.nio.channels.Channels; | ||
| import java.nio.channels.FileChannel; | ||
| import java.nio.channels.SelectionKey; | ||
| import java.nio.channels.SocketChannel; | ||
| import java.nio.charset.StandardCharsets; | ||
|
|
@@ -61,8 +62,11 @@ | |
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.concurrent.Callable; | ||
| import java.util.concurrent.atomic.AtomicBoolean; | ||
| import java.util.concurrent.atomic.AtomicInteger; | ||
| import java.util.concurrent.atomic.AtomicLong; | ||
| import java.util.concurrent.atomic.AtomicReference; | ||
| import java.util.function.Function; | ||
| import java.util.stream.Stream; | ||
|
|
||
|
|
@@ -81,6 +85,7 @@ | |
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
| import static org.junit.jupiter.api.Assumptions.assumeTrue; | ||
| import static org.mockito.ArgumentMatchers.any; | ||
| import static org.mockito.ArgumentMatchers.anyLong; | ||
| import static org.mockito.Mockito.doReturn; | ||
| import static org.mockito.Mockito.doThrow; | ||
| import static org.mockito.Mockito.mock; | ||
|
|
@@ -96,6 +101,8 @@ | |
| public class SslTransportLayerTest { | ||
|
|
||
| private static final int BUFFER_SIZE = 4 * 1024; | ||
| // The size of SslTransportLayer's file channel buffer | ||
| private static final int TRANSFER_SIZE = 32 * 1024; | ||
| private static final Time TIME = Time.SYSTEM; | ||
|
|
||
| private static class Args { | ||
|
|
@@ -1283,6 +1290,108 @@ public void testInvalidSslEngineFactory(Args args) { | |
| assertThrows(KafkaException.class, () -> createSelector(args.sslClientConfigs)); | ||
| } | ||
|
|
||
| /** | ||
| * A failed read of the file being transferred is rethrown, even after part of the transfer was sent, and must not | ||
| * leave the previous read in the buffer to be sent as the contents of the failed position. | ||
| */ | ||
| @ParameterizedTest | ||
| @ArgumentsSource(SslTransportLayerArgumentsProvider.class) | ||
| public void testTransferFromDoesNotSendStaleBytesAfterFailedRead(Args args) throws Exception { | ||
| ByteArrayOutputStream sent = new ByteArrayOutputStream(); | ||
| SslTransportLayer layer = connectRecordingWrites(args, sent); | ||
| FileChannel fileChannel = fileChannelReadableUpTo(TRANSFER_SIZE, () -> { | ||
| throw new IOException("Input/output error"); | ||
| }); | ||
|
|
||
| // The readable part is sent, then the read of the next part fails | ||
| assertThrows(IOException.class, () -> layer.transferFrom(fileChannel, 0, 3 * TRANSFER_SIZE)); | ||
| assertArrayEquals(fileContents(TRANSFER_SIZE), sent.toByteArray()); | ||
| // A transfer from the unreadable position must fail as well instead of sending what was read before | ||
| assertThrows(IOException.class, () -> layer.transferFrom(fileChannel, TRANSFER_SIZE, 2 * TRANSFER_SIZE)); | ||
| assertArrayEquals(fileContents(TRANSFER_SIZE), sent.toByteArray()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we add this check after the first transferFrom() too? Ditto in the test below.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done, in both tests. |
||
| } | ||
|
|
||
| /** | ||
| * Nor must the previous read be sent when the file ends early, for instance because it was truncated during the | ||
| * transfer: that throws nothing. | ||
| */ | ||
| @ParameterizedTest | ||
| @ArgumentsSource(SslTransportLayerArgumentsProvider.class) | ||
| public void testTransferFromDoesNotSendStaleBytesAtEndOfFile(Args args) throws Exception { | ||
| ByteArrayOutputStream sent = new ByteArrayOutputStream(); | ||
| SslTransportLayer layer = connectRecordingWrites(args, sent); | ||
| FileChannel fileChannel = fileChannelReadableUpTo(TRANSFER_SIZE, () -> -1); | ||
|
|
||
| assertEquals(TRANSFER_SIZE, layer.transferFrom(fileChannel, 0, 3 * TRANSFER_SIZE)); | ||
| assertArrayEquals(fileContents(TRANSFER_SIZE), sent.toByteArray()); | ||
| assertEquals(0, layer.transferFrom(fileChannel, TRANSFER_SIZE, 2 * TRANSFER_SIZE)); | ||
| assertArrayEquals(fileContents(TRANSFER_SIZE), sent.toByteArray()); | ||
| } | ||
|
|
||
| /** | ||
| * Connects to an echo server and returns the client's transport layer, which from then on records what is | ||
| * written to it instead of sending it. Data is exchanged first, as with TLSv1.3 the transport layer only becomes | ||
| * ready for transferFrom once it has read some. | ||
| */ | ||
| private SslTransportLayer connectRecordingWrites(Args args, ByteArrayOutputStream sent) throws Exception { | ||
| String node = "0"; | ||
| server = createEchoServer(args, SecurityProtocol.SSL); | ||
| AtomicBoolean record = new AtomicBoolean(false); | ||
| AtomicReference<SslTransportLayer> transportLayer = new AtomicReference<>(); | ||
| TestSslChannelBuilder channelBuilder = new TestSslChannelBuilder(ConnectionMode.CLIENT) { | ||
| @Override | ||
| protected TestSslTransportLayer newTransportLayer(String id, SelectionKey key, SSLEngine sslEngine) { | ||
| TestSslTransportLayer layer = new TestSslTransportLayer(id, key, sslEngine) { | ||
| @Override | ||
| public int write(ByteBuffer src) throws IOException { | ||
| if (!record.get()) | ||
| return super.write(src); | ||
| int length = src.remaining(); | ||
| byte[] bytes = new byte[length]; | ||
| src.get(bytes); | ||
| sent.write(bytes); | ||
| return length; | ||
| } | ||
| }; | ||
| transportLayer.set(layer); | ||
| return layer; | ||
| } | ||
| }; | ||
| channelBuilder.configure(args.sslClientConfigs); | ||
| selector = new Selector(5000, new Metrics(), TIME, "MetricGroup", channelBuilder, new LogContext()); | ||
| selector.connect(node, new InetSocketAddress("localhost", server.port()), BUFFER_SIZE, BUFFER_SIZE); | ||
| NetworkTestUtils.checkClientConnection(selector, node, 100, 10); | ||
| record.set(true); | ||
| return transportLayer.get(); | ||
| } | ||
|
|
||
| /** | ||
| * A file channel of three times readableSize bytes whose content is fileContents up to readableSize, and whose | ||
| * reads from there on are answered by beyondReadable. | ||
| */ | ||
| private static FileChannel fileChannelReadableUpTo(int readableSize, Callable<Integer> beyondReadable) throws IOException { | ||
| FileChannel fileChannel = mock(FileChannel.class); | ||
| when(fileChannel.size()).thenReturn(3L * readableSize); | ||
| when(fileChannel.read(any(ByteBuffer.class), anyLong())).thenAnswer(invocation -> { | ||
| ByteBuffer buffer = invocation.getArgument(0); | ||
| long position = invocation.getArgument(1); | ||
| if (position >= readableSize) | ||
| return beyondReadable.call(); | ||
| int length = (int) Math.min(buffer.remaining(), readableSize - position); | ||
| for (int i = 0; i < length; i++) | ||
| buffer.put((byte) (position + i)); | ||
| return length; | ||
| }); | ||
| return fileChannel; | ||
| } | ||
|
|
||
| private static byte[] fileContents(int size) { | ||
| byte[] contents = new byte[size]; | ||
| for (int i = 0; i < size; i++) | ||
| contents[i] = (byte) i; | ||
| return contents; | ||
| } | ||
|
|
||
| private void verifyInvalidReconfigure(ListenerReconfigurable reconfigurable, | ||
| Map<String, Object> invalidConfigs) { | ||
| assertThrows(KafkaException.class, () -> reconfigurable.validateReconfiguration(invalidConfigs)); | ||
|
|
||
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.
Could we add a javadoc that transferFrom() throws IOException, after which, the channel shouldn't be used?
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.
Done.