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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
72 changes: 49 additions & 23 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,49 @@ 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 IsUnpreferred(DatabaseState state, TargetSessionAttributes preferredType)
=> state switch
{
DatabaseState.Offline => false,
DatabaseState.UnknownAfterError => true,
DatabaseState.Unknown => true, // We will check compatibility again after refreshing the database state

DatabaseState.PrimaryReadWrite when preferredType is
TargetSessionAttributes.Primary or
TargetSessionAttributes.PreferPrimary or
TargetSessionAttributes.ReadWrite or
TargetSessionAttributes.PreferStandby
=> true,

DatabaseState.PrimaryReadOnly when preferredType is
TargetSessionAttributes.Primary or
TargetSessionAttributes.PreferPrimary or
TargetSessionAttributes.ReadOnly or
TargetSessionAttributes.PreferStandby
=> true,

DatabaseState.Standby when preferredType is
TargetSessionAttributes.Standby or
TargetSessionAttributes.PreferStandby or
TargetSessionAttributes.ReadOnly or
TargetSessionAttributes.PreferPrimary
=> true,

_ => preferredType == TargetSessionAttributes.Any
};

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 : IsUnpreferred;

var pools = _pools;
for (var i = 0; i < pools.Length; i++)
{
Expand All @@ -183,7 +212,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 +230,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 +264,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 : IsUnpreferred;

var pools = _pools;
for (var i = 0; i < pools.Length; i++)
{
Expand All @@ -257,11 +288,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 +330,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 @@ -421,7 +447,7 @@ internal override bool TryRentEnlistedPending(

// Can't get valid preferred connector. Try to get an unpreferred connector, if supported.
if ((preferredType == TargetSessionAttributes.PreferPrimary || preferredType == TargetSessionAttributes.PreferStandby)
&& TryGetValidConnector(list, preferredType, IsOnline, out connector))
&& TryGetValidConnector(list, preferredType, IsUnpreferred, out connector))
{
return true;
}
Expand All @@ -437,7 +463,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
4 changes: 2 additions & 2 deletions test/Npgsql.Tests/MultipleHostsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -878,12 +878,12 @@ await firstServer
await secondServer.SendMockState(Primary);

await Task.Delay(TimeSpan.FromSeconds(10));
Assert.That(firstDataSource.GetDatabaseState(), Is.EqualTo(DatabaseState.Unknown));
Assert.That(firstDataSource.GetDatabaseState(), Is.EqualTo(DatabaseState.UnknownAfterError));
Assert.That(secondDataSource.GetDatabaseState(), Is.EqualTo(DatabaseState.Unknown));

await conn.OpenAsync();
Assert.That(conn.Port, Is.EqualTo(secondPostmaster.Port));
Assert.That(firstDataSource.GetDatabaseState(), Is.EqualTo(DatabaseState.Standby));
Assert.That(firstDataSource.GetDatabaseState(), Is.EqualTo(DatabaseState.UnknownAfterError));
Assert.That(secondDataSource.GetDatabaseState(), Is.EqualTo(DatabaseState.PrimaryReadWrite));
}

Expand Down