/*
* Copyright (c) 2002-2017 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.neo4j.driver.internal.net;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.ReadableByteChannel;
import java.util.Arrays;
import org.neo4j.driver.v1.exceptions.ClientException;
import org.neo4j.driver.v1.exceptions.ServiceUnavailableException;
import org.neo4j.driver.v1.util.RecordingByteChannel;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class BufferingChunkedInputTest
{
@Rule
public ExpectedException exception = ExpectedException.none();
@Test
public void shouldReadOneByteInOneChunk() throws IOException
{
// Given
BufferingChunkedInput input = new BufferingChunkedInput( packet( 0, 2, 13, 37, 0, 0 ) );
// When
byte b1 = input.readByte();
byte b2 = input.readByte();
// Then
assertThat( b1, equalTo( (byte) 13 ) );
assertThat( b2, equalTo( (byte) 37 ) );
}
@Test
public void shouldReadOneByteInTwoChunks() throws IOException
{
// Given
BufferingChunkedInput input = new BufferingChunkedInput( packet( 0, 1, 13, 0, 1, 37, 0, 0 ) );
// When
byte b1 = input.readByte();
byte b2 = input.readByte();
// Then
assertThat( b1, equalTo( (byte) 13 ) );
assertThat( b2, equalTo( (byte) 37 ) );
}
@Test
public void shouldReadOneByteWhenSplitHeader() throws IOException
{
// Given
BufferingChunkedInput input =
new BufferingChunkedInput( packets( packet( 0 ), packet( 1, 13, 0, 1, 37, 0, 0 ) ) );
// When
byte b1 = input.readByte();
byte b2 = input.readByte();
// Then
assertThat( b1, equalTo( (byte) 13 ) );
assertThat( b2, equalTo( (byte) 37 ) );
}
@Test
public void shouldReadBytesAcrossHeaders() throws IOException
{
// Given
BufferingChunkedInput input =
new BufferingChunkedInput( packets( packet( 0, 2, 1, 2, 0, 6), packet(3, 4, 5, 6, 7, 8, 0, 0 ) ) );
// When
byte[] dst = new byte[8];
input.readBytes(dst, 0, 8);
// Then
assertThat( dst, equalTo( new byte[]{1, 2, 3, 4, 5, 6, 7, 8} ) );
}
@Test
public void shouldReadChunkWithSplitHeaderForBigMessages() throws IOException
{
// Given
int packetSize = 384;
BufferingChunkedInput input =
new BufferingChunkedInput( packets( packet( 1 ), packet( -128 ), fillPacket( packetSize, 1 ) ) );
// Then
assertThat( input.readByte(), equalTo( (byte) 1 ) );
assertThat( input.remainingChunkSize(), equalTo( packetSize - 1 ) );
for ( int i = 1; i < packetSize; i++ )
{
assertThat( input.readByte(), equalTo( (byte) 1 ) );
}
assertThat( input.remainingChunkSize(), equalTo( 0 ) );
}
@Test
public void shouldReadChunkWithSplitHeaderForBigMessagesWhenInternalBufferHasOneByte() throws IOException
{
// Given
int packetSize = 32780;
BufferingChunkedInput input =
new BufferingChunkedInput( packets( packet( -128 ), packet( 12 ), fillPacket( packetSize, 1 ) ), 1);
// Then
assertThat( input.readByte(), equalTo( (byte) 1 ) );
assertThat( input.remainingChunkSize(), equalTo( packetSize - 1 ) );
}
@Test
public void shouldReadUnsignedByteFromBuffer() throws IOException
{
ByteBuffer buffer = ByteBuffer.allocate( 1 );
buffer.put( (byte) -1 );
buffer.flip();
assertThat(BufferingChunkedInput.getUnsignedByteFromBuffer( buffer ), equalTo( 255 ));
}
@Test
public void shouldReadOneByteInOneChunkWhenBustingBuffer() throws IOException
{
// Given
BufferingChunkedInput input = new BufferingChunkedInput( packet( 0, 2, 13, 37, 0, 0 ), 2 );
// When
byte b1 = input.readByte();
byte b2 = input.readByte();
// Then
assertThat( b1, equalTo( (byte) 13 ) );
assertThat( b2, equalTo( (byte) 37 ) );
}
@Test
public void shouldExposeMultipleChunksAsCohesiveStream() throws Throwable
{
// Given
BufferingChunkedInput ch = new BufferingChunkedInput( packet( 0, 5, 1, 2, 3, 4, 5 ), 2 );
// When
byte[] bytes = new byte[5];
ch.readBytes( bytes, 0, 5 );
// Then
assertThat( bytes, equalTo( new byte[]{1, 2, 3, 4, 5} ) );
}
@Test
public void shouldReadIntoMisalignedDestinationBuffer() throws Throwable
{
// Given
BufferingChunkedInput ch = new BufferingChunkedInput( packet( 0, 7, 1, 2, 3, 4, 5, 6, 7 ), 2 );
byte[] bytes = new byte[3];
// When I read {1,2,3}
ch.readBytes( bytes, 0, 3 );
// Then
assertThat( bytes, equalTo( new byte[]{1, 2, 3} ) );
// When I read {4,5,6}
ch.readBytes( bytes, 0, 3 );
// Then
assertThat( bytes, equalTo( new byte[]{4, 5, 6} ) );
// When I read {7}
Arrays.fill( bytes, (byte) 0 );
ch.readBytes( bytes, 0, 1 );
// Then
assertThat( bytes, equalTo( new byte[]{7, 0, 0} ) );
}
@Test
public void canReadBytesAcrossChunkBoundaries() throws Exception
{
// Given
byte[] inputBuffer = {
0, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // chunk 1 with size 10
0, 5, 1, 2, 3, 4, 5 // chunk 2 with size 5
};
RecordingByteChannel ch = new RecordingByteChannel();
ch.write( ByteBuffer.wrap( inputBuffer ) );
BufferingChunkedInput input = new BufferingChunkedInput( ch );
byte[] outputBuffer = new byte[15];
// When
input.hasMoreData();
// Then
input.readBytes( outputBuffer, 0, 15 );
assertThat( outputBuffer, equalTo( new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5} ) );
}
@Test
public void canReadBytesAcrossChunkBoundariesWithMisalignedBuffer() throws Exception
{
// Given
byte[] inputBuffer = {
0, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // chunk 1 with size 10
0, 5, 1, 2, 3, 4, 5 // chunk 2 with size 5
};
RecordingByteChannel ch = new RecordingByteChannel();
ch.write( ByteBuffer.wrap( inputBuffer ) );
BufferingChunkedInput input = new BufferingChunkedInput( ch, 11 );
byte[] outputBuffer = new byte[15];
// When
input.hasMoreData();
// Then
input.readBytes( outputBuffer, 0, 15 );
assertThat( outputBuffer, equalTo( new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5} ) );
}
@Test
public void canReadAllNumberSizes() throws Exception
{
// Given
RecordingByteChannel ch = new RecordingByteChannel();
ChunkedOutput out = new ChunkedOutput( ch );
// these are written in one go on purpose, to check for buffer pointer errors where writes
// would interfere with one another, writing at the wrong offsets
out.writeByte( Byte.MAX_VALUE );
out.writeByte( (byte) 1 );
out.writeByte( Byte.MIN_VALUE );
out.writeLong( Long.MAX_VALUE );
out.writeLong( 0L );
out.writeLong( Long.MIN_VALUE );
out.writeShort( Short.MAX_VALUE );
out.writeShort( (short) 0 );
out.writeShort( Short.MIN_VALUE );
out.writeInt( Integer.MAX_VALUE );
out.writeInt( 0 );
out.writeInt( Integer.MIN_VALUE );
out.writeDouble( Double.MAX_VALUE );
out.writeDouble( 0d );
out.writeDouble( Double.MIN_VALUE );
out.flush();
BufferingChunkedInput in = new BufferingChunkedInput( ch );
// when / then
assertEquals( Byte.MAX_VALUE, in.readByte() );
assertEquals( (byte) 1, in.readByte() );
assertEquals( Byte.MIN_VALUE, in.readByte() );
assertEquals( Long.MAX_VALUE, in.readLong() );
assertEquals( 0L, in.readLong() );
assertEquals( Long.MIN_VALUE, in.readLong() );
assertEquals( Short.MAX_VALUE, in.readShort() );
assertEquals( (short) 0, in.readShort() );
assertEquals( Short.MIN_VALUE, in.readShort() );
assertEquals( Integer.MAX_VALUE, in.readInt() );
assertEquals( 0, in.readInt() );
assertEquals( Integer.MIN_VALUE, in.readInt() );
assertEquals( Double.MAX_VALUE, in.readDouble(), 0d );
assertEquals( 0D, in.readDouble(), 0d );
assertEquals( Double.MIN_VALUE, in.readDouble(), 0d );
}
@Test
public void shouldNotReadMessageEndingWhenByteLeftInBuffer() throws IOException
{
// Given
ReadableByteChannel channel = Channels.newChannel(
new ByteArrayInputStream( new byte[]{0, 5, 1, 2, 3, 4, 5, 0, 0} ) );
BufferingChunkedInput ch = new BufferingChunkedInput( channel, 2 );
byte[] bytes = new byte[4];
ch.readBytes( bytes, 0, 4 );
assertThat( bytes, equalTo( new byte[]{1, 2, 3, 4} ) );
// When
try
{
ch.messageBoundaryHook().run();
fail( "The expected ClientException is not thrown" );
}
catch ( ClientException e )
{
assertEquals( "org.neo4j.driver.v1.exceptions.ClientException: Trying to read message complete ending " +
"'00 00' while there are more data left in the message content unread: buffer [], " +
"unread chunk size 1", e.toString() );
}
}
@Test
public void shouldGiveHelpfulMessageOnInterrupt() throws IOException
{
// Given
ReadableByteChannel channel = mock( ReadableByteChannel.class );
when( channel.read( any( ByteBuffer.class ) ) ).thenThrow( new ClosedByInterruptException() );
BufferingChunkedInput ch = new BufferingChunkedInput( channel, 2 );
// Expect
exception.expectMessage(
"Connection to the database was lost because someone called `interrupt()` on the driver thread " +
"waiting for a reply. " +
"This normally happens because the JVM is shutting down, but it can also happen because your " +
"application code or some " +
"framework you are using is manually interrupting the thread." );
// When
ch.readByte();
}
@Test
public void shouldPeekOneByteInOneChunk() throws IOException
{
// Given
BufferingChunkedInput input = new BufferingChunkedInput( packet( 0, 2, 13, 37, 0, 0 ) );
// When
byte peeked1 = input.peekByte();
byte read1 = input.readByte();
byte peeked2 = input.peekByte();
byte read2 = input.readByte();
// Then
assertThat( peeked1, equalTo( (byte) 13 ) );
assertThat( read1, equalTo( (byte) 13 ) );
assertThat( peeked2, equalTo( (byte) 37 ) );
assertThat( read2, equalTo( (byte) 37 ) );
}
@Test
public void shouldPeekOneByteInTwoChunks() throws IOException
{
// Given
BufferingChunkedInput input = new BufferingChunkedInput( packet( 0, 1, 13, 0, 1, 37, 0, 0 ) );
// When
byte peeked1 = input.peekByte();
byte read1 = input.readByte();
byte peeked2 = input.peekByte();
byte read2 = input.readByte();
// Then
assertThat( peeked1, equalTo( (byte) 13 ) );
assertThat( read1, equalTo( (byte) 13 ) );
assertThat( peeked2, equalTo( (byte) 37 ) );
assertThat( read2, equalTo( (byte) 37 ) );
}
@Test
public void shouldPeekOneByteWhenSplitHeader() throws IOException
{
// Given
BufferingChunkedInput input =
new BufferingChunkedInput( packets( packet( 0 ), packet( 1, 13, 0, 1, 37, 0, 0 ) ) );
// When
byte peeked1 = input.peekByte();
byte read1 = input.readByte();
byte peeked2 = input.peekByte();
byte read2 = input.readByte();
// Then
assertThat( peeked1, equalTo( (byte) 13 ) );
assertThat( read1, equalTo( (byte) 13 ) );
assertThat( peeked2, equalTo( (byte) 37 ) );
assertThat( read2, equalTo( (byte) 37 ) );
}
@Test
public void shouldPeekOneByteInOneChunkWhenBustingBuffer() throws IOException
{
// Given
BufferingChunkedInput input = new BufferingChunkedInput( packet( 0, 2, 13, 37, 0, 0 ), 2 );
// When
byte peeked1 = input.peekByte();
byte read1 = input.readByte();
byte peeked2 = input.peekByte();
byte read2 = input.readByte();
// Then
assertThat( peeked1, equalTo( (byte) 13 ) );
assertThat( read1, equalTo( (byte) 13 ) );
assertThat( peeked2, equalTo( (byte) 37 ) );
assertThat( read2, equalTo( (byte) 37 ) );
}
@Test
public void shouldNotStackOverflowWhenDataIsNotAvailable() throws IOException
{
// Given a channel that does not get data from the channel
ReadableByteChannel channel = new ReadableByteChannel()
{
private int counter = 0;
private int numberOfTries = 10000;
@Override
public int read( ByteBuffer dst ) throws IOException
{
if ( counter++ < numberOfTries )
{
return 0;
}
else
{
dst.put( (byte) 11 );
return 1;
}
}
@Override
public boolean isOpen()
{
return true;
}
@Override
public void close() throws IOException
{
}
};
// When
BufferingChunkedInput input = new BufferingChunkedInput( channel );
// Then
assertThat( input.readByte(), equalTo( (byte) 11 ) );
}
@Test
public void shouldFailNicelyOnClosedConnections() throws IOException
{
// Given
ReadableByteChannel channel = mock( ReadableByteChannel.class );
when( channel.read( any( ByteBuffer.class ) ) ).thenReturn( -1 );
BufferingChunkedInput input = new BufferingChunkedInput( channel );
//Expect
exception.expect( ServiceUnavailableException.class );
exception.expectMessage( "Connection terminated while receiving data. This can happen due to network " +
"instabilities, or due to restarts of the database." );
// When
input.readByte();
}
@Test
public void shouldKeepBufferCorrectWhenError() throws Throwable
{
// Given
ReadableByteChannel channel = mock( ReadableByteChannel.class );
when( channel.read( any( ByteBuffer.class ) ) ).thenReturn( -1 );
ByteBuffer buffer = ByteBuffer.allocate( 8 );
buffer.limit(0);
//Expect
exception.expect( ServiceUnavailableException.class );
exception.expectMessage( "Connection terminated while receiving data. This can happen due to network " +
"instabilities, or due to restarts of the database." );
// When
BufferingChunkedInput.readNextPacket( channel, buffer );
assertEquals( buffer.position(), 0 );
assertEquals( buffer.limit(), 0 );
assertEquals( buffer.capacity(), 8 );
assertFalse( channel.isOpen() );
}
private ReadableByteChannel fillPacket( int size, int value )
{
int[] ints = new int[size];
for ( int i = 0; i < size; i++ )
{
ints[i] = value;
}
return packet( ints );
}
private ReadableByteChannel packet( int... bytes )
{
byte[] byteArray = new byte[bytes.length];
for ( int i = 0; i < bytes.length; i++ )
{
byteArray[i] = (byte) bytes[i];
}
return Channels.newChannel(
new ByteArrayInputStream( byteArray ) );
}
private ReadableByteChannel packets( final ReadableByteChannel... channels )
{
return new ReadableByteChannel()
{
private int index = 0;
@Override
public int read( ByteBuffer dst ) throws IOException
{
return channels[index++].read( dst );
}
@Override
public boolean isOpen()
{
return false;
}
@Override
public void close() throws IOException
{
}
};
}
}