Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/Npgsql/Internal/NpgsqlConnector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,10 @@ internal async Task Open(NpgsqlTimeout timeout, bool async, CancellationToken ca
{
await Authenticate(username, timeout, async, cancellationToken);

#if DEBUG
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than wrapping each call with #if DEBUG, the methods themselves can have [Conditional("DEBUG")]. This automatically does what we want, and makes the call sites much nicer.

ReadBuffer.EnableFrameTracking = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any particular reason this shouldn't always be on (in DEBUG)? We could also use it to catch bugs in the Startup message and in authentication...

#endif

// We treat BackendKeyData as optional because some PostgreSQL-like database
// don't send it (CockroachDB, CrateDB)
var msg = await ReadMessage(async);
Expand Down Expand Up @@ -1178,6 +1182,10 @@ internal ValueTask<IBackendMessage> ReadMessage(bool async, DataRowLoadingMode d
return ReadMessageLong(this, async, dataRowLoadingMode, readingNotifications: readingNotifications);
}

#if DEBUG
ReadBuffer.NextFrame();
#endif

var messageCode = (BackendMessageCode)ReadBuffer.ReadByte();
switch (messageCode)
{
Expand All @@ -1186,6 +1194,9 @@ internal ValueTask<IBackendMessage> ReadMessage(bool async, DataRowLoadingMode d
case BackendMessageCode.ParameterStatus:
case BackendMessageCode.ErrorResponse:
ReadBuffer.ReadPosition--;
#if DEBUG
ReadBuffer.ResetFrame();
#endif
return ReadMessageLong(this, async, dataRowLoadingMode, readingNotifications: false);
case BackendMessageCode.ReadyForQuery:
break;
Expand All @@ -1196,6 +1207,9 @@ internal ValueTask<IBackendMessage> ReadMessage(bool async, DataRowLoadingMode d
if (len > ReadBuffer.ReadBytesLeft)
{
ReadBuffer.ReadPosition -= 5;
#if DEBUG
ReadBuffer.ResetFrame();
#endif
return ReadMessageLong(this, async, dataRowLoadingMode, readingNotifications: false);
}

Expand Down Expand Up @@ -1233,6 +1247,9 @@ internal ValueTask<IBackendMessage> ReadMessage(bool async, DataRowLoadingMode d
while (true)
{
await connector.ReadBuffer.Ensure(5, async, readingNotifications);
#if DEBUG
connector.ReadBuffer.NextFrame();
#endif
var messageCode = (BackendMessageCode)connector.ReadBuffer.ReadByte();
PGUtil.ValidateBackendMessageCode(messageCode);
var len = connector.ReadBuffer.ReadInt32() - 4; // Transmitted length includes itself
Expand All @@ -1244,6 +1261,9 @@ internal ValueTask<IBackendMessage> ReadMessage(bool async, DataRowLoadingMode d
if (dataRowLoadingMode == DataRowLoadingMode.Skip)
{
await connector.ReadBuffer.Skip(len, async);
#if DEBUG
connector.ReadBuffer.ResetFrame();
#endif
continue;
}
}
Expand Down Expand Up @@ -1289,6 +1309,10 @@ internal ValueTask<IBackendMessage> ReadMessage(bool async, DataRowLoadingMode d
throw connector.Break(error);
}

#if DEBUG
connector.ReadBuffer.ResetFrame();
#endif

continue;

case BackendMessageCode.ReadyForQuery:
Expand All @@ -1307,7 +1331,12 @@ internal ValueTask<IBackendMessage> ReadMessage(bool async, DataRowLoadingMode d
case BackendMessageCode.ParameterStatus:
Debug.Assert(msg == null);
if (!readingNotifications)
{
#if DEBUG
connector.ReadBuffer.ResetFrame();
#endif
continue;
}
return null;
}

Expand Down
103 changes: 103 additions & 0 deletions src/Npgsql/Internal/NpgsqlReadBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,87 @@ internal TimeSpan Timeout
/// </summary>
internal Encoding RelaxedTextEncoding { get; }

#if DEBUG
internal bool EnableFrameTracking { get; set; }

internal void NextFrame()
{
Debug.Assert(_currentFrame.LengthLeft == 0, "Protocol failure");
_inFrame = false;
}

internal void ResetFrame()
{
_inFrame = true;
if (_previousFrame.HasValue)
_currentFrame = _previousFrame.Value;
}

bool _inFrame;

bool _bufferReset;

struct Frame
{
public Frame(byte code, int length, int lengthLeft)
{
Code = code;
Length = length;
LengthLeft = lengthLeft;
}

public readonly byte Code;
public readonly int Length;
public int LengthLeft;
}

Frame _currentFrame;
Frame? _previousFrame;

int _readPosition;

internal int ReadPosition
{
get => _readPosition;
set
{
if (EnableFrameTracking)
{
var positionDelta = value - _readPosition;

// Setting the same position as the one we already have
if (positionDelta == 0)
return;

// Buffer reset
if (!_bufferReset)
{
if (!_inFrame)
{
Debug.Assert(positionDelta == 1, "Protocol failure");
Debug.Assert(ReadBytesLeft >= 5);
var frameCode = Buffer[_readPosition];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convert to BackendMessageCode?

var frameLength = BinaryPrimitives.ReverseEndianness(Unsafe.ReadUnaligned<int>(ref Buffer[_readPosition + 1])) + 1;
var frameLengthLeft = frameLength - 1;
_previousFrame = _currentFrame;
_currentFrame = new Frame(frameCode, frameLength, frameLengthLeft);
_inFrame = true;
}
else
{
_currentFrame.LengthLeft -= positionDelta;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe better to have Position (in the frame) - just like the the buffer's ReadPosition - which gets incremented, rather than LengthLeft which gets decremented.

Debug.Assert(_currentFrame.LengthLeft <= _currentFrame.Length && _currentFrame.LengthLeft >= 0, "Going outside of frame bounds");
}
}
}

_readPosition = value;
}
}
#else
internal int ReadPosition { get; set; }
#endif

internal int ReadBytesLeft => FilledBytes - ReadPosition;

internal readonly byte[] Buffer;
Expand Down Expand Up @@ -159,9 +239,17 @@ static async Task EnsureLong(
}
else if (count > buffer.Size - buffer.FilledBytes)
{
#if DEBUG
buffer._bufferReset = true;
#endif

Array.Copy(buffer.Buffer, buffer.ReadPosition, buffer.Buffer, 0, buffer.ReadBytesLeft);
buffer.FilledBytes = buffer.ReadBytesLeft;
buffer.ReadPosition = 0;

#if DEBUG
buffer._bufferReset = false;
#endif
}

var finalCt = async && buffer.Timeout != TimeSpan.Zero
Expand Down Expand Up @@ -277,6 +365,15 @@ internal NpgsqlReadBuffer AllocateOversize(int count)
tempBuf.Timeout = Timeout;
CopyTo(tempBuf);
Clear();

#if DEBUG
tempBuf._currentFrame = _currentFrame;
tempBuf._previousFrame = _previousFrame;
tempBuf._inFrame = _inFrame;
tempBuf.EnableFrameTracking = EnableFrameTracking;
ResetFrame();
#endif

return tempBuf;
}

Expand Down Expand Up @@ -637,8 +734,14 @@ public void Dispose()

internal void Clear()
{
#if DEBUG
_bufferReset = true;
#endif
ReadPosition = 0;
FilledBytes = 0;
#if DEBUG
_bufferReset = false;
#endif
}

internal void CopyTo(NpgsqlReadBuffer other)
Expand Down
8 changes: 8 additions & 0 deletions src/Npgsql/NpgsqlDataReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,19 @@ public override Task<bool> ReadAsync(CancellationToken cancellationToken)
var readBuf = Connector.ReadBuffer;
if (readBuf.ReadBytesLeft < 5)
return null;

#if DEBUG
readBuf.NextFrame();
#endif

var messageCode = (BackendMessageCode)readBuf.ReadByte();
var len = readBuf.ReadInt32() - 4; // Transmitted length includes itself
if (messageCode != BackendMessageCode.DataRow || readBuf.ReadBytesLeft < len)
{
readBuf.ReadPosition -= 5;
#if DEBUG
readBuf.ResetFrame();
#endif
return null;
}

Expand Down
3 changes: 2 additions & 1 deletion src/Npgsql/NpgsqlNestedDataReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -381,13 +381,14 @@ public override bool Read()
{
CheckResultSet();

Buffer.ReadPosition = _nextRowBufferPos;
if (_nextRowIndex == _numRows)
{
_readerState = ReaderState.AfterRows;
return false;
}

Buffer.ReadPosition = _nextRowBufferPos;

if (_nextRowIndex++ != 0)
Buffer.ReadInt32(); // Length of record

Expand Down
8 changes: 4 additions & 4 deletions test/Npgsql.Tests/Support/PgServerMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ internal PgServerMock WriteCopyInResponse()
{
CheckDisposed();
_writeBuffer.WriteByte((byte)BackendMessageCode.CopyInResponse);
_writeBuffer.WriteInt32(5);
_writeBuffer.WriteInt32(9);
_writeBuffer.WriteByte(0);
_writeBuffer.WriteInt16(1);
_writeBuffer.WriteInt16(0);
Expand All @@ -341,9 +341,9 @@ internal PgServerMock WriteErrorResponse(string code, string severity, string me
_writeBuffer.WriteByte((byte)BackendMessageCode.ErrorResponse);
_writeBuffer.WriteInt32(
4 +
1 + Encoding.GetByteCount(code) +
1 + Encoding.GetByteCount(severity) +
1 + Encoding.GetByteCount(message) +
1 + Encoding.GetByteCount(code) + 1 +
1 + Encoding.GetByteCount(severity) + 1 +
1 + Encoding.GetByteCount(message) + 1 +
1);
_writeBuffer.WriteByte((byte)ErrorOrNoticeMessage.ErrorFieldTypeCode.Code);
_writeBuffer.WriteNullTerminatedString(code);
Expand Down