Skip to content

Commit b61d2b9

Browse files
committed
Cache database state on the data source
Instead of a global, static cluster state cache. Closes #4727
1 parent 317a452 commit b61d2b9

File tree

10 files changed

+319
-332
lines changed

10 files changed

+319
-332
lines changed

src/Npgsql/DatabaseState.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace Npgsql;
2+
3+
enum DatabaseState : byte
4+
{
5+
Unknown = 0,
6+
Offline = 1,
7+
PrimaryReadWrite = 2,
8+
PrimaryReadOnly = 3,
9+
Standby = 4
10+
}

src/Npgsql/Internal/ClusterStateCache.cs

Lines changed: 0 additions & 77 deletions
This file was deleted.

src/Npgsql/Internal/NpgsqlConnector.cs

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,7 @@ await OpenCore(
603603
}
604604
}
605605

606-
internal async ValueTask<ClusterState> QueryClusterState(
606+
internal async ValueTask<DatabaseState> QueryDatabaseState(
607607
NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken = default)
608608
{
609609
using var cmd = CreateCommand("select pg_is_in_recovery(); SHOW default_transaction_read_only");
@@ -629,9 +629,9 @@ internal async ValueTask<ClusterState> QueryClusterState(
629629

630630
_isTransactionReadOnly = reader.GetString(0) != "off";
631631

632-
var clusterState = UpdateClusterState();
633-
Debug.Assert(clusterState.HasValue);
634-
return clusterState.Value;
632+
var databaseState = UpdateDatabaseState();
633+
Debug.Assert(databaseState.HasValue);
634+
return databaseState.Value;
635635
}
636636
finally
637637
{
@@ -1934,8 +1934,7 @@ internal Exception Break(Exception reason)
19341934
(ne.InnerException is not TimeoutException || Settings.CancellationTimeout != -1) ||
19351935
reason is PostgresException pe && PostgresErrorCodes.IsCriticalFailure(pe))
19361936
{
1937-
ClusterStateCache.UpdateClusterState(Host, Port, ClusterState.Offline, DateTime.UtcNow,
1938-
Settings.HostRecheckSecondsTranslated);
1937+
DataSource.UpdateDatabaseState(DatabaseState.Offline, DateTime.UtcNow, Settings.HostRecheckSecondsTranslated);
19391938
DataSource.Clear();
19401939
}
19411940

@@ -2582,27 +2581,26 @@ void ReadParameterStatus(ReadOnlySpan<byte> incomingName, ReadOnlySpan<byte> inc
25822581

25832582
case "default_transaction_read_only":
25842583
_isTransactionReadOnly = value == "on";
2585-
UpdateClusterState();
2584+
UpdateDatabaseState();
25862585
return;
25872586

25882587
case "in_hot_standby":
25892588
_isHotStandBy = value == "on";
2590-
UpdateClusterState();
2589+
UpdateDatabaseState();
25912590
return;
25922591
}
25932592
}
25942593

2595-
ClusterState? UpdateClusterState()
2594+
DatabaseState? UpdateDatabaseState()
25962595
{
25972596
if (_isTransactionReadOnly.HasValue && _isHotStandBy.HasValue)
25982597
{
25992598
var state = _isHotStandBy.Value
2600-
? ClusterState.Standby
2599+
? DatabaseState.Standby
26012600
: _isTransactionReadOnly.Value
2602-
? ClusterState.PrimaryReadOnly
2603-
: ClusterState.PrimaryReadWrite;
2604-
return ClusterStateCache.UpdateClusterState(Settings.Host!, Settings.Port, state, DateTime.UtcNow,
2605-
Settings.HostRecheckSecondsTranslated);
2601+
? DatabaseState.PrimaryReadOnly
2602+
: DatabaseState.PrimaryReadWrite;
2603+
return DataSource.UpdateDatabaseState(state, DateTime.UtcNow, Settings.HostRecheckSecondsTranslated);
26062604
}
26072605

26082606
return null;

src/Npgsql/NpgsqlDataSource.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ public abstract class NpgsqlDataSource : DbDataSource
6161

6262
bool _isBootstrapped;
6363

64+
volatile DatabaseStateInfo _databaseStateInfo = new();
65+
6466
// Note that while the dictionary is protected by locking, we assume that the lists it contains don't need to be
6567
// (i.e. access to connectors of a specific transaction won't be concurrent)
6668
private protected readonly Dictionary<Transaction, List<NpgsqlConnector>> _pendingEnlistedConnectors
@@ -315,6 +317,39 @@ internal abstract ValueTask<NpgsqlConnector> Get(
315317

316318
internal abstract bool OwnsConnectors { get; }
317319

320+
#region Database state management
321+
322+
internal DatabaseState GetDatabaseState(bool ignoreExpiration = false)
323+
{
324+
Debug.Assert(this is not NpgsqlMultiHostDataSource);
325+
326+
var databaseStateInfo = _databaseStateInfo;
327+
328+
return ignoreExpiration || !databaseStateInfo.Timeout.HasExpired
329+
? databaseStateInfo.State
330+
: DatabaseState.Unknown;
331+
}
332+
333+
internal DatabaseState UpdateDatabaseState(
334+
DatabaseState newState,
335+
DateTime timeStamp,
336+
TimeSpan stateExpiration,
337+
bool ignoreTimeStamp = false)
338+
{
339+
Debug.Assert(this is not NpgsqlMultiHostDataSource);
340+
341+
var databaseStateInfo = _databaseStateInfo;
342+
343+
if (!ignoreTimeStamp && timeStamp <= databaseStateInfo.TimeStamp)
344+
return _databaseStateInfo.State;
345+
346+
_databaseStateInfo = new(newState, new NpgsqlTimeout(stateExpiration), timeStamp);
347+
348+
return newState;
349+
}
350+
351+
#endregion Database state management
352+
318353
#region Pending Enlisted Connections
319354

320355
internal virtual void AddPendingEnlistedConnector(NpgsqlConnector connector, Transaction transaction)
@@ -399,4 +434,17 @@ private protected void CheckDisposed()
399434
}
400435

401436
#endregion
437+
438+
class DatabaseStateInfo
439+
{
440+
internal readonly DatabaseState State;
441+
internal readonly NpgsqlTimeout Timeout;
442+
// While the TimeStamp is not strictly required, it does lower the risk of overwriting the current state with an old value
443+
internal readonly DateTime TimeStamp;
444+
445+
public DatabaseStateInfo() : this(default, default, default) {}
446+
447+
public DatabaseStateInfo(DatabaseState state, NpgsqlTimeout timeout, DateTime timeStamp)
448+
=> (State, Timeout, TimeStamp) = (state, timeout, timeStamp);
449+
}
402450
}

0 commit comments

Comments
 (0)