Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Treat offline hosts as unpreferred
Fixes #5789
  • Loading branch information
vonzshik committed Aug 1, 2024
commit 407ef89a0d5ecaa0ec3418fa831987cc383f5999
3 changes: 2 additions & 1 deletion src/Npgsql/DatabaseState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ enum DatabaseState : byte
Offline = 1,
PrimaryReadWrite = 2,
PrimaryReadOnly = 3,
Standby = 4
Standby = 4,
UnknownAfterError = 5
}
4 changes: 3 additions & 1 deletion src/Npgsql/NpgsqlDataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,9 @@ internal DatabaseState GetDatabaseState(bool ignoreExpiration = false)

return ignoreExpiration || !databaseStateInfo.Timeout.HasExpired
? databaseStateInfo.State
: DatabaseState.Unknown;
: databaseStateInfo.State == DatabaseState.Offline
? DatabaseState.UnknownAfterError
: DatabaseState.Unknown;
}

internal DatabaseState UpdateDatabaseState(
Expand Down
41 changes: 19 additions & 22 deletions src/Npgsql/NpgsqlMultiHostDataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ static bool IsPreferred(DatabaseState state, TargetSessionAttributes preferredTy
=> state switch
{
DatabaseState.Offline => false,
DatabaseState.UnknownAfterError => false, // We will check compatibility only if we don't find preferred
DatabaseState.Unknown => true, // We will check compatibility again after refreshing the database state

DatabaseState.PrimaryReadWrite when preferredType is
Expand All @@ -150,21 +151,20 @@ TargetSessionAttributes.PreferStandby or
_ => preferredType == TargetSessionAttributes.Any
};

static bool IsOnline(DatabaseState state, TargetSessionAttributes preferredType)
{
Debug.Assert(preferredType is TargetSessionAttributes.PreferPrimary or TargetSessionAttributes.PreferStandby);
return state != DatabaseState.Offline;
}
static bool IsOnline(DatabaseState state, TargetSessionAttributes preferredType) => state != DatabaseState.Offline;

async ValueTask<NpgsqlConnector?> TryGetIdleOrNew(
NpgsqlConnection conn,
TimeSpan timeoutPerHost,
bool async,
TargetSessionAttributes preferredType, Func<DatabaseState, TargetSessionAttributes, bool> stateValidator,
TargetSessionAttributes preferredType,
bool preferred,
int poolIndex,
IList<Exception> exceptions,
CancellationToken cancellationToken)
{
Func<DatabaseState, TargetSessionAttributes, bool> stateValidator = preferred ? IsPreferred : IsOnline;

var pools = _pools;
for (var i = 0; i < pools.Length; i++)
{
Expand All @@ -183,7 +183,7 @@ static bool IsOnline(DatabaseState state, TargetSessionAttributes preferredType)
{
if (pool.TryGetIdleConnector(out connector))
{
if (databaseState == DatabaseState.Unknown)
if (databaseState == DatabaseState.Unknown || !preferred && databaseState == DatabaseState.UnknownAfterError)
{
databaseState = await connector.QueryDatabaseState(new NpgsqlTimeout(timeoutPerHost), async, cancellationToken).ConfigureAwait(false);
Debug.Assert(databaseState != DatabaseState.Unknown);
Expand All @@ -201,11 +201,11 @@ static bool IsOnline(DatabaseState state, TargetSessionAttributes preferredType)
connector = await pool.OpenNewConnector(conn, new NpgsqlTimeout(timeoutPerHost), async, cancellationToken).ConfigureAwait(false);
if (connector is not null)
{
if (databaseState == DatabaseState.Unknown)
if (databaseState == DatabaseState.Unknown || !preferred && databaseState == DatabaseState.UnknownAfterError)
{
// While opening a new connector we might have refreshed the database state, check again
databaseState = pool.GetDatabaseState();
if (databaseState == DatabaseState.Unknown)
if (databaseState == DatabaseState.Unknown || !preferred && databaseState == DatabaseState.UnknownAfterError)
databaseState = await connector.QueryDatabaseState(new NpgsqlTimeout(timeoutPerHost), async, cancellationToken).ConfigureAwait(false);
Debug.Assert(databaseState != DatabaseState.Unknown);
if (!stateValidator(databaseState, preferredType))
Expand Down Expand Up @@ -235,11 +235,13 @@ static bool IsOnline(DatabaseState state, TargetSessionAttributes preferredType)
TimeSpan timeoutPerHost,
bool async,
TargetSessionAttributes preferredType,
Func<DatabaseState, TargetSessionAttributes, bool> stateValidator,
bool preferred,
int poolIndex,
IList<Exception> exceptions,
CancellationToken cancellationToken)
{
Func<DatabaseState, TargetSessionAttributes, bool> stateValidator = preferred ? IsPreferred : IsOnline;

var pools = _pools;
for (var i = 0; i < pools.Length; i++)
{
Expand All @@ -257,11 +259,11 @@ static bool IsOnline(DatabaseState state, TargetSessionAttributes preferredType)
try
{
connector = await pool.Get(conn, new NpgsqlTimeout(timeoutPerHost), async, cancellationToken).ConfigureAwait(false);
if (databaseState == DatabaseState.Unknown)
if (databaseState == DatabaseState.Unknown || !preferred && databaseState == DatabaseState.UnknownAfterError)
{
// Get might have opened a new physical connection and refreshed the database state, check again
databaseState = pool.GetDatabaseState();
if (databaseState == DatabaseState.Unknown)
if (databaseState == DatabaseState.Unknown || !preferred && databaseState == DatabaseState.UnknownAfterError)
databaseState = await connector.QueryDatabaseState(new NpgsqlTimeout(timeoutPerHost), async, cancellationToken).ConfigureAwait(false);

Debug.Assert(databaseState != DatabaseState.Unknown);
Expand Down Expand Up @@ -299,16 +301,11 @@ internal override async ValueTask<NpgsqlConnector> Get(

var timeoutPerHost = timeout.IsSet ? timeout.CheckAndGetTimeLeft() : TimeSpan.Zero;
var preferredType = GetTargetSessionAttributes(conn);
var checkUnpreferred = preferredType is TargetSessionAttributes.PreferPrimary or TargetSessionAttributes.PreferStandby;

var connector = await TryGetIdleOrNew(conn, timeoutPerHost, async, preferredType, IsPreferred, poolIndex, exceptions, cancellationToken).ConfigureAwait(false) ??
(checkUnpreferred ?
await TryGetIdleOrNew(conn, timeoutPerHost, async, preferredType, IsOnline, poolIndex, exceptions, cancellationToken).ConfigureAwait(false)
: null) ??
await TryGet(conn, timeoutPerHost, async, preferredType, IsPreferred, poolIndex, exceptions, cancellationToken).ConfigureAwait(false) ??
(checkUnpreferred ?
await TryGet(conn, timeoutPerHost, async, preferredType, IsOnline, poolIndex, exceptions, cancellationToken).ConfigureAwait(false)
: null);
var connector = await TryGetIdleOrNew(conn, timeoutPerHost, async, preferredType, preferred: true, poolIndex, exceptions, cancellationToken).ConfigureAwait(false) ??
await TryGetIdleOrNew(conn, timeoutPerHost, async, preferredType, preferred: false, poolIndex, exceptions, cancellationToken).ConfigureAwait(false) ??
await TryGet(conn, timeoutPerHost, async, preferredType, preferred: true, poolIndex, exceptions, cancellationToken).ConfigureAwait(false) ??
await TryGet(conn, timeoutPerHost, async, preferredType, preferred: false, poolIndex, exceptions, cancellationToken).ConfigureAwait(false);

return connector ?? throw NoSuitableHostsException(exceptions);
}
Expand Down Expand Up @@ -437,7 +434,7 @@ bool TryGetValidConnector(List<NpgsqlConnector> list, TargetSessionAttributes pr
{
connector = list[i];
var lastKnownState = connector.DataSource.GetDatabaseState(ignoreExpiration: true);
Debug.Assert(lastKnownState != DatabaseState.Unknown);
Debug.Assert(lastKnownState != DatabaseState.Unknown && lastKnownState != DatabaseState.UnknownAfterError);
if (validationFunc(lastKnownState, preferredType))
{
list.RemoveAt(i);
Expand Down