Kit.Core/LibExternal/Npgsql/Internal/NpgsqlConnector.cs

2844 lines
112 KiB
C#

using System.Buffers;
using System.Data;
using System.Diagnostics;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Channels;
using System.Transactions;
using Npgsql.BackendMessages;
using Npgsql.Logging;
using Npgsql.TypeMapping;
using Npgsql.Util;
using static Npgsql.Util.Statics;
namespace Npgsql.Internal;
/// <summary>
/// Represents a connection to a PostgreSQL backend. Unlike NpgsqlConnection objects, which are
/// exposed to users, connectors are internal to Npgsql and are recycled by the connection pool.
/// </summary>
public sealed partial class NpgsqlConnector : IDisposable
{
#region Fields and Properties
/// <summary>
/// The physical connection socket to the backend.
/// </summary>
Socket _socket = default!;
/// <summary>
/// The physical connection stream to the backend, without anything on top.
/// </summary>
NetworkStream _baseStream = default!;
/// <summary>
/// The physical connection stream to the backend, layered with an SSL/TLS stream if in secure mode.
/// </summary>
Stream _stream = default!;
/// <summary>
/// The parsed connection string.
/// </summary>
public NpgsqlConnectionStringBuilder Settings { get; }
ProvideClientCertificatesCallback? ProvideClientCertificatesCallback { get; }
RemoteCertificateValidationCallback? UserCertificateValidationCallback { get; }
ProvidePasswordCallback? ProvidePasswordCallback { get; }
#pragma warning disable CA2252 // Experimental API
PhysicalOpenCallback? PhysicalOpenCallback { get; }
PhysicalOpenAsyncCallback? PhysicalOpenAsyncCallback { get; }
#pragma warning restore CA2252
public Encoding TextEncoding { get; private set; } = default!;
/// <summary>
/// Same as <see cref="TextEncoding"/>, except that it does not throw an exception if an invalid char is
/// encountered (exception fallback), but rather replaces it with a question mark character (replacement
/// fallback).
/// </summary>
internal Encoding RelaxedTextEncoding { get; private set; } = default!;
/// <summary>
/// Buffer used for reading data.
/// </summary>
internal NpgsqlReadBuffer ReadBuffer { get; private set; } = default!;
/// <summary>
/// If we read a data row that's bigger than <see cref="ReadBuffer"/>, we allocate an oversize buffer.
/// The original (smaller) buffer is stored here, and restored when the connection is reset.
/// </summary>
NpgsqlReadBuffer? _origReadBuffer;
/// <summary>
/// Buffer used for writing data.
/// </summary>
internal NpgsqlWriteBuffer WriteBuffer { get; private set; } = default!;
/// <summary>
/// The secret key of the backend for this connector, used for query cancellation.
/// </summary>
int _backendSecretKey;
/// <summary>
/// The process ID of the backend for this connector.
/// </summary>
internal int BackendProcessId { get; private set; }
bool SupportsPostgresCancellation => BackendProcessId != 0;
/// <summary>
/// A unique ID identifying this connector, used for logging. Currently mapped to BackendProcessId
/// </summary>
internal int Id => BackendProcessId;
/// <summary>
/// Information about PostgreSQL and PostgreSQL-like databases (e.g. type definitions, capabilities...).
/// </summary>
public NpgsqlDatabaseInfo DatabaseInfo { get; internal set; } = default!;
internal ConnectorTypeMapper TypeMapper { get; set; } = default!;
/// <summary>
/// The current transaction status for this connector.
/// </summary>
internal TransactionStatus TransactionStatus { get; set; }
/// <summary>
/// A transaction object for this connector. Since only one transaction can be in progress at any given time,
/// this instance is recycled. To check whether a transaction is currently in progress on this connector,
/// see <see cref="TransactionStatus"/>.
/// </summary>
internal NpgsqlTransaction? Transaction { get; set; }
internal NpgsqlTransaction? UnboundTransaction { get; set; }
/// <summary>
/// The NpgsqlConnection that (currently) owns this connector. Null if the connector isn't
/// owned (i.e. idle in the pool)
/// </summary>
internal NpgsqlConnection? Connection { get; set; }
/// <summary>
/// The number of messages that were prepended to the current message chain, but not yet sent.
/// Note that this only tracks messages which produce a ReadyForQuery message
/// </summary>
internal int PendingPrependedResponses { get; set; }
/// <summary>
/// A ManualResetEventSlim used to make sure a cancellation request doesn't run
/// while we're reading responses for the prepended query
/// as we can't gracefully handle their cancellation.
/// </summary>
readonly ManualResetEventSlim ReadingPrependedMessagesMRE = new(initialState: true);
internal NpgsqlDataReader? CurrentReader;
internal PreparedStatementManager PreparedStatementManager { get; }
internal SqlQueryParser SqlQueryParser { get; } = new();
/// <summary>
/// If the connector is currently in COPY mode, holds a reference to the importer/exporter object.
/// Otherwise null.
/// </summary>
internal ICancelable? CurrentCopyOperation;
/// <summary>
/// Holds all run-time parameters received from the backend (via ParameterStatus messages)
/// </summary>
internal Dictionary<string, string> PostgresParameters { get; }
/// <summary>
/// Holds all run-time parameters in raw, binary format for efficient handling without allocations.
/// </summary>
readonly List<(byte[] Name, byte[] Value)> _rawParameters = new();
/// <summary>
/// If this connector was broken, this contains the exception that caused the break.
/// </summary>
volatile Exception? _breakReason;
/// <summary>
/// Semaphore, used to synchronize DatabaseInfo between multiple connections, so it wouldn't be loaded in parallel.
/// </summary>
static readonly SemaphoreSlim DatabaseInfoSemaphore = new(1);
/// <summary>
/// <para>
/// Used by the pool to indicate that I/O is currently in progress on this connector, so that another write
/// isn't started concurrently. Note that since we have only one write loop, this is only ever usedto
/// protect against an over-capacity writes into a connector that's currently *asynchronously* writing.
/// </para>
/// <para>
/// It is guaranteed that the currently-executing
/// Specifically, reading may occur - and the connector may even be returned to the pool - before this is
/// released.
/// </para>
/// </summary>
internal volatile int MultiplexAsyncWritingLock;
/// <seealso cref="MultiplexAsyncWritingLock"/>
internal void FlagAsNotWritableForMultiplexing()
{
Debug.Assert(Settings.Multiplexing);
Debug.Assert(CommandsInFlightCount > 0 || IsBroken || IsClosed,
$"About to mark multiplexing connector as non-writable, but {nameof(CommandsInFlightCount)} is {CommandsInFlightCount}");
Interlocked.Exchange(ref MultiplexAsyncWritingLock, 1);
}
/// <seealso cref="MultiplexAsyncWritingLock"/>
internal void FlagAsWritableForMultiplexing()
{
Debug.Assert(Settings.Multiplexing);
if (Interlocked.CompareExchange(ref MultiplexAsyncWritingLock, 0, 1) != 1)
throw new Exception("Multiplexing lock was not taken when releasing. Please report a bug.");
}
/// <summary>
/// The timeout for reading messages that are part of the user's command
/// (i.e. which aren't internal prepended commands).
/// </summary>
/// <remarks>Precision is milliseconds</remarks>
internal int UserTimeout { private get; set; }
/// <summary>
/// A lock that's taken while a user action is in progress, e.g. a command being executed.
/// Only used when keepalive is enabled, otherwise null.
/// </summary>
SemaphoreSlim? _userLock;
/// <summary>
/// A lock that's taken while a cancellation is being delivered; new queries are blocked until the
/// cancellation is delivered. This reduces the chance that a cancellation meant for a previous
/// command will accidentally cancel a later one, see #615.
/// </summary>
object CancelLock { get; } = new();
/// <summary>
/// A lock that's taken to make sure no other concurrent operation is running.
/// Break takes it to set the state of the connector.
/// Anyone else should immediately check the state and exit
/// if the connector is closed.
/// </summary>
object SyncObj { get; } = new();
/// <summary>
/// A lock that's used to wait for the Cleanup to complete while breaking the connection.
/// </summary>
object CleanupLock { get; } = new();
readonly bool _isKeepAliveEnabled;
readonly Timer? _keepAliveTimer;
/// <summary>
/// The command currently being executed by the connector, null otherwise.
/// Used only for concurrent use error reporting purposes.
/// </summary>
NpgsqlCommand? _currentCommand;
bool _sendResetOnClose;
/// <summary>
/// The connector source (e.g. pool) from where this connector came, and to which it will be returned.
/// Note that in multi-host scenarios, this references the host-specific <see cref="ConnectorPool"/> rather than the
/// <see cref="MultiHostConnectorPool"/>,
/// </summary>
readonly ConnectorSource _connectorSource;
internal string UserFacingConnectionString => _connectorSource.UserFacingConnectionString;
/// <summary>
/// Contains the UTC timestamp when this connector was opened, used to implement
/// <see cref="NpgsqlConnectionStringBuilder.ConnectionLifetime"/>.
/// </summary>
internal DateTime OpenTimestamp { get; private set; }
internal int ClearCounter { get; set; }
volatile bool _postgresCancellationPerformed;
internal bool PostgresCancellationPerformed
{
get => _postgresCancellationPerformed;
private set => _postgresCancellationPerformed = value;
}
volatile bool _userCancellationRequested;
CancellationTokenRegistration _cancellationTokenRegistration;
internal bool UserCancellationRequested => _userCancellationRequested;
internal CancellationToken UserCancellationToken { get; set; }
internal bool AttemptPostgresCancellation { get; private set; }
static readonly TimeSpan _cancelImmediatelyTimeout = TimeSpan.FromMilliseconds(-1);
X509Certificate2? _certificate;
static readonly NpgsqlLogger Log = NpgsqlLogManager.CreateLogger(nameof(NpgsqlConnector));
internal readonly Stopwatch QueryLogStopWatch = new();
internal EndPoint? ConnectedEndPoint { get; private set; }
#endregion
#region Constants
/// <summary>
/// The minimum timeout that can be set on internal commands such as COMMIT, ROLLBACK.
/// </summary>
/// <remarks>Precision is seconds</remarks>
internal const int MinimumInternalCommandTimeout = 3;
#endregion
#region Reusable Message Objects
byte[]? _resetWithoutDeallocateMessage;
int _resetWithoutDeallocateResponseCount;
// Backend
readonly CommandCompleteMessage _commandCompleteMessage = new();
readonly ReadyForQueryMessage _readyForQueryMessage = new();
readonly ParameterDescriptionMessage _parameterDescriptionMessage = new();
readonly DataRowMessage _dataRowMessage = new();
readonly RowDescriptionMessage _rowDescriptionMessage = new();
// Since COPY is rarely used, allocate these lazily
CopyInResponseMessage? _copyInResponseMessage;
CopyOutResponseMessage? _copyOutResponseMessage;
CopyDataMessage? _copyDataMessage;
CopyBothResponseMessage? _copyBothResponseMessage;
#endregion
internal NpgsqlDataReader DataReader { get; set; }
internal NpgsqlDataReader? UnboundDataReader { get; set; }
#region Constructors
internal NpgsqlConnector(ConnectorSource connectorSource, NpgsqlConnection conn)
: this(connectorSource)
{
ProvideClientCertificatesCallback = conn.ProvideClientCertificatesCallback;
UserCertificateValidationCallback = conn.UserCertificateValidationCallback;
ProvidePasswordCallback = conn.ProvidePasswordCallback;
#pragma warning disable CA2252 // Experimental API
PhysicalOpenCallback = conn.PhysicalOpenCallback;
PhysicalOpenAsyncCallback = conn.PhysicalOpenAsyncCallback;
#pragma warning restore CA2252
}
NpgsqlConnector(NpgsqlConnector connector)
: this(connector._connectorSource)
{
ProvideClientCertificatesCallback = connector.ProvideClientCertificatesCallback;
UserCertificateValidationCallback = connector.UserCertificateValidationCallback;
ProvidePasswordCallback = connector.ProvidePasswordCallback;
}
NpgsqlConnector(ConnectorSource connectorSource)
{
Debug.Assert(connectorSource.OwnsConnectors);
_connectorSource = connectorSource;
State = ConnectorState.Closed;
TransactionStatus = TransactionStatus.Idle;
Settings = connectorSource.Settings;
PostgresParameters = new Dictionary<string, string>();
_isKeepAliveEnabled = Settings.KeepAlive > 0;
if (_isKeepAliveEnabled)
{
_userLock = new SemaphoreSlim(1, 1);
_keepAliveTimer = new Timer(PerformKeepAlive, null, Timeout.Infinite, Timeout.Infinite);
}
DataReader = new NpgsqlDataReader(this);
// TODO: Not just for automatic preparation anymore...
PreparedStatementManager = new PreparedStatementManager(this);
if (Settings.Multiplexing)
{
// Note: It's OK for this channel to be unbounded: each command enqueued to it is accompanied by sending
// it to PostgreSQL. If we overload it, a TCP zero window will make us block on the networking side
// anyway.
// Note: the in-flight channel can probably be single-writer, but that doesn't actually do anything
// at this point. And we currently rely on being able to complete the channel at any point (from
// Break). We may want to revisit this if an optimized, SingleWriter implementation is introduced.
var commandsInFlightChannel = Channel.CreateUnbounded<NpgsqlCommand>(
new UnboundedChannelOptions { SingleReader = true });
CommandsInFlightReader = commandsInFlightChannel.Reader;
CommandsInFlightWriter = commandsInFlightChannel.Writer;
// TODO: Properly implement this
if (_isKeepAliveEnabled)
throw new NotImplementedException("Keepalive not yet implemented for multiplexing");
}
}
#endregion
#region Configuration settings
internal string Host => Settings.Host!;
internal int Port => Settings.Port;
internal string Database => Settings.Database!;
string KerberosServiceName => Settings.KerberosServiceName;
int ConnectionTimeout => Settings.Timeout;
bool IntegratedSecurity => Settings.IntegratedSecurity;
/// <summary>
/// The actual command timeout value that gets set on internal commands.
/// </summary>
/// <remarks>Precision is milliseconds</remarks>
int InternalCommandTimeout
{
get
{
var internalTimeout = Settings.InternalCommandTimeout;
if (internalTimeout == -1)
return Math.Max(Settings.CommandTimeout, MinimumInternalCommandTimeout) * 1000;
// Todo: Decide what we really want here
// This assertion can easily fail if InternalCommandTimeout is set to 1 or 2 in the connection string
// We probably don't want to allow these values but in that case a Debug.Assert is the wrong way to enforce it.
Debug.Assert(internalTimeout == 0 || internalTimeout >= MinimumInternalCommandTimeout);
return internalTimeout * 1000;
}
}
#endregion Configuration settings
#region State management
int _state;
/// <summary>
/// Gets the current state of the connector
/// </summary>
internal ConnectorState State
{
get => (ConnectorState)_state;
set
{
var newState = (int)value;
if (newState == _state)
return;
Interlocked.Exchange(ref _state, newState);
}
}
/// <summary>
/// Returns whether the connector is open, regardless of any task it is currently performing
/// </summary>
bool IsConnected
=> State switch
{
ConnectorState.Ready => true,
ConnectorState.Executing => true,
ConnectorState.Fetching => true,
ConnectorState.Waiting => true,
ConnectorState.Copy => true,
ConnectorState.Replication => true,
ConnectorState.Closed => false,
ConnectorState.Connecting => false,
ConnectorState.Broken => false,
_ => throw new ArgumentOutOfRangeException("Unknown state: " + State)
};
internal bool IsReady => State == ConnectorState.Ready;
internal bool IsClosed => State == ConnectorState.Closed;
internal bool IsBroken => State == ConnectorState.Broken;
#endregion
#region Open
/// <summary>
/// Opens the physical connection to the server.
/// </summary>
/// <remarks>Usually called by the RequestConnector
/// Method of the connection pool manager.</remarks>
internal async Task Open(NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken)
{
Debug.Assert(State == ConnectorState.Closed);
State = ConnectorState.Connecting;
try
{
await OpenCore(this, Settings.SslMode, timeout, async, cancellationToken);
await LoadDatabaseInfo(forceReload: false, timeout, async, cancellationToken);
if (Settings.Pooling && !Settings.Multiplexing && !Settings.NoResetOnClose && DatabaseInfo.SupportsDiscard)
{
_sendResetOnClose = true;
GenerateResetMessage();
}
OpenTimestamp = DateTime.UtcNow;
Log.Trace($"Opened connection to {Host}:{Port}");
#pragma warning disable CA2252 // Experimental API
if (async && PhysicalOpenAsyncCallback is not null)
await PhysicalOpenAsyncCallback(this);
else if (!async && PhysicalOpenCallback is not null)
PhysicalOpenCallback(this);
#pragma warning restore CA2252
if (Settings.Multiplexing)
{
// Start an infinite async loop, which processes incoming multiplexing traffic.
// It is intentionally not awaited and will run as long as the connector is alive.
// The CommandsInFlightWriter channel is completed in Cleanup, which should cause this task
// to complete.
_ = Task.Run(MultiplexingReadLoop, CancellationToken.None)
.ContinueWith(t =>
{
// Note that we *must* observe the exception if the task is faulted.
Log.Error("Exception bubbled out of multiplexing read loop", t.Exception!, Id);
}, TaskContinuationOptions.OnlyOnFaulted);
}
if (_isKeepAliveEnabled)
{
// Start the keep alive mechanism to work by scheduling the timer.
// Otherwise, it doesn't work for cases when no query executed during
// the connection lifetime in case of a new connector.
lock (SyncObj)
{
var keepAlive = Settings.KeepAlive * 1000;
_keepAliveTimer!.Change(keepAlive, keepAlive);
}
}
}
catch (Exception e)
{
Break(e);
throw;
}
static async Task OpenCore(
NpgsqlConnector conn,
SslMode sslMode,
NpgsqlTimeout timeout,
bool async,
CancellationToken cancellationToken,
bool isFirstAttempt = true)
{
await conn.RawOpen(sslMode, timeout, async, cancellationToken, isFirstAttempt);
var username = conn.GetUsername();
if (conn.Settings.Database == null)
conn.Settings.Database = username;
timeout.CheckAndApply(conn);
conn.WriteStartupMessage(username);
await conn.Flush(async, cancellationToken);
var cancellationRegistration = conn.StartCancellableOperation(cancellationToken, attemptPgCancellation: false);
try
{
await conn.Authenticate(username, timeout, async, cancellationToken);
}
catch (PostgresException e)
when (e.SqlState == PostgresErrorCodes.InvalidAuthorizationSpecification &&
(sslMode == SslMode.Prefer && conn.IsSecure || sslMode == SslMode.Allow && !conn.IsSecure))
{
cancellationRegistration.Dispose();
Debug.Assert(!conn.IsBroken);
conn.Cleanup();
// If Prefer was specified and we failed (with SSL), retry without SSL.
// If Allow was specified and we failed (without SSL), retry with SSL
await OpenCore(
conn,
sslMode == SslMode.Prefer ? SslMode.Disable : SslMode.Require,
timeout,
async,
cancellationToken,
isFirstAttempt: false);
return;
}
using var _ = cancellationRegistration;
// We treat BackendKeyData as optional because some PostgreSQL-like database
// don't send it (CockroachDB, CrateDB)
var msg = await conn.ReadMessage(async);
if (msg.Code == BackendMessageCode.BackendKeyData)
{
var keyDataMsg = (BackendKeyDataMessage)msg;
conn.BackendProcessId = keyDataMsg.BackendProcessId;
conn._backendSecretKey = keyDataMsg.BackendSecretKey;
msg = await conn.ReadMessage(async);
}
if (msg.Code != BackendMessageCode.ReadyForQuery)
throw new NpgsqlException($"Received backend message {msg.Code} while expecting ReadyForQuery. Please file a bug.");
conn.State = ConnectorState.Ready;
}
}
internal async ValueTask LoadDatabaseInfo(bool forceReload, NpgsqlTimeout timeout, bool async,
CancellationToken cancellationToken = default)
{
// The type loading below will need to send queries to the database, and that depends on a type mapper being set up (even if its
// empty). So we set up here, and then later inject the DatabaseInfo.
// For multiplexing connectors, the type mapper is the shared pool-wide one (since when validating/binding parameters on
// multiplexing there's no connector yet). However, in the very first multiplexing connection (bootstrap phase) we create
// a connector-specific mapper, which will later become shared pool-wide one.
TypeMapper =
Settings.Multiplexing && ((MultiplexingConnectorPool)_connectorSource).MultiplexingTypeMapper is { } multiplexingTypeMapper
? multiplexingTypeMapper
: new ConnectorTypeMapper(this);
var key = new NpgsqlDatabaseInfoCacheKey(Settings);
if (forceReload || !NpgsqlDatabaseInfo.Cache.TryGetValue(key, out var database))
{
var hasSemaphore = async
? await DatabaseInfoSemaphore.WaitAsync(timeout.CheckAndGetTimeLeft(), cancellationToken)
: DatabaseInfoSemaphore.Wait(timeout.CheckAndGetTimeLeft(), cancellationToken);
// We've timed out - calling Check, to throw the correct exception
if (!hasSemaphore)
timeout.Check();
try
{
if (forceReload || !NpgsqlDatabaseInfo.Cache.TryGetValue(key, out database))
{
NpgsqlDatabaseInfo.Cache[key] = database = await NpgsqlDatabaseInfo.Load(this, timeout, async);
}
}
finally
{
DatabaseInfoSemaphore.Release();
}
}
DatabaseInfo = database;
TypeMapper.DatabaseInfo = database;
}
internal async ValueTask<ClusterState> QueryClusterState(
NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken = default)
{
using var batch = CreateBatch();
batch.BatchCommands.Add(new NpgsqlBatchCommand("select pg_is_in_recovery()"));
batch.BatchCommands.Add(new NpgsqlBatchCommand("SHOW default_transaction_read_only"));
batch.Timeout = (int)timeout.CheckAndGetTimeLeft().TotalSeconds;
var reader = async ? await batch.ExecuteReaderAsync(cancellationToken) : batch.ExecuteReader();
try
{
if (async)
{
await reader.ReadAsync(cancellationToken);
_isHotStandBy = reader.GetBoolean(0);
await reader.NextResultAsync(cancellationToken);
await reader.ReadAsync(cancellationToken);
}
else
{
reader.Read();
_isHotStandBy = reader.GetBoolean(0);
reader.NextResult();
reader.Read();
}
_isTransactionReadOnly = reader.GetString(0) != "off";
var clusterState = UpdateClusterState();
Debug.Assert(clusterState.HasValue);
return clusterState.Value;
}
finally
{
if (async)
await reader.DisposeAsync();
else
reader.Dispose();
}
}
void WriteStartupMessage(string username)
{
var startupParams = new Dictionary<string, string>
{
["user"] = username,
["client_encoding"] = Settings.ClientEncoding ??
PostgresEnvironment.ClientEncoding ??
"UTF8",
["database"] = Settings.Database!
};
if (Settings.ApplicationName?.Length > 0)
startupParams["application_name"] = Settings.ApplicationName;
if (Settings.SearchPath?.Length > 0)
startupParams["search_path"] = Settings.SearchPath;
var timezone = Settings.Timezone ?? PostgresEnvironment.TimeZone;
if (timezone != null)
startupParams["TimeZone"] = timezone;
var options = Settings.Options ?? PostgresEnvironment.Options;
if (options?.Length > 0)
startupParams["options"] = options;
switch (Settings.ReplicationMode)
{
case ReplicationMode.Logical:
startupParams["replication"] = "database";
break;
case ReplicationMode.Physical:
startupParams["replication"] = "true";
break;
}
WriteStartup(startupParams);
}
string GetUsername()
{
var username = Settings.Username;
if (username?.Length > 0)
return username;
username = PostgresEnvironment.User;
if (username?.Length > 0)
return username;
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
username = KerberosUsernameProvider.GetUsername(Settings.IncludeRealm);
if (username?.Length > 0)
return username;
}
username = Environment.UserName;
if (username?.Length > 0)
return username;
throw new NpgsqlException("No username could be found, please specify one explicitly");
}
async Task RawOpen(SslMode sslMode, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken, bool isFirstAttempt = true)
{
try
{
if (async)
await ConnectAsync(timeout, cancellationToken);
else
Connect(timeout);
_baseStream = new NetworkStream(_socket, true);
_stream = _baseStream;
if (Settings.Encoding == "UTF8")
{
TextEncoding = PGUtil.UTF8Encoding;
RelaxedTextEncoding = PGUtil.RelaxedUTF8Encoding;
}
else
{
TextEncoding = Encoding.GetEncoding(Settings.Encoding, EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback);
RelaxedTextEncoding = Encoding.GetEncoding(Settings.Encoding, EncoderFallback.ReplacementFallback, DecoderFallback.ReplacementFallback);
}
ReadBuffer = new NpgsqlReadBuffer(this, _stream, _socket, Settings.ReadBufferSize, TextEncoding, RelaxedTextEncoding);
WriteBuffer = new NpgsqlWriteBuffer(this, _stream, _socket, Settings.WriteBufferSize, TextEncoding);
timeout.CheckAndApply(this);
IsSecure = false;
if (sslMode is SslMode.Prefer or SslMode.Require or SslMode.VerifyCA or SslMode.VerifyFull)
{
WriteSslRequest();
await Flush(async, cancellationToken);
await ReadBuffer.Ensure(1, async);
var response = (char)ReadBuffer.ReadByte();
timeout.CheckAndApply(this);
switch (response)
{
default:
throw new NpgsqlException($"Received unknown response {response} for SSLRequest (expecting S or N)");
case 'N':
if (sslMode != SslMode.Prefer)
throw new NpgsqlException("SSL connection requested. No SSL enabled connection from this host is configured.");
break;
case 'S':
var clientCertificates = new X509Certificate2Collection();
var certPath = Settings.SslCertificate ?? PostgresEnvironment.SslCert ?? PostgresEnvironment.SslCertDefault;
if (certPath != null)
{
var password = Settings.SslPassword;
if (Path.GetExtension(certPath).ToUpperInvariant() != ".PFX")
{
#if NET5_0_OR_GREATER
// It's PEM time
var keyPath = Settings.SslKey ?? PostgresEnvironment.SslKey ?? PostgresEnvironment.SslKeyDefault;
_certificate = string.IsNullOrEmpty(password)
? X509Certificate2.CreateFromPemFile(certPath, keyPath)
: X509Certificate2.CreateFromEncryptedPemFile(certPath, password, keyPath);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Windows crypto API has a bug with pem certs
// See #3650
using var previousCert = _certificate;
_certificate = new X509Certificate2(_certificate.Export(X509ContentType.Pkcs12));
}
#else
throw new NotSupportedException("PEM certificates are only supported with .NET 5 and higher");
#endif
}
if (_certificate is null)
_certificate = new X509Certificate2(certPath, password);
clientCertificates.Add(_certificate);
}
ProvideClientCertificatesCallback?.Invoke(clientCertificates);
var checkCertificateRevocation = Settings.CheckCertificateRevocation;
RemoteCertificateValidationCallback? certificateValidationCallback;
if (UserCertificateValidationCallback is not null)
{
if (sslMode is SslMode.VerifyCA or SslMode.VerifyFull)
throw new ArgumentException(string.Format(NpgsqlStrings.CannotUseSslVerifyWithUserCallback, sslMode));
if (Settings.RootCertificate is not null)
throw new ArgumentException(NpgsqlStrings.CannotUseSslRootCertificateWithUserCallback);
certificateValidationCallback = UserCertificateValidationCallback;
}
else if (sslMode is SslMode.Prefer or SslMode.Require)
{
if (isFirstAttempt && sslMode is SslMode.Require && !Settings.TrustServerCertificate)
throw new ArgumentException(NpgsqlStrings.CannotUseSslModeRequireWithoutTrustServerCertificate);
certificateValidationCallback = SslTrustServerValidation;
checkCertificateRevocation = false;
}
else if ((Settings.RootCertificate ?? PostgresEnvironment.SslCertRoot ?? PostgresEnvironment.SslCertRootDefault) is
{ } certRootPath)
{
certificateValidationCallback = SslRootValidation(certRootPath, sslMode == SslMode.VerifyFull);
}
else if (sslMode == SslMode.VerifyCA)
{
certificateValidationCallback = SslVerifyCAValidation;
}
else
{
Debug.Assert(sslMode == SslMode.VerifyFull);
certificateValidationCallback = SslVerifyFullValidation;
}
timeout.CheckAndApply(this);
try
{
var sslStream = new SslStream(_stream, leaveInnerStreamOpen: false, certificateValidationCallback);
var sslProtocols = SslProtocols.None;
// On .NET Framework SslProtocols.None can be disabled, see #3718
#if NETSTANDARD2_0
sslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12;
#endif
if (async)
await sslStream.AuthenticateAsClientAsync(Host, clientCertificates,
sslProtocols, checkCertificateRevocation);
else
sslStream.AuthenticateAsClient(Host, clientCertificates,
sslProtocols, checkCertificateRevocation);
_stream = sslStream;
}
catch (Exception e)
{
throw new NpgsqlException("Exception while performing SSL handshake", e);
}
ReadBuffer.Underlying = _stream;
WriteBuffer.Underlying = _stream;
IsSecure = true;
Log.Trace("SSL negotiation successful");
break;
}
if (ReadBuffer.ReadBytesLeft > 0)
throw new NpgsqlException("Additional unencrypted data received after SSL negotiation - this should never happen, and may be an indication of a man-in-the-middle attack.");
}
Log.Trace($"Socket connected to {Host}:{Port}");
}
catch
{
_certificate?.Dispose();
_certificate = null;
_stream?.Dispose();
_stream = null!;
_baseStream?.Dispose();
_baseStream = null!;
_socket?.Dispose();
_socket = null!;
throw;
}
}
void Connect(NpgsqlTimeout timeout)
{
// Note that there aren't any timeout-able or cancellable DNS methods
var endpoints = NpgsqlConnectionStringBuilder.IsUnixSocket(Host, Port, out var socketPath)
? new EndPoint[] { new UnixDomainSocketEndPoint(socketPath) }
: Dns.GetHostAddresses(Host).Select(a => new IPEndPoint(a, Port)).ToArray();
timeout.Check();
// Give each endpoint an equal share of the remaining time
var perEndpointTimeout = -1; // Default to infinity
if (timeout.IsSet)
perEndpointTimeout = (int)(timeout.CheckAndGetTimeLeft().Ticks / endpoints.Length / 10);
for (var i = 0; i < endpoints.Length; i++)
{
var endpoint = endpoints[i];
Log.Trace($"Attempting to connect to {endpoint}");
var protocolType =
endpoint.AddressFamily == AddressFamily.InterNetwork ||
endpoint.AddressFamily == AddressFamily.InterNetworkV6
? ProtocolType.Tcp
: ProtocolType.IP;
var socket = new Socket(endpoint.AddressFamily, SocketType.Stream, protocolType)
{
Blocking = false
};
try
{
try
{
socket.Connect(endpoint);
}
catch (SocketException e)
{
if (e.SocketErrorCode != SocketError.WouldBlock)
throw;
}
var write = new List<Socket> { socket };
var error = new List<Socket> { socket };
Socket.Select(null, write, error, perEndpointTimeout);
var errorCode = (int)socket.GetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Error)!;
if (errorCode != 0)
throw new SocketException(errorCode);
if (!write.Any())
throw new TimeoutException("Timeout during connection attempt");
socket.Blocking = true;
SetSocketOptions(socket);
_socket = socket;
ConnectedEndPoint = endpoint;
return;
}
catch (Exception e)
{
try { socket.Dispose(); }
catch
{
// ignored
}
Log.Trace($"Failed to connect to {endpoint}", e);
if (i == endpoints.Length - 1)
throw new NpgsqlException($"Failed to connect to {endpoint}", e);
}
}
}
async Task ConnectAsync(NpgsqlTimeout timeout, CancellationToken cancellationToken)
{
// Note that there aren't any timeout-able or cancellable DNS methods
var endpoints = NpgsqlConnectionStringBuilder.IsUnixSocket(Host, Port, out var socketPath)
? new EndPoint[] { new UnixDomainSocketEndPoint(socketPath) }
: (await GetHostAddressesAsync(timeout, cancellationToken))
.Select(a => new IPEndPoint(a, Port)).ToArray();
// Give each IP an equal share of the remaining time
var perIpTimespan = default(TimeSpan);
var perIpTimeout = timeout;
if (timeout.IsSet)
{
perIpTimespan = new TimeSpan(timeout.CheckAndGetTimeLeft().Ticks / endpoints.Length);
perIpTimeout = new NpgsqlTimeout(perIpTimespan);
}
for (var i = 0; i < endpoints.Length; i++)
{
var endpoint = endpoints[i];
Log.Trace($"Attempting to connect to {endpoint}");
var protocolType =
endpoint.AddressFamily == AddressFamily.InterNetwork ||
endpoint.AddressFamily == AddressFamily.InterNetworkV6
? ProtocolType.Tcp
: ProtocolType.IP;
var socket = new Socket(endpoint.AddressFamily, SocketType.Stream, protocolType);
try
{
await OpenSocketConnectionAsync(socket, endpoint, perIpTimeout, cancellationToken);
SetSocketOptions(socket);
_socket = socket;
ConnectedEndPoint = endpoint;
return;
}
catch (Exception e)
{
try
{
socket.Dispose();
}
catch
{
// ignored
}
cancellationToken.ThrowIfCancellationRequested();
if (e is OperationCanceledException)
e = new TimeoutException("Timeout during connection attempt");
Log.Trace($"Failed to connect to {endpoint}", e);
if (i == endpoints.Length - 1)
throw new NpgsqlException($"Failed to connect to {endpoint}", e);
}
}
Task<IPAddress[]> GetHostAddressesAsync(NpgsqlTimeout timeout, CancellationToken cancellationToken)
{
// .NET 6.0 added cancellation support to GetHostAddressesAsync, which allows us to implement real
// cancellation and timeout. On older TFMs, we fake-cancel the operation, i.e. stop waiting
// and raise the exception, but the actual connection task is left running.
#if NET6_0_OR_GREATER
var task = TaskExtensions.ExecuteWithTimeout(
ct => Dns.GetHostAddressesAsync(Host, ct),
timeout, cancellationToken);
#else
var task = Dns.GetHostAddressesAsync(Host);
#endif
// As the cancellation support of GetHostAddressesAsync is not guaranteed on all platforms
// we apply the fake-cancel mechanism in all cases.
return task.WithCancellationAndTimeout(timeout, cancellationToken);
}
static Task OpenSocketConnectionAsync(Socket socket, EndPoint endpoint, NpgsqlTimeout perIpTimeout, CancellationToken cancellationToken)
{
// .NET 5.0 added cancellation support to ConnectAsync, which allows us to implement real
// cancellation and timeout. On older TFMs, we fake-cancel the operation, i.e. stop waiting
// and raise the exception, but the actual connection task is left running.
#if NET5_0_OR_GREATER
return TaskExtensions.ExecuteWithTimeout(
ct => socket.ConnectAsync(endpoint, ct).AsTask(),
perIpTimeout, cancellationToken);
#else
return socket.ConnectAsync(endpoint)
.WithCancellationAndTimeout(perIpTimeout, cancellationToken);
#endif
}
}
void SetSocketOptions(Socket socket)
{
if (socket.AddressFamily == AddressFamily.InterNetwork || socket.AddressFamily == AddressFamily.InterNetworkV6)
socket.NoDelay = true;
if (Settings.SocketReceiveBufferSize > 0)
socket.ReceiveBufferSize = Settings.SocketReceiveBufferSize;
if (Settings.SocketSendBufferSize > 0)
socket.SendBufferSize = Settings.SocketSendBufferSize;
if (Settings.TcpKeepAlive)
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
if (Settings.TcpKeepAliveInterval > 0 && Settings.TcpKeepAliveTime == 0)
throw new ArgumentException("If TcpKeepAliveInterval is defined, TcpKeepAliveTime must be defined as well");
if (Settings.TcpKeepAliveTime > 0)
{
var timeSeconds = Settings.TcpKeepAliveTime;
var intervalSeconds = Settings.TcpKeepAliveInterval > 0
? Settings.TcpKeepAliveInterval
: Settings.TcpKeepAliveTime;
#if NETSTANDARD2_0 || NETSTANDARD2_1
var timeMilliseconds = timeSeconds * 1000;
var intervalMilliseconds = intervalSeconds * 1000;
// For the following see https://msdn.microsoft.com/en-us/library/dd877220.aspx
var uintSize = Marshal.SizeOf(typeof(uint));
var inOptionValues = new byte[uintSize * 3];
BitConverter.GetBytes((uint)1).CopyTo(inOptionValues, 0);
BitConverter.GetBytes((uint)timeMilliseconds).CopyTo(inOptionValues, uintSize);
BitConverter.GetBytes((uint)intervalMilliseconds).CopyTo(inOptionValues, uintSize * 2);
var result = 0;
try
{
result = socket.IOControl(IOControlCode.KeepAliveValues, inOptionValues, null);
}
catch (PlatformNotSupportedException)
{
throw new PlatformNotSupportedException("Setting TCP Keepalive Time and TCP Keepalive Interval is supported only on Windows, Mono and .NET Core 3.1+. " +
"TCP keepalives can still be used on other systems but are enabled via the TcpKeepAlive option or configured globally for the machine, see the relevant docs.");
}
if (result != 0)
throw new NpgsqlException($"Got non-zero value when trying to set TCP keepalive: {result}");
#else
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, timeSeconds);
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, intervalSeconds);
#endif
}
}
#endregion
#region I/O
readonly ChannelReader<NpgsqlCommand>? CommandsInFlightReader;
internal readonly ChannelWriter<NpgsqlCommand>? CommandsInFlightWriter;
internal volatile int CommandsInFlightCount;
internal ManualResetValueTaskSource<object?> ReaderCompleted { get; } =
new() { RunContinuationsAsynchronously = true };
async Task MultiplexingReadLoop()
{
Debug.Assert(Settings.Multiplexing);
Debug.Assert(CommandsInFlightReader != null);
NpgsqlCommand? command = null;
var commandsRead = 0;
try
{
while (await CommandsInFlightReader.WaitToReadAsync())
{
commandsRead = 0;
Debug.Assert(!InTransaction);
while (CommandsInFlightReader.TryRead(out command))
{
commandsRead++;
await ReadBuffer.Ensure(5, true);
// We have a resultset for the command - hand back control to the command (which will
// return it to the user)
command.TraceReceivedFirstResponse();
ReaderCompleted.Reset();
command.ExecutionCompletion.SetResult(this);
// Now wait until that command's reader is disposed. Note that RunContinuationsAsynchronously is
// true, so that the user code calling NpgsqlDataReader.Dispose will not continue executing
// synchronously here. The prevents issues if the code after the next command's execution
// completion blocks.
await new ValueTask(ReaderCompleted, ReaderCompleted.Version);
Debug.Assert(!InTransaction);
}
// Atomically update the commands in-flight counter, and check if it reached 0. If so, the
// connector is idle and can be returned.
// Note that this is racing with over-capacity writing, which can select any connector at any
// time (see MultiplexingWriteLoop), and we must make absolutely sure that if a connector is
// returned to the pool, it is *never* written to unless properly dequeued from the Idle channel.
if (Interlocked.Add(ref CommandsInFlightCount, -commandsRead) == 0)
{
// There's a race condition where the continuation of an asynchronous multiplexing write may not
// have executed yet, and the flush may still be in progress. We know all I/O has already
// been sent - because the reader has already consumed the entire resultset. So we wait until
// the connector's write lock has been released (long waiting will never occur here).
SpinWait.SpinUntil(() => MultiplexAsyncWritingLock == 0 || IsBroken);
ResetReadBuffer();
_connectorSource.Return(this);
}
}
Log.Trace("Exiting multiplexing read loop", Id);
}
catch (Exception e)
{
Debug.Assert(IsBroken);
// Decrement the commands already dequeued from the in-flight counter
Interlocked.Add(ref CommandsInFlightCount, -commandsRead);
// When a connector is broken, the causing exception is stored on it. We fail commands with
// that exception - rather than the one thrown here - since the break may have happened during
// writing, and we want to bubble that one up.
// Drain any pending in-flight commands and fail them. Note that some have only been written
// to the buffer, and not sent to the server.
command?.ExecutionCompletion.SetException(_breakReason!);
try
{
while (true)
{
var pendingCommand = await CommandsInFlightReader.ReadAsync();
// TODO: the exception we have here is sometimes just the result of the write loop breaking
// the connector, so it doesn't represent the actual root cause.
pendingCommand.ExecutionCompletion.SetException(_breakReason!);
}
}
catch (ChannelClosedException)
{
// All good, drained to the channel and failed all commands
}
// "Return" the connector to the pool to for cleanup (e.g. update total connector count)
_connectorSource.Return(this);
Log.Error("Exception in multiplexing read loop", e, Id);
}
Debug.Assert(CommandsInFlightCount == 0);
}
#endregion
#region Frontend message processing
/// <summary>
/// Prepends a message to be sent at the beginning of the next message chain.
/// </summary>
internal void PrependInternalMessage(byte[] rawMessage, int responseMessageCount)
{
PendingPrependedResponses += responseMessageCount;
var t = WritePregenerated(rawMessage);
Debug.Assert(t.IsCompleted, "Could not fully write pregenerated message into the buffer");
}
#endregion
#region Backend message processing
internal IBackendMessage ReadMessage(DataRowLoadingMode dataRowLoadingMode = DataRowLoadingMode.NonSequential)
=> ReadMessage(async: false, dataRowLoadingMode).GetAwaiter().GetResult();
internal ValueTask<IBackendMessage> ReadMessage(bool async, DataRowLoadingMode dataRowLoadingMode = DataRowLoadingMode.NonSequential)
=> ReadMessage(async, dataRowLoadingMode, readingNotifications: false)!;
internal ValueTask<IBackendMessage?> ReadMessageWithNotifications(bool async)
=> ReadMessage(async, DataRowLoadingMode.NonSequential, readingNotifications: true);
internal ValueTask<IBackendMessage?> ReadMessage(
bool async,
DataRowLoadingMode dataRowLoadingMode,
bool readingNotifications)
{
if (PendingPrependedResponses > 0 ||
dataRowLoadingMode != DataRowLoadingMode.NonSequential ||
readingNotifications ||
ReadBuffer.ReadBytesLeft < 5)
{
return ReadMessageLong(this, async, dataRowLoadingMode, readingNotifications);
}
var messageCode = (BackendMessageCode)ReadBuffer.ReadByte();
switch (messageCode)
{
case BackendMessageCode.NoticeResponse:
case BackendMessageCode.NotificationResponse:
case BackendMessageCode.ParameterStatus:
case BackendMessageCode.ErrorResponse:
ReadBuffer.ReadPosition--;
return ReadMessageLong(this, async, dataRowLoadingMode, readingNotifications: false);
case BackendMessageCode.ReadyForQuery:
break;
}
PGUtil.ValidateBackendMessageCode(messageCode);
var len = ReadBuffer.ReadInt32() - 4; // Transmitted length includes itself
if (len > ReadBuffer.ReadBytesLeft)
{
ReadBuffer.ReadPosition -= 5;
return ReadMessageLong(this, async, dataRowLoadingMode, readingNotifications: false);
}
return new ValueTask<IBackendMessage?>(ParseServerMessage(ReadBuffer, messageCode, len, false));
static async ValueTask<IBackendMessage?> ReadMessageLong(
NpgsqlConnector connector,
bool async,
DataRowLoadingMode dataRowLoadingMode,
bool readingNotifications,
bool isReadingPrependedMessage = false)
{
// First read the responses of any prepended messages.
if (connector.PendingPrependedResponses > 0 && !isReadingPrependedMessage)
{
try
{
// TODO: There could be room for optimization here, rather than the async call(s)
connector.ReadBuffer.Timeout = TimeSpan.FromMilliseconds(connector.InternalCommandTimeout);
for (; connector.PendingPrependedResponses > 0; connector.PendingPrependedResponses--)
await ReadMessageLong(connector, async, DataRowLoadingMode.Skip, readingNotifications: false, isReadingPrependedMessage: true);
// We've read all the prepended response.
// Allow cancellation to proceed.
connector.ReadingPrependedMessagesMRE.Set();
}
catch (Exception e)
{
// Prepended queries should never fail.
// If they do, we're not even going to attempt to salvage the connector.
throw connector.Break(e);
}
}
PostgresException? error = null;
try
{
connector.ReadBuffer.Timeout = TimeSpan.FromMilliseconds(connector.UserTimeout);
while (true)
{
await connector.ReadBuffer.Ensure(5, async, readingNotifications);
var messageCode = (BackendMessageCode)connector.ReadBuffer.ReadByte();
PGUtil.ValidateBackendMessageCode(messageCode);
var len = connector.ReadBuffer.ReadInt32() - 4; // Transmitted length includes itself
if ((messageCode == BackendMessageCode.DataRow &&
dataRowLoadingMode != DataRowLoadingMode.NonSequential) ||
messageCode == BackendMessageCode.CopyData)
{
if (dataRowLoadingMode == DataRowLoadingMode.Skip)
{
await connector.ReadBuffer.Skip(len, async);
continue;
}
}
else if (len > connector.ReadBuffer.ReadBytesLeft)
{
if (len > connector.ReadBuffer.Size)
{
var oversizeBuffer = connector.ReadBuffer.AllocateOversize(len);
if (connector._origReadBuffer == null)
connector._origReadBuffer = connector.ReadBuffer;
else
connector.ReadBuffer.Dispose();
connector.ReadBuffer = oversizeBuffer;
}
await connector.ReadBuffer.Ensure(len, async);
}
var msg = connector.ParseServerMessage(connector.ReadBuffer, messageCode, len, isReadingPrependedMessage);
switch (messageCode)
{
case BackendMessageCode.ErrorResponse:
Debug.Assert(msg == null);
// An ErrorResponse is (almost) always followed by a ReadyForQuery. Save the error
// and throw it as an exception when the ReadyForQuery is received (next).
error = PostgresException.Load(connector.ReadBuffer, connector.Settings.IncludeErrorDetail);
if (connector.State == ConnectorState.Connecting)
{
// During the startup/authentication phase, an ErrorResponse isn't followed by
// an RFQ. Instead, the server closes the connection immediately
throw error;
}
else if (PostgresErrorCodes.IsCriticalFailure(error, clusterError: false))
{
// Consider the connection dead
throw connector.Break(error);
}
continue;
case BackendMessageCode.ReadyForQuery:
if (error != null)
{
NpgsqlEventSource.Log.CommandFailed();
throw error;
}
break;
// Asynchronous messages which can come anytime, they have already been handled
// in ParseServerMessage. Read the next message.
case BackendMessageCode.NoticeResponse:
case BackendMessageCode.NotificationResponse:
case BackendMessageCode.ParameterStatus:
Debug.Assert(msg == null);
if (!readingNotifications)
continue;
return null;
}
Debug.Assert(msg != null, "Message is null for code: " + messageCode);
return msg;
}
}
catch (PostgresException e)
{
// TODO: move it up the stack, like #3126 did (relevant for non-command-execution scenarios, like COPY)
if (connector.CurrentReader is null)
connector.EndUserAction();
if (e.SqlState == PostgresErrorCodes.QueryCanceled && connector.PostgresCancellationPerformed)
{
// The query could be canceled because of a user cancellation or a timeout - raise the proper exception.
// If _postgresCancellationPerformed is false, this is an unsolicited cancellation -
// just bubble up thePostgresException.
throw connector.UserCancellationRequested
? new OperationCanceledException("Query was cancelled", e, connector.UserCancellationToken)
: new NpgsqlException("Exception while reading from stream",
new TimeoutException("Timeout during reading attempt"));
}
throw;
}
catch (NpgsqlException)
{
// An ErrorResponse isn't followed by ReadyForQuery
if (error != null)
ExceptionDispatchInfo.Capture(error).Throw();
throw;
}
}
}
internal IBackendMessage? ParseServerMessage(NpgsqlReadBuffer buf, BackendMessageCode code, int len, bool isPrependedMessage)
{
switch (code)
{
case BackendMessageCode.RowDescription:
return _rowDescriptionMessage.Load(buf, TypeMapper);
case BackendMessageCode.DataRow:
return _dataRowMessage.Load(len);
case BackendMessageCode.CommandComplete:
return _commandCompleteMessage.Load(buf, len);
case BackendMessageCode.ReadyForQuery:
var rfq = _readyForQueryMessage.Load(buf);
if (!isPrependedMessage)
{
// Transaction status on prepended messages shouldn't be processed, because there may be prepended messages
// before the begin transaction message. In this case, they will contain transaction status Idle, which will
// clear our Pending transaction status. Only process transaction status on RFQ's from user-provided, non
// prepended messages.
ProcessNewTransactionStatus(rfq.TransactionStatusIndicator);
}
return rfq;
case BackendMessageCode.EmptyQueryResponse:
return EmptyQueryMessage.Instance;
case BackendMessageCode.ParseComplete:
return ParseCompleteMessage.Instance;
case BackendMessageCode.ParameterDescription:
return _parameterDescriptionMessage.Load(buf);
case BackendMessageCode.BindComplete:
return BindCompleteMessage.Instance;
case BackendMessageCode.NoData:
return NoDataMessage.Instance;
case BackendMessageCode.CloseComplete:
return CloseCompletedMessage.Instance;
case BackendMessageCode.ParameterStatus:
ReadParameterStatus(buf.GetNullTerminatedBytes(), buf.GetNullTerminatedBytes());
return null;
case BackendMessageCode.NoticeResponse:
var notice = PostgresNotice.Load(buf, Settings.IncludeErrorDetail);
Log.Debug($"Received notice: {notice.MessageText}", Id);
Connection?.OnNotice(notice);
return null;
case BackendMessageCode.NotificationResponse:
Connection?.OnNotification(new NpgsqlNotificationEventArgs(buf));
return null;
case BackendMessageCode.AuthenticationRequest:
var authType = (AuthenticationRequestType)buf.ReadInt32();
return authType switch
{
AuthenticationRequestType.AuthenticationOk => (AuthenticationRequestMessage)AuthenticationOkMessage.Instance,
AuthenticationRequestType.AuthenticationCleartextPassword => AuthenticationCleartextPasswordMessage.Instance,
AuthenticationRequestType.AuthenticationMD5Password => AuthenticationMD5PasswordMessage.Load(buf),
AuthenticationRequestType.AuthenticationGSS => AuthenticationGSSMessage.Instance,
AuthenticationRequestType.AuthenticationSSPI => AuthenticationSSPIMessage.Instance,
AuthenticationRequestType.AuthenticationGSSContinue => AuthenticationGSSContinueMessage.Load(buf, len),
AuthenticationRequestType.AuthenticationSASL => new AuthenticationSASLMessage(buf),
AuthenticationRequestType.AuthenticationSASLContinue => new AuthenticationSASLContinueMessage(buf, len - 4),
AuthenticationRequestType.AuthenticationSASLFinal => new AuthenticationSASLFinalMessage(buf, len - 4),
_ => throw new NotSupportedException($"Authentication method not supported (Received: {authType})")
};
case BackendMessageCode.BackendKeyData:
return new BackendKeyDataMessage(buf);
case BackendMessageCode.CopyInResponse:
return (_copyInResponseMessage ??= new CopyInResponseMessage()).Load(ReadBuffer);
case BackendMessageCode.CopyOutResponse:
return (_copyOutResponseMessage ??= new CopyOutResponseMessage()).Load(ReadBuffer);
case BackendMessageCode.CopyData:
return (_copyDataMessage ??= new CopyDataMessage()).Load(len);
case BackendMessageCode.CopyBothResponse:
return (_copyBothResponseMessage ??= new CopyBothResponseMessage()).Load(ReadBuffer);
case BackendMessageCode.CopyDone:
return CopyDoneMessage.Instance;
case BackendMessageCode.PortalSuspended:
throw new NpgsqlException("Unimplemented message: " + code);
case BackendMessageCode.ErrorResponse:
return null;
case BackendMessageCode.FunctionCallResponse:
// We don't use the obsolete function call protocol
throw new NpgsqlException("Unexpected backend message: " + code);
default:
throw new InvalidOperationException($"Internal Npgsql bug: unexpected value {code} of enum {nameof(BackendMessageCode)}. Please file a bug.");
}
}
/// <summary>
/// Reads backend messages and discards them, stopping only after a message of the given type has
/// been seen. Only a sync I/O version of this method exists - in async flows we inline the loop
/// rather than calling an additional async method, in order to avoid the overhead.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal IBackendMessage SkipUntil(BackendMessageCode stopAt)
{
Debug.Assert(stopAt != BackendMessageCode.DataRow, "Shouldn't be used for rows, doesn't know about sequential");
while (true)
{
var msg = ReadMessage(async: false, DataRowLoadingMode.Skip).GetAwaiter().GetResult()!;
Debug.Assert(!(msg is DataRowMessage));
if (msg.Code == stopAt)
return msg;
}
}
#endregion Backend message processing
#region Transactions
internal Task Rollback(bool async, CancellationToken cancellationToken = default)
{
Log.Debug("Rolling back transaction", Id);
return ExecuteInternalCommand(PregeneratedMessages.RollbackTransaction, async, cancellationToken);
}
internal bool InTransaction
=> TransactionStatus switch
{
TransactionStatus.Idle => false,
TransactionStatus.Pending => true,
TransactionStatus.InTransactionBlock => true,
TransactionStatus.InFailedTransactionBlock => true,
_ => throw new InvalidOperationException($"Internal Npgsql bug: unexpected value {TransactionStatus} of enum {nameof(TransactionStatus)}. Please file a bug.")
};
/// <summary>
/// Handles a new transaction indicator received on a ReadyForQuery message
/// </summary>
void ProcessNewTransactionStatus(TransactionStatus newStatus)
{
if (newStatus == TransactionStatus)
return;
TransactionStatus = newStatus;
switch (newStatus)
{
case TransactionStatus.Idle:
break;
case TransactionStatus.InTransactionBlock:
case TransactionStatus.InFailedTransactionBlock:
// In multiplexing mode, we can't support transaction in SQL: the connector must be removed from the
// writable connectors list, otherwise other commands may get written to it. So the user must tell us
// about the transaction via BeginTransaction.
if (Connection is null)
{
Debug.Assert(Settings.Multiplexing);
throw new NotSupportedException("In multiplexing mode, transactions must be started with BeginTransaction");
}
break;
case TransactionStatus.Pending:
throw new Exception($"Internal Npgsql bug: invalid TransactionStatus {nameof(TransactionStatus.Pending)} received, should be frontend-only");
default:
throw new InvalidOperationException(
$"Internal Npgsql bug: unexpected value {newStatus} of enum {nameof(TransactionStatus)}. Please file a bug.");
}
}
internal void ClearTransaction(Exception? disposeReason = null)
{
Transaction?.DisposeImmediately(disposeReason);
TransactionStatus = TransactionStatus.Idle;
}
#endregion
#region SSL
/// <summary>
/// Returns whether SSL is being used for the connection
/// </summary>
internal bool IsSecure { get; private set; }
/// <summary>
/// Returns whether SCRAM-SHA256 is being user for the connection
/// </summary>
internal bool IsScram { get; private set; }
/// <summary>
/// Returns whether SCRAM-SHA256-PLUS is being user for the connection
/// </summary>
internal bool IsScramPlus { get; private set; }
static readonly RemoteCertificateValidationCallback SslVerifyFullValidation =
(sender, certificate, chain, sslPolicyErrors)
=> sslPolicyErrors == SslPolicyErrors.None;
static readonly RemoteCertificateValidationCallback SslVerifyCAValidation =
(sender, certificate, chain, sslPolicyErrors)
=> sslPolicyErrors == SslPolicyErrors.None || sslPolicyErrors == SslPolicyErrors.RemoteCertificateNameMismatch;
static readonly RemoteCertificateValidationCallback SslTrustServerValidation =
(sender, certificate, chain, sslPolicyErrors)
=> true;
static RemoteCertificateValidationCallback SslRootValidation(string certRootPath, bool verifyFull) =>
(sender, certificate, chain, sslPolicyErrors) =>
{
if (certificate is null || chain is null)
return false;
// No errors here - no reason to check further
if (sslPolicyErrors == SslPolicyErrors.None)
return true;
// That's VerifyCA check and the only error is name mismatch - no reason to check further
if (!verifyFull && sslPolicyErrors == SslPolicyErrors.RemoteCertificateNameMismatch)
return true;
// That's VerifyFull check and we have name mismatch - no reason to check further
if (verifyFull && sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch))
return false;
var certs = new X509Certificate2Collection();
#if NET5_0_OR_GREATER
if (Path.GetExtension(certRootPath).ToUpperInvariant() != ".PFX")
certs.ImportFromPemFile(certRootPath);
#endif
if (certs.Count == 0)
certs.Add(new X509Certificate2(certRootPath));
#if NET5_0_OR_GREATER
chain.ChainPolicy.CustomTrustStore.AddRange(certs);
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
#endif
chain.ChainPolicy.ExtraStore.AddRange(certs);
return chain.Build(certificate as X509Certificate2 ?? new X509Certificate2(certificate));
};
#endregion SSL
#region Cancel
internal void ResetCancellation()
{
// If a cancellation is in progress, wait for it to "complete" before proceeding (#615)
lock (CancelLock)
{
if (PendingPrependedResponses > 0)
ReadingPrependedMessagesMRE.Reset();
Debug.Assert(ReadingPrependedMessagesMRE.IsSet || PendingPrependedResponses > 0);
}
}
internal void PerformUserCancellation()
{
var connection = Connection;
if (connection is null || connection.ConnectorBindingScope == ConnectorBindingScope.Reader || UserCancellationRequested)
return;
// Take the lock first to make sure there is no concurrent Break.
// We should be safe to take it as Break only take it to set the state.
lock (SyncObj)
{
// The connector is dead, exit gracefully.
if (!IsConnected)
return;
// The connector is still alive, take the CancelLock before exiting SingleUseLock.
// If a break will happen after, it's going to wait for the cancellation to complete.
Monitor.Enter(CancelLock);
}
try
{
// Wait before we've read all responses for the prepended queries
// as we can't gracefully handle their cancellation.
// Break makes sure that it's going to be set even if we fail while reading them.
// We don't wait indefinitely to avoid deadlocks from synchronous CancellationToken.Register
// See #5032
if (!ReadingPrependedMessagesMRE.Wait(0))
return;
_userCancellationRequested = true;
if (AttemptPostgresCancellation && SupportsPostgresCancellation)
{
var cancellationTimeout = Settings.CancellationTimeout;
if (PerformPostgresCancellation() && cancellationTimeout >= 0)
{
if (cancellationTimeout > 0)
{
UserTimeout = cancellationTimeout;
ReadBuffer.Timeout = TimeSpan.FromMilliseconds(cancellationTimeout);
ReadBuffer.Cts.CancelAfter(cancellationTimeout);
}
return;
}
}
UserTimeout = -1;
ReadBuffer.Timeout = _cancelImmediatelyTimeout;
ReadBuffer.Cts.Cancel();
}
finally
{
Monitor.Exit(CancelLock);
}
}
/// <summary>
/// Creates another connector and sends a cancel request through it for this connector. This method never throws, but returns
/// whether the cancellation attempt failed.
/// </summary>
/// <returns>
/// <para>
/// <see langword="true" /> if the cancellation request was successfully delivered, or if it was skipped because a previous
/// request was already sent. <see langword="false"/> if the cancellation request could not be delivered because of an exception
/// (the method logs internally).
/// </para>
/// <para>
/// This does not indicate whether the cancellation attempt was successful on the PostgreSQL side - only if the request was
/// delivered.
/// </para>
/// </returns>
internal bool PerformPostgresCancellation()
{
Debug.Assert(BackendProcessId != 0, "PostgreSQL cancellation requested by the backend doesn't support it");
lock (CancelLock)
{
if (PostgresCancellationPerformed)
return true;
Log.Debug("Sending cancellation...", Id);
PostgresCancellationPerformed = true;
try
{
var cancelConnector = new NpgsqlConnector(this);
cancelConnector.DoCancelRequest(BackendProcessId, _backendSecretKey);
}
catch (Exception e)
{
var socketException = e.InnerException as SocketException;
if (socketException == null || socketException.SocketErrorCode != SocketError.ConnectionReset)
{
Log.Debug("Exception caught while attempting to cancel command", e, Id);
return false;
}
}
return true;
}
}
void DoCancelRequest(int backendProcessId, int backendSecretKey)
{
Debug.Assert(State == ConnectorState.Closed);
try
{
RawOpen(Settings.SslMode, new NpgsqlTimeout(TimeSpan.FromSeconds(ConnectionTimeout)), false, CancellationToken.None)
.GetAwaiter().GetResult();
WriteCancelRequest(backendProcessId, backendSecretKey);
Flush();
Debug.Assert(ReadBuffer.ReadBytesLeft == 0);
// Now wait for the server to close the connection, better chance of the cancellation
// actually being delivered before we continue with the user's logic.
var count = _stream.Read(ReadBuffer.Buffer, 0, 1);
if (count > 0)
Log.Error("Received response after sending cancel request, shouldn't happen! First byte: " + ReadBuffer.Buffer[0]);
}
finally
{
FullCleanup();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal CancellationTokenRegistration StartCancellableOperation(
CancellationToken cancellationToken = default,
bool attemptPgCancellation = true)
{
_userCancellationRequested = PostgresCancellationPerformed = false;
UserCancellationToken = cancellationToken;
ReadBuffer.Cts.ResetCts();
AttemptPostgresCancellation = attemptPgCancellation;
return _cancellationTokenRegistration =
cancellationToken.Register(static c => ((NpgsqlConnector)c!).PerformUserCancellation(), this);
}
/// <summary>
/// Starts a new cancellable operation within an ongoing user action. This should only be used if a single user
/// action spans several different actions which each has its own cancellation tokens. For example, a command
/// execution is a single user action, but spans ExecuteReaderQuery, NextResult, Read and so forth.
/// </summary>
/// <remarks>
/// Only one level of nested operations is supported. It is an error to call this method if it has previously
/// been called, and the returned <see cref="CancellationTokenRegistration"/> was not disposed.
/// </remarks>
/// <param name="cancellationToken">
/// The cancellation token provided by the user. Callbacks will be registered on this token for executing the
/// cancellation, and the token will be included in any thrown <see cref="OperationCanceledException"/>.
/// </param>
/// <param name="attemptPgCancellation">
/// If <see langword="true" />, PostgreSQL cancellation will be attempted when the user requests cancellation or
/// a timeout occurs, followed by a client-side socket cancellation once
/// <see cref="NpgsqlConnectionStringBuilder.CancellationTimeout"/> has elapsed. If <see langword="false" />,
/// PostgreSQL cancellation will be skipped and client-socket cancellation will occur immediately.
/// </param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal CancellationTokenRegistration StartNestedCancellableOperation(
CancellationToken cancellationToken = default,
bool attemptPgCancellation = true)
{
UserCancellationToken = cancellationToken;
AttemptPostgresCancellation = attemptPgCancellation;
return _cancellationTokenRegistration =
cancellationToken.Register(static c => ((NpgsqlConnector)c!).PerformUserCancellation(), this);
}
#endregion Cancel
#region Close / Reset
/// <summary>
/// Closes ongoing operations, i.e. an open reader exists or a COPY operation still in progress, as
/// part of a connection close.
/// </summary>
internal async Task CloseOngoingOperations(bool async)
{
var reader = CurrentReader;
var copyOperation = CurrentCopyOperation;
if (reader != null)
await reader.Close(connectionClosing: true, async, isDisposing: false);
else if (copyOperation != null)
{
// TODO: There's probably a race condition as the COPY operation may finish on its own during the next few lines
// Note: we only want to cancel import operations, since in these cases cancel is safe.
// Export cancellations go through the PostgreSQL "asynchronous" cancel mechanism and are
// therefore vulnerable to the race condition in #615.
if (copyOperation is NpgsqlBinaryImporter ||
copyOperation is NpgsqlCopyTextWriter ||
copyOperation is NpgsqlRawCopyStream rawCopyStream && rawCopyStream.CanWrite)
{
try
{
if (async)
await copyOperation.CancelAsync();
else
copyOperation.Cancel();
}
catch (Exception e)
{
Log.Warn("Error while cancelling COPY on connector close", e, Id);
}
}
try
{
if (async)
await copyOperation.DisposeAsync();
else
copyOperation.Dispose();
}
catch (Exception e)
{
Log.Warn("Error while disposing cancelled COPY on connector close", e, Id);
}
}
}
// TODO in theory this should be async-optional, but the only I/O done here is the Terminate Flush, which is
// very unlikely to block (plus locking would need to be worked out)
internal void Close()
{
lock (SyncObj)
{
Log.Trace("Closing connector", Id);
if (IsReady)
{
try
{
// At this point, there could be some prepended commands (like DISCARD ALL)
// which make no sense to send on connection close
// see https://github.com/npgsql/npgsql/issues/3592
WriteBuffer.Clear();
WriteTerminate();
Flush();
}
catch (Exception e)
{
Log.Error("Exception while closing connector", e, Id);
Debug.Assert(IsBroken);
}
}
switch (State)
{
case ConnectorState.Broken:
case ConnectorState.Closed:
return;
}
State = ConnectorState.Closed;
}
FullCleanup();
}
internal bool TryRemovePendingEnlistedConnector(Transaction transaction)
=> _connectorSource.TryRemovePendingEnlistedConnector(this, transaction);
internal void Return() => _connectorSource.Return(this);
/// <inheritdoc />
public void Dispose() => Close();
/// <summary>
/// Called when an unexpected message has been received during an action. Breaks the
/// connector and returns the appropriate message.
/// </summary>
internal Exception UnexpectedMessageReceived(BackendMessageCode received)
=> throw Break(new Exception($"Received unexpected backend message {received}. Please file a bug."));
/// <summary>
/// Called when a connector becomes completely unusable, e.g. when an unexpected I/O exception is raised or when
/// we lose protocol sync.
/// Note that fatal errors during the Open phase do *not* pass through here.
/// </summary>
/// <param name="reason">The exception that caused the break.</param>
/// <returns>The exception given in <paramref name="reason"/> for chaining calls.</returns>
internal Exception Break(Exception reason)
{
Debug.Assert(!IsClosed);
Monitor.Enter(SyncObj);
if (State == ConnectorState.Broken)
{
// We're already broken.
// Exit SingleUseLock to unblock other threads (like cancellation).
Monitor.Exit(SyncObj);
// Wait for the break to complete before going forward.
lock (CleanupLock) { }
return reason;
}
try
{
// If we're broken while reading prepended messages
// the cancellation request might still be waiting on the MRE.
// Unblock it.
ReadingPrependedMessagesMRE.Set();
Log.Error("Breaking connector", reason, Id);
// Note that we may be reading and writing from the same connector concurrently, so safely set
// the original reason for the break before actually closing the socket etc.
Interlocked.CompareExchange(ref _breakReason, reason, null);
State = ConnectorState.Broken;
// Take the CleanupLock while in SingleUseLock to make sure concurrent Break doesn't take it first.
Monitor.Enter(CleanupLock);
}
finally
{
// Unblock other threads (like cancellation) to proceed and exit gracefully.
Monitor.Exit(SyncObj);
}
try
{
lock (CancelLock)
{
// Note we only set the cluster to offline and clear the pool if the connection is being broken (we're in this method),
// *and* the exception indicates that the PG cluster really is down; the latter includes any IO/timeout issue,
// but does not include e.g. authentication failure or timeouts with disabled cancellation.
if (reason is NpgsqlException { IsTransient: true } ne &&
(ne.InnerException is not TimeoutException || Settings.CancellationTimeout != -1) ||
reason is PostgresException pe && PostgresErrorCodes.IsCriticalFailure(pe))
{
ClusterStateCache.UpdateClusterState(Host, Port, ClusterState.Offline, DateTime.UtcNow,
Settings.HostRecheckSecondsTranslated);
_connectorSource.Clear();
}
var connection = Connection;
FullCleanup();
if (connection is not null)
{
var closeLockTaken = connection.TakeCloseLock();
Debug.Assert(closeLockTaken);
if (Settings.ReplicationMode == ReplicationMode.Off)
{
Connection = null;
if (connection.ConnectorBindingScope != ConnectorBindingScope.None)
Return();
connection.EnlistedTransaction = null;
connection.Connector = null;
connection.ConnectorBindingScope = ConnectorBindingScope.None;
}
connection.FullState = ConnectionState.Broken;
connection.ReleaseCloseLock();
}
}
return reason;
}
finally
{
Monitor.Exit(CleanupLock);
}
}
void FullCleanup()
{
lock (CleanupLock)
{
if (Settings.Multiplexing)
{
FlagAsNotWritableForMultiplexing();
// Note that in multiplexing, this could be called from the read loop, while the write loop is
// writing into the channel. To make sure this race condition isn't a problem, the channel currently
// isn't set up with SingleWriter (since at this point it doesn't do anything).
CommandsInFlightWriter!.Complete();
// The connector's read loop has a continuation to observe and log any exception coming out
// (see Open)
}
Log.Trace("Cleaning up connector", Id);
Cleanup();
if (_isKeepAliveEnabled)
{
_userLock!.Dispose();
_userLock = null;
_keepAliveTimer!.Change(Timeout.Infinite, Timeout.Infinite);
_keepAliveTimer.Dispose();
}
}
}
/// <summary>
/// Closes the socket and cleans up client-side resources associated with this connector.
/// </summary>
/// <remarks>
/// This method doesn't actually perform any meaningful I/O, and therefore is sync-only.
/// </remarks>
void Cleanup()
{
try
{
_stream?.Dispose();
}
catch
{
// ignored
}
if (CurrentReader != null)
{
CurrentReader.Command.State = CommandState.Idle;
try
{
// Note that this never actually blocks on I/O, since the stream is also closed
// (which is why we don't need to call CloseAsync)
CurrentReader.Close();
}
catch
{
// ignored
}
CurrentReader = null;
}
if (CurrentCopyOperation != null)
{
try
{
// Note that this never actually blocks on I/O, since the stream is also closed
// (which is why we don't need to call DisposeAsync)
CurrentCopyOperation.Dispose();
}
catch
{
// ignored
}
CurrentCopyOperation = null;
}
ClearTransaction(_breakReason);
_stream = null!;
_baseStream = null!;
_origReadBuffer?.Dispose();
_origReadBuffer = null;
ReadBuffer?.Dispose();
ReadBuffer = null!;
WriteBuffer?.Dispose();
WriteBuffer = null!;
Connection = null;
PostgresParameters.Clear();
_currentCommand = null;
if (_certificate is not null)
{
_certificate.Dispose();
_certificate = null;
}
}
void GenerateResetMessage()
{
var sb = new StringBuilder("SET SESSION AUTHORIZATION DEFAULT;RESET ALL;");
_resetWithoutDeallocateResponseCount = 2;
if (DatabaseInfo.SupportsCloseAll)
{
sb.Append("CLOSE ALL;");
_resetWithoutDeallocateResponseCount++;
}
if (DatabaseInfo.SupportsUnlisten)
{
sb.Append("UNLISTEN *;");
_resetWithoutDeallocateResponseCount++;
}
if (DatabaseInfo.SupportsAdvisoryLocks)
{
sb.Append("SELECT pg_advisory_unlock_all();");
_resetWithoutDeallocateResponseCount += 2;
}
if (DatabaseInfo.SupportsDiscardSequences)
{
sb.Append("DISCARD SEQUENCES;");
_resetWithoutDeallocateResponseCount++;
}
if (DatabaseInfo.SupportsDiscardTemp)
{
sb.Append("DISCARD TEMP");
_resetWithoutDeallocateResponseCount++;
}
_resetWithoutDeallocateResponseCount++; // One ReadyForQuery at the end
_resetWithoutDeallocateMessage = PregeneratedMessages.Generate(WriteBuffer, sb.ToString());
}
/// <summary>
/// Called when a pooled connection is closed, and its connector is returned to the pool.
/// Resets the connector back to its initial state, releasing server-side sources
/// (e.g. prepared statements), resetting parameters to their defaults, and resetting client-side
/// state
/// </summary>
internal async Task Reset(bool async)
{
bool endBindingScope;
// We start user action in case a keeplive happens concurrently, or a concurrent user command (bug)
using (StartUserAction(attemptPgCancellation: false))
{
// Our buffer may contain unsent prepended messages, so clear it out.
// In practice, this is (currently) only done when beginning a transaction or a transaction savepoint.
WriteBuffer.Clear();
PendingPrependedResponses = 0;
ResetReadBuffer();
Transaction?.UnbindIfNecessary();
// Must rollback transaction before sending DISCARD ALL
switch (TransactionStatus)
{
case TransactionStatus.Idle:
// There is an undisposed transaction on multiplexing connection
endBindingScope = Connection?.ConnectorBindingScope == ConnectorBindingScope.Transaction;
break;
case TransactionStatus.Pending:
// BeginTransaction() was called, but was left in the write buffer and not yet sent to server.
// Just clear the transaction state.
ProcessNewTransactionStatus(TransactionStatus.Idle);
ClearTransaction();
endBindingScope = true;
break;
case TransactionStatus.InTransactionBlock:
case TransactionStatus.InFailedTransactionBlock:
await Rollback(async);
ClearTransaction();
endBindingScope = true;
break;
default:
throw new InvalidOperationException($"Internal Npgsql bug: unexpected value {TransactionStatus} of enum {nameof(TransactionStatus)}. Please file a bug.");
}
if (_sendResetOnClose)
{
if (PreparedStatementManager.NumPrepared > 0)
{
// We have prepared statements, so we can't reset the connection state with DISCARD ALL
// Note: the send buffer has been cleared above, and we assume all this will fit in it.
PrependInternalMessage(_resetWithoutDeallocateMessage!, _resetWithoutDeallocateResponseCount);
}
else
{
// There are no prepared statements.
// We simply send DISCARD ALL which is more efficient than sending the above messages separately
PrependInternalMessage(PregeneratedMessages.DiscardAll, 2);
}
}
DataReader.UnbindIfNecessary();
}
if (endBindingScope)
{
// Connection is null if a connection enlisted in a TransactionScope was closed before the
// TransactionScope completed - the connector is still enlisted, but has no connection.
Connection?.EndBindingScope(ConnectorBindingScope.Transaction);
}
}
/// <summary>
/// The connector may have allocated an oversize read buffer, to hold big rows in non-sequential reading.
/// This switches us back to the original one and returns the buffer to <see cref="ArrayPool{T}" />.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void ResetReadBuffer()
{
if (_origReadBuffer != null)
{
Debug.Assert(_origReadBuffer.ReadBytesLeft == 0);
Debug.Assert(_origReadBuffer.ReadPosition == 0);
if (ReadBuffer.ReadBytesLeft > 0)
{
// There is still something in the buffer which we haven't read yet
// In most cases it's ParameterStatus which can be sent asynchronously
// If in some extreme case we have too much data left in the buffer to store in the original buffer
// we just leave the oversize buffer as is and will try again on next reset
if (ReadBuffer.ReadBytesLeft > _origReadBuffer.Size)
return;
ReadBuffer.CopyTo(_origReadBuffer);
}
ReadBuffer.Dispose();
ReadBuffer = _origReadBuffer;
_origReadBuffer = null;
}
}
internal void UnprepareAll()
{
ExecuteInternalCommand("DEALLOCATE ALL");
PreparedStatementManager.ClearAll();
}
#endregion Close / Reset
#region Locking
internal UserAction StartUserAction(CancellationToken cancellationToken = default, bool attemptPgCancellation = true)
=> StartUserAction(ConnectorState.Executing, command: null, cancellationToken, attemptPgCancellation);
internal UserAction StartUserAction(
ConnectorState newState,
CancellationToken cancellationToken = default,
bool attemptPgCancellation = true)
=> StartUserAction(newState, command: null, cancellationToken, attemptPgCancellation);
/// <summary>
/// Starts a user action. This makes sure that another action isn't already in progress, handles synchronization with keepalive,
/// and sets up cancellation.
/// </summary>
/// <param name="newState">The new state to be set when entering this user action.</param>
/// <param name="command">
/// The <see cref="NpgsqlCommand" /> that is starting execution - if an <see cref="NpgsqlOperationInProgressException" /> is
/// thrown, it will reference this.
/// </param>
/// <param name="cancellationToken">
/// The cancellation token provided by the user. Callbacks will be registered on this token for executing the cancellation,
/// and the token will be included in any thrown <see cref="OperationCanceledException"/>.
/// </param>
/// <param name="attemptPgCancellation">
/// If <see langword="true" />, PostgreSQL cancellation will be attempted when the user requests cancellation or a timeout
/// occurs, followed by a client-side socket cancellation once <see cref="NpgsqlConnectionStringBuilder.CancellationTimeout"/> has
/// elapsed. If <see langword="false" />, PostgreSQL cancellation will be skipped and client-socket cancellation will occur
/// immediately.
/// </param>
internal UserAction StartUserAction(
ConnectorState newState,
NpgsqlCommand? command,
CancellationToken cancellationToken = default,
bool attemptPgCancellation = true)
{
// If keepalive is enabled, we must protect state transitions with a SemaphoreSlim
// (which itself must be protected by a lock, since its dispose isn't thread-safe).
// This will make the keepalive abort safely if a user query is in progress, and make
// the user query wait if a keepalive is in progress.
// If keepalive isn't enabled, we don't use the semaphore and rely only on the connector's
// state (updated via Interlocked.Exchange) to detect concurrent use, on a best-effort basis.
if (!_isKeepAliveEnabled)
return DoStartUserAction();
lock (SyncObj)
{
if (!IsConnected)
{
throw IsBroken
? new NpgsqlException("The connection was previously broken because of the following exception", _breakReason)
: new NpgsqlException("The connection is closed");
}
if (!_userLock!.Wait(0))
{
var currentCommand = _currentCommand;
throw currentCommand == null
? new NpgsqlOperationInProgressException(State)
: new NpgsqlOperationInProgressException(currentCommand);
}
try
{
// Disable keepalive, it will be restarted at the end of the user action
_keepAliveTimer!.Change(Timeout.Infinite, Timeout.Infinite);
// We now have both locks and are sure nothing else is running.
// Check that the connector is ready.
return DoStartUserAction();
}
catch
{
_userLock.Release();
throw;
}
}
UserAction DoStartUserAction()
{
switch (State)
{
case ConnectorState.Ready:
break;
case ConnectorState.Closed:
case ConnectorState.Broken:
throw new InvalidOperationException("Connection is not open");
case ConnectorState.Executing:
case ConnectorState.Fetching:
case ConnectorState.Waiting:
case ConnectorState.Replication:
case ConnectorState.Connecting:
case ConnectorState.Copy:
var currentCommand = _currentCommand;
throw currentCommand == null
? new NpgsqlOperationInProgressException(State)
: new NpgsqlOperationInProgressException(currentCommand);
default:
throw new ArgumentOutOfRangeException(nameof(State), State, "Invalid connector state: " + State);
}
Debug.Assert(IsReady);
cancellationToken.ThrowIfCancellationRequested();
Log.Trace("Start user action", Id);
State = newState;
_currentCommand = command;
StartCancellableOperation(cancellationToken, attemptPgCancellation);
// We reset the UserTimeout for every user action, so it wouldn't leak from the previous query or action
// For example, we might have successfully cancelled the previous query (so the connection is not broken)
// But the next time, we call the Prepare, which doesn't set it's own timeout
UserTimeout = (command?.CommandTimeout ?? Settings.CommandTimeout) * 1000;
return new UserAction(this);
}
}
internal void EndUserAction()
{
Debug.Assert(CurrentReader == null);
_cancellationTokenRegistration.Dispose();
if (_isKeepAliveEnabled)
{
lock (SyncObj)
{
if (IsReady || !IsConnected)
return;
var keepAlive = Settings.KeepAlive * 1000;
_keepAliveTimer!.Change(keepAlive, keepAlive);
Log.Trace("End user action", Id);
_currentCommand = null;
_userLock!.Release();
State = ConnectorState.Ready;
}
}
else
{
if (IsReady || !IsConnected)
return;
Log.Trace("End user action", Id);
_currentCommand = null;
State = ConnectorState.Ready;
}
}
/// <summary>
/// An IDisposable wrapper around <see cref="EndUserAction"/>.
/// </summary>
internal readonly struct UserAction : IDisposable
{
readonly NpgsqlConnector _connector;
internal UserAction(NpgsqlConnector connector) => _connector = connector;
public void Dispose() => _connector.EndUserAction();
}
#endregion
#region Keepalive
#pragma warning disable CA1801 // Review unused parameters
void PerformKeepAlive(object? state)
{
Debug.Assert(_isKeepAliveEnabled);
// SemaphoreSlim.Dispose() isn't thread-safe - it may be in progress so we shouldn't try to wait on it;
// we need a standard lock to protect it.
if (!Monitor.TryEnter(SyncObj))
return;
try
{
// There may already be a user action, or the connector may be closed etc.
if (!IsReady)
return;
Log.Trace("Performing keepalive", Id);
AttemptPostgresCancellation = false;
var timeout = InternalCommandTimeout;
WriteBuffer.Timeout = TimeSpan.FromSeconds(timeout);
UserTimeout = timeout;
WriteSync(async: false);
Flush();
SkipUntil(BackendMessageCode.ReadyForQuery);
Log.Trace("Performed keepalive", Id);
}
catch (Exception e)
{
Log.Error("Keepalive failure", e, Id);
try
{
Break(new NpgsqlException("Exception while sending a keepalive", e));
}
catch (Exception e2)
{
Log.Error("Further exception while breaking connector on keepalive failure", e2, Id);
}
}
finally
{
Monitor.Exit(SyncObj);
}
}
#pragma warning restore CA1801 // Review unused parameters
#endregion
#region Wait
internal async Task<bool> Wait(bool async, int timeout, CancellationToken cancellationToken = default)
{
using var _ = StartUserAction(ConnectorState.Waiting, cancellationToken: cancellationToken, attemptPgCancellation: false);
// We may have prepended messages in the connection's write buffer - these need to be flushed now.
await Flush(async, cancellationToken);
var keepaliveMs = Settings.KeepAlive * 1000;
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
var timeoutForKeepalive = _isKeepAliveEnabled && (timeout <= 0 || keepaliveMs < timeout);
UserTimeout = timeoutForKeepalive ? keepaliveMs : timeout;
try
{
var msg = await ReadMessageWithNotifications(async);
if (msg != null)
{
throw Break(
new NpgsqlException($"Received unexpected message of type {msg.Code} while waiting"));
}
return true;
}
catch (NpgsqlException e) when (e.InnerException is TimeoutException)
{
if (!timeoutForKeepalive) // We really timed out
return false;
}
// Time for a keepalive
var keepaliveTime = Stopwatch.StartNew();
await WriteSync(async, cancellationToken);
await Flush(async, cancellationToken);
var receivedNotification = false;
var expectedMessageCode = BackendMessageCode.RowDescription;
while (true)
{
IBackendMessage? msg;
try
{
msg = await ReadMessageWithNotifications(async);
}
catch (Exception e) when (e is OperationCanceledException || e is NpgsqlException npgEx && npgEx.InnerException is TimeoutException)
{
// We're somewhere in the middle of a reading keepalive messages
// Breaking the connection, as we've lost protocol sync
throw Break(e);
}
if (msg == null)
{
receivedNotification = true;
continue;
}
if (msg.Code != BackendMessageCode.ReadyForQuery)
throw new NpgsqlException($"Received unexpected message of type {msg.Code} while expecting {expectedMessageCode} as part of keepalive");
Log.Trace("Performed keepalive", Id);
if (receivedNotification)
return true; // Notification was received during the keepalive
cancellationToken.ThrowIfCancellationRequested();
break;
}
if (timeout > 0)
timeout -= (keepaliveMs + (int)keepaliveTime.ElapsedMilliseconds);
}
}
#endregion
#region Supported features and PostgreSQL settings
internal bool UseConformingStrings { get; private set; }
/// <summary>
/// The connection's timezone as reported by PostgreSQL, in the IANA/Olson database format.
/// </summary>
internal string Timezone { get; private set; } = default!;
bool? _isTransactionReadOnly;
bool? _isHotStandBy;
#endregion Supported features and PostgreSQL settings
#region Execute internal command
internal void ExecuteInternalCommand(string query)
=> ExecuteInternalCommand(query, false).GetAwaiter().GetResult();
internal async Task ExecuteInternalCommand(string query, bool async, CancellationToken cancellationToken = default)
{
Log.Trace($"Executing internal command: {query}", Id);
await WriteQuery(query, async, cancellationToken);
await Flush(async, cancellationToken);
Expect<CommandCompleteMessage>(await ReadMessage(async), this);
Expect<ReadyForQueryMessage>(await ReadMessage(async), this);
}
internal async Task ExecuteInternalCommand(byte[] data, bool async, CancellationToken cancellationToken = default)
{
Debug.Assert(State != ConnectorState.Ready, "Forgot to start a user action...");
Log.Trace("Executing internal pregenerated command", Id);
await WritePregenerated(data, async, cancellationToken);
await Flush(async, cancellationToken);
Expect<CommandCompleteMessage>(await ReadMessage(async), this);
Expect<ReadyForQueryMessage>(await ReadMessage(async), this);
}
#endregion
#region Misc
/// <summary>
/// Creates and returns a <see cref="NpgsqlCommand"/> object associated with the <see cref="NpgsqlConnector"/>.
/// </summary>
/// <param name="cmdText">The text of the query.</param>
/// <returns>A <see cref="NpgsqlCommand"/> object.</returns>
public NpgsqlCommand CreateCommand(string? cmdText = null) => new(cmdText, this);
/// <summary>
/// Creates and returns a <see cref="NpgsqlBatch"/> object associated with the <see cref="NpgsqlConnector"/>.
/// </summary>
/// <returns>A <see cref="NpgsqlBatch"/> object.</returns>
public NpgsqlBatch CreateBatch() => new NpgsqlBatch(this);
void ReadParameterStatus(ReadOnlySpan<byte> incomingName, ReadOnlySpan<byte> incomingValue)
{
byte[] rawName;
byte[] rawValue;
for (var i = 0; i < _rawParameters.Count; i++)
{
(var currentName, var currentValue) = _rawParameters[i];
if (incomingName.SequenceEqual(currentName))
{
if (incomingValue.SequenceEqual(currentValue))
return;
rawName = currentName;
rawValue = incomingValue.ToArray();
_rawParameters[i] = (rawName, rawValue);
goto ProcessParameter;
}
}
rawName = incomingName.ToArray();
rawValue = incomingValue.ToArray();
_rawParameters.Add((rawName, rawValue));
ProcessParameter:
var name = TextEncoding.GetString(rawName);
var value = TextEncoding.GetString(rawValue);
PostgresParameters[name] = value;
switch (name)
{
case "standard_conforming_strings":
if (value != "on" && Settings.Multiplexing)
throw Break(new NotSupportedException("standard_conforming_strings must be on with multiplexing"));
UseConformingStrings = value == "on";
return;
case "TimeZone":
Timezone = value;
return;
case "default_transaction_read_only":
_isTransactionReadOnly = value == "on";
UpdateClusterState();
return;
case "in_hot_standby":
_isHotStandBy = value == "on";
UpdateClusterState();
return;
}
}
ClusterState? UpdateClusterState()
{
if (_isTransactionReadOnly.HasValue && _isHotStandBy.HasValue)
{
var state = _isHotStandBy.Value
? ClusterState.Standby
: _isTransactionReadOnly.Value
? ClusterState.PrimaryReadOnly
: ClusterState.PrimaryReadWrite;
return ClusterStateCache.UpdateClusterState(Settings.Host!, Settings.Port, state, DateTime.UtcNow,
Settings.HostRecheckSecondsTranslated);
}
return null;
}
#endregion Misc
}
#region Enums
/// <summary>
/// Expresses the exact state of a connector.
/// </summary>
enum ConnectorState
{
/// <summary>
/// The connector has either not yet been opened or has been closed.
/// </summary>
Closed,
/// <summary>
/// The connector is currently connecting to a PostgreSQL server.
/// </summary>
Connecting,
/// <summary>
/// The connector is connected and may be used to send a new query.
/// </summary>
Ready,
/// <summary>
/// The connector is waiting for a response to a query which has been sent to the server.
/// </summary>
Executing,
/// <summary>
/// The connector is currently fetching and processing query results.
/// </summary>
Fetching,
/// <summary>
/// The connector is currently waiting for asynchronous notifications to arrive.
/// </summary>
Waiting,
/// <summary>
/// The connection was broken because an unexpected error occurred which left it in an unknown state.
/// This state isn't implemented yet.
/// </summary>
Broken,
/// <summary>
/// The connector is engaged in a COPY operation.
/// </summary>
Copy,
/// <summary>
/// The connector is engaged in streaming replication.
/// </summary>
Replication,
}
#pragma warning disable CA1717
enum TransactionStatus : byte
#pragma warning restore CA1717
{
/// <summary>
/// Currently not in a transaction block
/// </summary>
Idle = (byte)'I',
/// <summary>
/// Currently in a transaction block
/// </summary>
InTransactionBlock = (byte)'T',
/// <summary>
/// Currently in a failed transaction block (queries will be rejected until block is ended)
/// </summary>
InFailedTransactionBlock = (byte)'E',
/// <summary>
/// A new transaction has been requested but not yet transmitted to the backend. It will be transmitted
/// prepended to the next query.
/// This is a client-side state option only, and is never transmitted from the backend.
/// </summary>
Pending = byte.MaxValue,
}
/// <summary>
/// Specifies how to load/parse DataRow messages as they're received from the backend.
/// </summary>
internal enum DataRowLoadingMode
{
/// <summary>
/// Load DataRows in non-sequential mode
/// </summary>
NonSequential,
/// <summary>
/// Load DataRows in sequential mode
/// </summary>
Sequential,
/// <summary>
/// Skip DataRow messages altogether
/// </summary>
Skip
}
#endregion