593 lines
20 KiB
C#
593 lines
20 KiB
C#
using System;
|
|
using System.Buffers;
|
|
using System.Buffers.Binary;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net.Sockets;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Npgsql.Util;
|
|
using static System.Threading.Timeout;
|
|
|
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
|
namespace Npgsql.Internal;
|
|
|
|
/// <summary>
|
|
/// A buffer used by Npgsql to write data to the socket efficiently.
|
|
/// Provides methods which encode different values types and tracks the current position.
|
|
/// </summary>
|
|
public sealed partial class NpgsqlWriteBuffer : IDisposable
|
|
{
|
|
#region Fields and Properties
|
|
|
|
internal readonly NpgsqlConnector Connector;
|
|
|
|
internal Stream Underlying { private get; set; }
|
|
|
|
readonly Socket? _underlyingSocket;
|
|
|
|
readonly ResettableCancellationTokenSource _timeoutCts;
|
|
|
|
/// <summary>
|
|
/// Timeout for sync and async writes
|
|
/// </summary>
|
|
internal TimeSpan Timeout
|
|
{
|
|
get => _timeoutCts.Timeout;
|
|
set
|
|
{
|
|
if (_timeoutCts.Timeout != value)
|
|
{
|
|
Debug.Assert(_underlyingSocket != null);
|
|
|
|
if (value > TimeSpan.Zero)
|
|
{
|
|
_underlyingSocket.SendTimeout = (int)value.TotalMilliseconds;
|
|
_timeoutCts.Timeout = value;
|
|
}
|
|
else
|
|
{
|
|
_underlyingSocket.SendTimeout = -1;
|
|
_timeoutCts.Timeout = InfiniteTimeSpan;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The total byte length of the buffer.
|
|
/// </summary>
|
|
internal int Size { get; private set; }
|
|
|
|
bool _copyMode;
|
|
internal Encoding TextEncoding { get; }
|
|
|
|
public int WriteSpaceLeft => Size - WritePosition;
|
|
|
|
internal readonly byte[] Buffer;
|
|
readonly Encoder _textEncoder;
|
|
|
|
internal int WritePosition;
|
|
|
|
ParameterStream? _parameterStream;
|
|
|
|
bool _disposed;
|
|
|
|
/// <summary>
|
|
/// The minimum buffer size possible.
|
|
/// </summary>
|
|
internal const int MinimumSize = 4096;
|
|
internal const int DefaultSize = 8192;
|
|
|
|
#endregion
|
|
|
|
#region Constructors
|
|
|
|
internal NpgsqlWriteBuffer(
|
|
NpgsqlConnector connector,
|
|
Stream stream,
|
|
Socket? socket,
|
|
int size,
|
|
Encoding textEncoding)
|
|
{
|
|
if (size < MinimumSize)
|
|
throw new ArgumentOutOfRangeException(nameof(size), size, "Buffer size must be at least " + MinimumSize);
|
|
|
|
Connector = connector;
|
|
Underlying = stream;
|
|
_underlyingSocket = socket;
|
|
_timeoutCts = new ResettableCancellationTokenSource();
|
|
Buffer = new byte[size];
|
|
Size = size;
|
|
|
|
TextEncoding = textEncoding;
|
|
_textEncoder = TextEncoding.GetEncoder();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region I/O
|
|
|
|
public async Task Flush(bool async, CancellationToken cancellationToken = default)
|
|
{
|
|
if (_copyMode)
|
|
{
|
|
// In copy mode, we write CopyData messages. The message code has already been
|
|
// written to the beginning of the buffer, but we need to go back and write the
|
|
// length.
|
|
if (WritePosition == 1)
|
|
return;
|
|
var pos = WritePosition;
|
|
WritePosition = 1;
|
|
WriteInt32(pos - 1);
|
|
WritePosition = pos;
|
|
} else if (WritePosition == 0)
|
|
return;
|
|
|
|
var finalCt = cancellationToken;
|
|
if (async && Timeout > TimeSpan.Zero)
|
|
finalCt = _timeoutCts.Start(cancellationToken);
|
|
|
|
try
|
|
{
|
|
if (async)
|
|
{
|
|
await Underlying.WriteAsync(Buffer, 0, WritePosition, finalCt);
|
|
await Underlying.FlushAsync(finalCt);
|
|
_timeoutCts.Stop();
|
|
}
|
|
else
|
|
{
|
|
Underlying.Write(Buffer, 0, WritePosition);
|
|
Underlying.Flush();
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
// Stopping twice (in case the previous Stop() call succeeded) doesn't hurt.
|
|
// Not stopping will cause an assertion failure in debug mode when we call Start() the next time.
|
|
// We can't stop in a finally block because Connector.Break() will dispose the buffer and the contained
|
|
// _timeoutCts
|
|
_timeoutCts.Stop();
|
|
switch (e)
|
|
{
|
|
// User requested the cancellation
|
|
case OperationCanceledException _ when (cancellationToken.IsCancellationRequested):
|
|
throw Connector.Break(e);
|
|
// Read timeout
|
|
case OperationCanceledException _:
|
|
// Note that mono throws SocketException with the wrong error (see #1330)
|
|
case IOException _ when (e.InnerException as SocketException)?.SocketErrorCode ==
|
|
(Type.GetType("Mono.Runtime") == null ? SocketError.TimedOut : SocketError.WouldBlock):
|
|
Debug.Assert(e is OperationCanceledException ? async : !async);
|
|
throw Connector.Break(new NpgsqlException("Exception while writing to stream", new TimeoutException("Timeout during writing attempt")));
|
|
}
|
|
|
|
throw Connector.Break(new NpgsqlException("Exception while writing to stream", e));
|
|
}
|
|
NpgsqlEventSource.Log.BytesWritten(WritePosition);
|
|
//NpgsqlEventSource.Log.RequestFailed();
|
|
|
|
WritePosition = 0;
|
|
if (_copyMode)
|
|
WriteCopyDataHeader();
|
|
}
|
|
|
|
internal void Flush() => Flush(false).GetAwaiter().GetResult();
|
|
|
|
#endregion
|
|
|
|
#region Direct write
|
|
|
|
internal void DirectWrite(ReadOnlySpan<byte> buffer)
|
|
{
|
|
Flush();
|
|
|
|
if (_copyMode)
|
|
{
|
|
// Flush has already written the CopyData header for us, but write the CopyData
|
|
// header to the socket with the write length before we can start writing the data directly.
|
|
Debug.Assert(WritePosition == 5);
|
|
|
|
WritePosition = 1;
|
|
WriteInt32(buffer.Length + 4);
|
|
WritePosition = 5;
|
|
_copyMode = false;
|
|
Flush();
|
|
_copyMode = true;
|
|
WriteCopyDataHeader(); // And ready the buffer after the direct write completes
|
|
}
|
|
else
|
|
Debug.Assert(WritePosition == 0);
|
|
|
|
try
|
|
{
|
|
Underlying.Write(buffer);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
throw Connector.Break(new NpgsqlException("Exception while writing to stream", e));
|
|
}
|
|
}
|
|
|
|
internal async Task DirectWrite(ReadOnlyMemory<byte> memory, bool async, CancellationToken cancellationToken = default)
|
|
{
|
|
await Flush(async, cancellationToken);
|
|
|
|
if (_copyMode)
|
|
{
|
|
// Flush has already written the CopyData header for us, but write the CopyData
|
|
// header to the socket with the write length before we can start writing the data directly.
|
|
Debug.Assert(WritePosition == 5);
|
|
|
|
WritePosition = 1;
|
|
WriteInt32(memory.Length + 4);
|
|
WritePosition = 5;
|
|
_copyMode = false;
|
|
await Flush(async, cancellationToken);
|
|
_copyMode = true;
|
|
WriteCopyDataHeader(); // And ready the buffer after the direct write completes
|
|
}
|
|
else
|
|
Debug.Assert(WritePosition == 0);
|
|
|
|
try
|
|
{
|
|
if (async)
|
|
await Underlying.WriteAsync(memory, cancellationToken);
|
|
else
|
|
Underlying.Write(memory.Span);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
throw Connector.Break(new NpgsqlException("Exception while writing to stream", e));
|
|
}
|
|
}
|
|
|
|
#endregion Direct write
|
|
|
|
#region Write Simple
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteSByte(sbyte value) => Write(value);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteByte(byte value) => Write(value);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
internal void WriteInt16(int value)
|
|
=> WriteInt16((short)value, false);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteInt16(short value)
|
|
=> WriteInt16(value, false);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteInt16(short value, bool littleEndian)
|
|
=> Write(littleEndian == BitConverter.IsLittleEndian ? value : BinaryPrimitives.ReverseEndianness(value));
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteUInt16(ushort value)
|
|
=> WriteUInt16(value, false);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteUInt16(ushort value, bool littleEndian)
|
|
=> Write(littleEndian == BitConverter.IsLittleEndian ? value : BinaryPrimitives.ReverseEndianness(value));
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteInt32(int value)
|
|
=> WriteInt32(value, false);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteInt32(int value, bool littleEndian)
|
|
=> Write(littleEndian == BitConverter.IsLittleEndian ? value : BinaryPrimitives.ReverseEndianness(value));
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteUInt32(uint value)
|
|
=> WriteUInt32(value, false);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteUInt32(uint value, bool littleEndian)
|
|
=> Write(littleEndian == BitConverter.IsLittleEndian ? value : BinaryPrimitives.ReverseEndianness(value));
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteInt64(long value)
|
|
=> WriteInt64(value, false);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteInt64(long value, bool littleEndian)
|
|
=> Write(littleEndian == BitConverter.IsLittleEndian ? value : BinaryPrimitives.ReverseEndianness(value));
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteUInt64(ulong value)
|
|
=> WriteUInt64(value, false);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteUInt64(ulong value, bool littleEndian)
|
|
=> Write(littleEndian == BitConverter.IsLittleEndian ? value : BinaryPrimitives.ReverseEndianness(value));
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteSingle(float value)
|
|
=> WriteSingle(value, false);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteSingle(float value, bool littleEndian)
|
|
=> WriteInt32(Unsafe.As<float, int>(ref value), littleEndian);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteDouble(double value)
|
|
=> WriteDouble(value, false);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteDouble(double value, bool littleEndian)
|
|
=> WriteInt64(Unsafe.As<double, long>(ref value), littleEndian);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
void Write<T>(T value)
|
|
{
|
|
if (Unsafe.SizeOf<T>() > WriteSpaceLeft)
|
|
ThrowNotSpaceLeft();
|
|
|
|
Unsafe.WriteUnaligned(ref Buffer[WritePosition], value);
|
|
WritePosition += Unsafe.SizeOf<T>();
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
static void ThrowNotSpaceLeft()
|
|
=> throw new InvalidOperationException("There is not enough space left in the buffer.");
|
|
|
|
public Task WriteString(string s, int byteLen, bool async, CancellationToken cancellationToken = default)
|
|
=> WriteString(s, s.Length, byteLen, async, cancellationToken);
|
|
|
|
public Task WriteString(string s, int charLen, int byteLen, bool async, CancellationToken cancellationToken = default)
|
|
{
|
|
if (byteLen <= WriteSpaceLeft)
|
|
{
|
|
WriteString(s, charLen);
|
|
return Task.CompletedTask;
|
|
}
|
|
return WriteStringLong(this, async, s, charLen, byteLen, cancellationToken);
|
|
|
|
static async Task WriteStringLong(NpgsqlWriteBuffer buffer, bool async, string s, int charLen, int byteLen, CancellationToken cancellationToken)
|
|
{
|
|
Debug.Assert(byteLen > buffer.WriteSpaceLeft);
|
|
if (byteLen <= buffer.Size)
|
|
{
|
|
// String can fit entirely in an empty buffer. Flush and retry rather than
|
|
// going into the partial writing flow below (which requires ToCharArray())
|
|
await buffer.Flush(async, cancellationToken);
|
|
buffer.WriteString(s, charLen);
|
|
}
|
|
else
|
|
{
|
|
var charPos = 0;
|
|
while (true)
|
|
{
|
|
buffer.WriteStringChunked(s, charPos, charLen - charPos, true, out var charsUsed, out var completed);
|
|
if (completed)
|
|
break;
|
|
await buffer.Flush(async, cancellationToken);
|
|
charPos += charsUsed;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
internal Task WriteChars(char[] chars, int offset, int charLen, int byteLen, bool async, CancellationToken cancellationToken = default)
|
|
{
|
|
if (byteLen <= WriteSpaceLeft)
|
|
{
|
|
WriteChars(chars, offset, charLen);
|
|
return Task.CompletedTask;
|
|
}
|
|
return WriteCharsLong(this, async, chars, offset, charLen, byteLen, cancellationToken);
|
|
|
|
static async Task WriteCharsLong(NpgsqlWriteBuffer buffer, bool async, char[] chars, int offset, int charLen, int byteLen, CancellationToken cancellationToken)
|
|
{
|
|
Debug.Assert(byteLen > buffer.WriteSpaceLeft);
|
|
if (byteLen <= buffer.Size)
|
|
{
|
|
// String can fit entirely in an empty buffer. Flush and retry rather than
|
|
// going into the partial writing flow below (which requires ToCharArray())
|
|
await buffer.Flush(async, cancellationToken);
|
|
buffer.WriteChars(chars, offset, charLen);
|
|
}
|
|
else
|
|
{
|
|
var charPos = 0;
|
|
|
|
while (true)
|
|
{
|
|
buffer.WriteStringChunked(chars, charPos + offset, charLen - charPos, true, out var charsUsed, out var completed);
|
|
if (completed)
|
|
break;
|
|
await buffer.Flush(async, cancellationToken);
|
|
charPos += charsUsed;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void WriteString(string s, int len = 0)
|
|
{
|
|
Debug.Assert(TextEncoding.GetByteCount(s) <= WriteSpaceLeft);
|
|
WritePosition += TextEncoding.GetBytes(s, 0, len == 0 ? s.Length : len, Buffer, WritePosition);
|
|
}
|
|
|
|
internal void WriteChars(char[] chars, int offset, int len)
|
|
{
|
|
var charCount = len == 0 ? chars.Length : len;
|
|
Debug.Assert(TextEncoding.GetByteCount(chars, 0, charCount) <= WriteSpaceLeft);
|
|
WritePosition += TextEncoding.GetBytes(chars, offset, charCount, Buffer, WritePosition);
|
|
}
|
|
|
|
public void WriteBytes(ReadOnlySpan<byte> buf)
|
|
{
|
|
Debug.Assert(buf.Length <= WriteSpaceLeft);
|
|
buf.CopyTo(new Span<byte>(Buffer, WritePosition, Buffer.Length - WritePosition));
|
|
WritePosition += buf.Length;
|
|
}
|
|
|
|
public void WriteBytes(byte[] buf, int offset, int count)
|
|
=> WriteBytes(new ReadOnlySpan<byte>(buf, offset, count));
|
|
|
|
public Task WriteBytesRaw(byte[] bytes, bool async, CancellationToken cancellationToken = default)
|
|
{
|
|
if (bytes.Length <= WriteSpaceLeft)
|
|
{
|
|
WriteBytes(bytes);
|
|
return Task.CompletedTask;
|
|
}
|
|
return WriteBytesLong(this, async, bytes, cancellationToken);
|
|
|
|
static async Task WriteBytesLong(NpgsqlWriteBuffer buffer, bool async, byte[] bytes, CancellationToken cancellationToken)
|
|
{
|
|
if (bytes.Length <= buffer.Size)
|
|
{
|
|
// value can fit entirely in an empty buffer. Flush and retry rather than
|
|
// going into the partial writing flow below
|
|
await buffer.Flush(async, cancellationToken);
|
|
buffer.WriteBytes(bytes);
|
|
}
|
|
else
|
|
{
|
|
var remaining = bytes.Length;
|
|
do
|
|
{
|
|
if (buffer.WriteSpaceLeft == 0)
|
|
await buffer.Flush(async, cancellationToken);
|
|
var writeLen = Math.Min(remaining, buffer.WriteSpaceLeft);
|
|
var offset = bytes.Length - remaining;
|
|
buffer.WriteBytes(bytes, offset, writeLen);
|
|
remaining -= writeLen;
|
|
}
|
|
while (remaining > 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void WriteNullTerminatedString(string s)
|
|
{
|
|
Debug.Assert(s.All(c => c < 128), "Method only supports ASCII strings");
|
|
Debug.Assert(WriteSpaceLeft >= s.Length + 1);
|
|
WritePosition += Encoding.ASCII.GetBytes(s, 0, s.Length, Buffer, WritePosition);
|
|
WriteByte(0);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Write Complex
|
|
|
|
public Stream GetStream()
|
|
{
|
|
if (_parameterStream == null)
|
|
_parameterStream = new ParameterStream(this);
|
|
|
|
_parameterStream.Init();
|
|
return _parameterStream;
|
|
}
|
|
|
|
internal void WriteStringChunked(char[] chars, int charIndex, int charCount,
|
|
bool flush, out int charsUsed, out bool completed)
|
|
{
|
|
if (WriteSpaceLeft < _textEncoder.GetByteCount(chars, charIndex, char.IsHighSurrogate(chars[charIndex]) ? 2 : 1, flush: false))
|
|
{
|
|
charsUsed = 0;
|
|
completed = false;
|
|
return;
|
|
}
|
|
|
|
_textEncoder.Convert(chars, charIndex, charCount, Buffer, WritePosition, WriteSpaceLeft,
|
|
flush, out charsUsed, out var bytesUsed, out completed);
|
|
WritePosition += bytesUsed;
|
|
}
|
|
|
|
internal unsafe void WriteStringChunked(string s, int charIndex, int charCount,
|
|
bool flush, out int charsUsed, out bool completed)
|
|
{
|
|
int bytesUsed;
|
|
|
|
fixed (char* sPtr = s)
|
|
fixed (byte* bufPtr = Buffer)
|
|
{
|
|
if (WriteSpaceLeft < _textEncoder.GetByteCount(sPtr + charIndex, char.IsHighSurrogate(*(sPtr + charIndex)) ? 2 : 1, flush: false))
|
|
{
|
|
charsUsed = 0;
|
|
completed = false;
|
|
return;
|
|
}
|
|
|
|
_textEncoder.Convert(sPtr + charIndex, charCount, bufPtr + WritePosition, WriteSpaceLeft,
|
|
flush, out charsUsed, out bytesUsed, out completed);
|
|
}
|
|
|
|
WritePosition += bytesUsed;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Copy
|
|
|
|
internal void StartCopyMode()
|
|
{
|
|
_copyMode = true;
|
|
Size -= 5;
|
|
WriteCopyDataHeader();
|
|
}
|
|
|
|
internal void EndCopyMode()
|
|
{
|
|
// EndCopyMode is usually called after a Flush which ended the last CopyData message.
|
|
// That Flush also wrote the header for another CopyData which we clear here.
|
|
_copyMode = false;
|
|
Size += 5;
|
|
Clear();
|
|
}
|
|
|
|
void WriteCopyDataHeader()
|
|
{
|
|
Debug.Assert(_copyMode);
|
|
Debug.Assert(WritePosition == 0);
|
|
WriteByte(FrontendMessageCode.CopyData);
|
|
// Leave space for the message length
|
|
WriteInt32(0);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Dispose
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
_timeoutCts.Dispose();
|
|
_disposed = true;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Misc
|
|
|
|
internal void Clear()
|
|
{
|
|
WritePosition = 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns all contents currently written to the buffer (but not flushed).
|
|
/// Useful for pre-generating messages.
|
|
/// </summary>
|
|
internal byte[] GetContents()
|
|
{
|
|
var buf = new byte[WritePosition];
|
|
Array.Copy(Buffer, buf, WritePosition);
|
|
return buf;
|
|
}
|
|
|
|
#endregion
|
|
} |