Skip to content

Commit 4482112

Browse files
authored
Fixes to multi-host data source support (#4591)
* Allow BuildMultiHost with one host and Build with multple. * Add AddNpgsqlMultiHostDatasource to Npgsql.DependencyInjection. * All tests which use pg_terminate_backend are now marked as NonParallelizable and reset the cluster state cache, to prevent interference with multihost tests. Closes #4578
1 parent 48060e8 commit 4482112

11 files changed

Lines changed: 221 additions & 80 deletions

File tree

src/Npgsql.DependencyInjection/NpgsqlServiceCollectionExtensions.cs

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace Microsoft.Extensions.DependencyInjection;
1313
public static class NpgsqlServiceCollectionExtensions
1414
{
1515
/// <summary>
16-
/// Registers an <see cref="NpgsqlDataSource" /> and an <see cref="NpgsqlConnection" /> in the <see cref="IServiceCollection" />,
16+
/// Registers an <see cref="NpgsqlDataSource" /> and an <see cref="NpgsqlConnection" /> in the <see cref="IServiceCollection" />.
1717
/// </summary>
1818
/// <param name="serviceCollection">The <see cref="IServiceCollection" /> to add services to.</param>
1919
/// <param name="connectionString">An Npgsql connection string.</param>
@@ -38,7 +38,7 @@ public static IServiceCollection AddNpgsqlDataSource(
3838
=> AddNpgsqlDataSourceCore(serviceCollection, connectionString, dataSourceBuilderAction, connectionLifetime, dataSourceLifetime);
3939

4040
/// <summary>
41-
/// Registers an <see cref="NpgsqlDataSource" /> and an <see cref="NpgsqlConnection" /> in the <see cref="IServiceCollection" />,
41+
/// Registers an <see cref="NpgsqlDataSource" /> and an <see cref="NpgsqlConnection" /> in the <see cref="IServiceCollection" />.
4242
/// </summary>
4343
/// <param name="serviceCollection">The <see cref="IServiceCollection" /> to add services to.</param>
4444
/// <param name="connectionString">An Npgsql connection string.</param>
@@ -59,12 +59,61 @@ public static IServiceCollection AddNpgsqlDataSource(
5959
=> AddNpgsqlDataSourceCore(
6060
serviceCollection, connectionString, dataSourceBuilderAction: null, connectionLifetime, dataSourceLifetime);
6161

62-
static IServiceCollection AddNpgsqlDataSourceCore(
62+
/// <summary>
63+
/// Registers an <see cref="NpgsqlMultiHostDataSource" /> and an <see cref="NpgsqlConnection" /> in the
64+
/// </summary>
65+
/// <param name="serviceCollection">The <see cref="IServiceCollection" /> to add services to.</param>
66+
/// <param name="connectionString">An Npgsql connection string.</param>
67+
/// <param name="dataSourceBuilderAction">
68+
/// An action to configure the <see cref="NpgsqlDataSourceBuilder" /> for further customizations of the <see cref="NpgsqlDataSource" />.
69+
/// </param>
70+
/// <param name="connectionLifetime">
71+
/// The lifetime with which to register the <see cref="NpgsqlConnection" /> in the container.
72+
/// Defaults to <see cref="ServiceLifetime.Scoped" />.
73+
/// </param>
74+
/// <param name="dataSourceLifetime">
75+
/// The lifetime with which to register the <see cref="NpgsqlDataSource" /> service in the container.
76+
/// Defaults to <see cref="ServiceLifetime.Singleton" />.
77+
/// </param>
78+
/// <returns>The same service collection so that multiple calls can be chained.</returns>
79+
public static IServiceCollection AddMultiHostNpgsqlDataSource(
80+
this IServiceCollection serviceCollection,
81+
string connectionString,
82+
Action<NpgsqlDataSourceBuilder> dataSourceBuilderAction,
83+
ServiceLifetime connectionLifetime = ServiceLifetime.Transient,
84+
ServiceLifetime dataSourceLifetime = ServiceLifetime.Singleton)
85+
=> AddNpgsqlMultiHostDataSourceCore(
86+
serviceCollection, connectionString, dataSourceBuilderAction, connectionLifetime, dataSourceLifetime);
87+
88+
/// <summary>
89+
/// Registers an <see cref="NpgsqlMultiHostDataSource" /> and an <see cref="NpgsqlConnection" /> in the
90+
/// <see cref="IServiceCollection" />.
91+
/// </summary>
92+
/// <param name="serviceCollection">The <see cref="IServiceCollection" /> to add services to.</param>
93+
/// <param name="connectionString">An Npgsql connection string.</param>
94+
/// <param name="connectionLifetime">
95+
/// The lifetime with which to register the <see cref="NpgsqlConnection" /> in the container.
96+
/// Defaults to <see cref="ServiceLifetime.Scoped" />.
97+
/// </param>
98+
/// <param name="dataSourceLifetime">
99+
/// The lifetime with which to register the <see cref="NpgsqlDataSource" /> service in the container.
100+
/// Defaults to <see cref="ServiceLifetime.Singleton" />.
101+
/// </param>
102+
/// <returns>The same service collection so that multiple calls can be chained.</returns>
103+
public static IServiceCollection AddMultiHostNpgsqlDataSource(
63104
this IServiceCollection serviceCollection,
64105
string connectionString,
65-
Action<NpgsqlDataSourceBuilder>? dataSourceBuilderAction,
66106
ServiceLifetime connectionLifetime = ServiceLifetime.Transient,
67107
ServiceLifetime dataSourceLifetime = ServiceLifetime.Singleton)
108+
=> AddNpgsqlMultiHostDataSourceCore(
109+
serviceCollection, connectionString, dataSourceBuilderAction: null, connectionLifetime, dataSourceLifetime);
110+
111+
static IServiceCollection AddNpgsqlDataSourceCore(
112+
this IServiceCollection serviceCollection,
113+
string connectionString,
114+
Action<NpgsqlDataSourceBuilder>? dataSourceBuilderAction,
115+
ServiceLifetime connectionLifetime,
116+
ServiceLifetime dataSourceLifetime)
68117
{
69118
serviceCollection.TryAdd(
70119
new ServiceDescriptor(
@@ -78,6 +127,46 @@ static IServiceCollection AddNpgsqlDataSourceCore(
78127
},
79128
dataSourceLifetime));
80129

130+
AddCommonServices(serviceCollection, connectionLifetime, dataSourceLifetime);
131+
132+
return serviceCollection;
133+
}
134+
135+
static IServiceCollection AddNpgsqlMultiHostDataSourceCore(
136+
this IServiceCollection serviceCollection,
137+
string connectionString,
138+
Action<NpgsqlDataSourceBuilder>? dataSourceBuilderAction,
139+
ServiceLifetime connectionLifetime,
140+
ServiceLifetime dataSourceLifetime)
141+
{
142+
serviceCollection.TryAdd(
143+
new ServiceDescriptor(
144+
typeof(NpgsqlMultiHostDataSource),
145+
sp =>
146+
{
147+
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
148+
dataSourceBuilder.UseLoggerFactory(sp.GetService<ILoggerFactory>());
149+
dataSourceBuilderAction?.Invoke(dataSourceBuilder);
150+
return dataSourceBuilder.BuildMultiHost();
151+
},
152+
dataSourceLifetime));
153+
154+
serviceCollection.TryAdd(
155+
new ServiceDescriptor(
156+
typeof(NpgsqlDataSource),
157+
sp => sp.GetRequiredService<NpgsqlMultiHostDataSource>(),
158+
dataSourceLifetime));
159+
160+
AddCommonServices(serviceCollection, connectionLifetime, dataSourceLifetime);
161+
162+
return serviceCollection;
163+
}
164+
165+
static void AddCommonServices(
166+
IServiceCollection serviceCollection,
167+
ServiceLifetime connectionLifetime,
168+
ServiceLifetime dataSourceLifetime)
169+
{
81170
serviceCollection.TryAdd(
82171
new ServiceDescriptor(
83172
typeof(NpgsqlConnection),
@@ -95,7 +184,5 @@ static IServiceCollection AddNpgsqlDataSourceCore(
95184
typeof(DbConnection),
96185
sp => sp.GetRequiredService<NpgsqlConnection>(),
97186
connectionLifetime));
98-
99-
return serviceCollection;
100187
}
101188
}

src/Npgsql/NpgsqlDataSourceBuilder.cs

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,36 @@ public NpgsqlDataSourceBuilder UsePhysicalConnectionInitializer(
337337
/// Builds and returns an <see cref="NpgsqlDataSource" /> which is ready for use.
338338
/// </summary>
339339
public NpgsqlDataSource Build()
340+
{
341+
var config = PrepareConfiguration();
342+
343+
if (ConnectionStringBuilder.Host!.Contains(","))
344+
{
345+
ValidateMultiHost();
346+
347+
return new NpgsqlMultiHostDataSource(ConnectionStringBuilder, config);
348+
}
349+
350+
return ConnectionStringBuilder.Multiplexing
351+
? new MultiplexingDataSource(ConnectionStringBuilder, config)
352+
: ConnectionStringBuilder.Pooling
353+
? new PoolingDataSource(ConnectionStringBuilder, config)
354+
: new UnpooledDataSource(ConnectionStringBuilder, config);
355+
}
356+
357+
/// <summary>
358+
/// Builds and returns a <see cref="NpgsqlMultiHostDataSource" /> which is ready for use for load-balancing and failover scenarios.
359+
/// </summary>
360+
public NpgsqlMultiHostDataSource BuildMultiHost()
361+
{
362+
var config = PrepareConfiguration();
363+
364+
ValidateMultiHost();
365+
366+
return new(ConnectionStringBuilder, config);
367+
}
368+
369+
NpgsqlDataSourceConfiguration PrepareConfiguration()
340370
{
341371
ConnectionStringBuilder.PostProcessAndValidate();
342372

@@ -346,12 +376,10 @@ public NpgsqlDataSource Build()
346376
throw new NotSupportedException(NpgsqlStrings.CannotSetBothPasswordProviderAndPassword);
347377
}
348378

349-
var loggingConfiguration = _loggerFactory is null
350-
? NpgsqlLoggingConfiguration.NullConfiguration
351-
: new NpgsqlLoggingConfiguration(_loggerFactory, _sensitiveDataLoggingEnabled);
352-
353-
var config = new NpgsqlDataSourceConfiguration(
354-
loggingConfiguration,
379+
return new(
380+
_loggerFactory is null
381+
? NpgsqlLoggingConfiguration.NullConfiguration
382+
: new NpgsqlLoggingConfiguration(_loggerFactory, _sensitiveDataLoggingEnabled),
355383
_userCertificateValidationCallback,
356384
_clientCertificatesCallback,
357385
_periodicPasswordProvider,
@@ -362,31 +390,15 @@ public NpgsqlDataSource Build()
362390
DefaultNameTranslator,
363391
_syncConnectionInitializer,
364392
_asyncConnectionInitializer);
365-
366-
if (ConnectionStringBuilder.Host!.Contains(","))
367-
{
368-
if (ConnectionStringBuilder.TargetSessionAttributes is not null)
369-
throw new InvalidOperationException(NpgsqlStrings.CannotSpecifyTargetSessionAttributes);
370-
if (ConnectionStringBuilder.Multiplexing)
371-
throw new NotSupportedException("Multiplexing is not supported with multiple hosts");
372-
if (ConnectionStringBuilder.ReplicationMode != ReplicationMode.Off)
373-
throw new NotSupportedException("Replication is not supported with multiple hosts");
374-
375-
return new NpgsqlMultiHostDataSource(ConnectionStringBuilder, config);
376-
}
377-
378-
return ConnectionStringBuilder.Multiplexing
379-
? new MultiplexingDataSource(ConnectionStringBuilder, config)
380-
: ConnectionStringBuilder.Pooling
381-
? new PoolingDataSource(ConnectionStringBuilder, config)
382-
: new UnpooledDataSource(ConnectionStringBuilder, config);
383393
}
384394

385-
#pragma warning disable RS0016
386-
/// <summary>
387-
/// Builds and returns a <see cref="NpgsqlMultiHostDataSource" /> which is ready for use for load-balancing and failover scenarios.
388-
/// </summary>
389-
public NpgsqlMultiHostDataSource BuildMultiHost()
390-
=> Build() as NpgsqlMultiHostDataSource ?? throw new InvalidOperationException(NpgsqlStrings.MultipleHostsMustBeSpecified);
391-
#pragma warning restore RS0016
395+
void ValidateMultiHost()
396+
{
397+
if (ConnectionStringBuilder.TargetSessionAttributes is not null)
398+
throw new InvalidOperationException(NpgsqlStrings.CannotSpecifyTargetSessionAttributes);
399+
if (ConnectionStringBuilder.Multiplexing)
400+
throw new NotSupportedException("Multiplexing is not supported with multiple hosts");
401+
if (ConnectionStringBuilder.ReplicationMode != ReplicationMode.Off)
402+
throw new NotSupportedException("Replication is not supported with multiple hosts");
403+
}
392404
}

src/Npgsql/NpgsqlMultiHostDataSource.cs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,25 @@ static bool IsPreferred(DatabaseState state, TargetSessionAttributes preferredTy
125125
{
126126
DatabaseState.Offline => false,
127127
DatabaseState.Unknown => true, // We will check compatibility again after refreshing the database state
128-
DatabaseState.PrimaryReadWrite when preferredType is TargetSessionAttributes.Primary or TargetSessionAttributes.PreferPrimary or TargetSessionAttributes.ReadWrite => true,
129-
DatabaseState.PrimaryReadOnly when preferredType is TargetSessionAttributes.Primary or TargetSessionAttributes.PreferPrimary or TargetSessionAttributes.ReadOnly => true,
130-
DatabaseState.Standby when preferredType is TargetSessionAttributes.Standby or TargetSessionAttributes.PreferStandby or TargetSessionAttributes.ReadOnly => true,
128+
129+
DatabaseState.PrimaryReadWrite when preferredType is
130+
TargetSessionAttributes.Primary or
131+
TargetSessionAttributes.PreferPrimary or
132+
TargetSessionAttributes.ReadWrite
133+
=> true,
134+
135+
DatabaseState.PrimaryReadOnly when preferredType is
136+
TargetSessionAttributes.Primary or
137+
TargetSessionAttributes.PreferPrimary or
138+
TargetSessionAttributes.ReadOnly
139+
=> true,
140+
141+
DatabaseState.Standby when preferredType is
142+
TargetSessionAttributes.Standby or
143+
TargetSessionAttributes.PreferStandby or
144+
TargetSessionAttributes.ReadOnly
145+
=> true,
146+
131147
_ => preferredType == TargetSessionAttributes.Any
132148
};
133149

@@ -280,9 +296,7 @@ internal override async ValueTask<NpgsqlConnector> Get(
280296

281297
var timeoutPerHost = timeout.IsSet ? timeout.CheckAndGetTimeLeft() : TimeSpan.Zero;
282298
var preferredType = GetTargetSessionAttributes(conn);
283-
var checkUnpreferred =
284-
preferredType == TargetSessionAttributes.PreferPrimary ||
285-
preferredType == TargetSessionAttributes.PreferStandby;
299+
var checkUnpreferred = preferredType is TargetSessionAttributes.PreferPrimary or TargetSessionAttributes.PreferStandby;
286300

287301
var connector = await TryGetIdleOrNew(conn, timeoutPerHost, async, preferredType, IsPreferred, poolIndex, exceptions, cancellationToken) ??
288302
(checkUnpreferred ?
@@ -293,10 +307,7 @@ await TryGet(conn, timeoutPerHost, async, preferredType, IsPreferred, poolIndex,
293307
await TryGet(conn, timeoutPerHost, async, preferredType, IsOnline, poolIndex, exceptions, cancellationToken)
294308
: null);
295309

296-
if (connector is not null)
297-
return connector;
298-
299-
throw NoSuitableHostsException(exceptions);
310+
return connector ?? throw NoSuitableHostsException(exceptions);
300311
}
301312

302313
static NpgsqlException NoSuitableHostsException(IList<Exception> exceptions)

src/Npgsql/Properties/NpgsqlStrings.Designer.cs

Lines changed: 0 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Npgsql/Properties/NpgsqlStrings.resx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,6 @@
4848
<data name="ArgumentMustBePositive" xml:space="preserve">
4949
<value>'{0}' must be positive.</value>
5050
</data>
51-
<data name="MultipleHostsMustBeSpecified" xml:space="preserve">
52-
<value>Multiple hosts must be specified.</value>
53-
</data>
5451
<data name="CannotSpecifyTargetSessionAttributes" xml:space="preserve">
5552
<value>When creating a multi-host data source, TargetSessionAttributes cannot be specified. Create without TargetSessionAttributes, and then obtain DataSource wrappers from it. Consult the docs for more information.</value>
5653
</data>

src/Npgsql/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ Npgsql.NpgsqlDataSourceBuilder.ConnectionString.get -> string!
296296
Npgsql.NpgsqlDataSourceBuilder.ConnectionStringBuilder.get -> Npgsql.NpgsqlConnectionStringBuilder!
297297
Npgsql.NpgsqlDataSourceBuilder.EnableParameterLogging(bool parameterLoggingEnabled = true) -> Npgsql.NpgsqlDataSourceBuilder!
298298
Npgsql.NpgsqlDataSourceBuilder.Build() -> Npgsql.NpgsqlDataSource!
299+
Npgsql.NpgsqlDataSourceBuilder.BuildMultiHost() -> Npgsql.NpgsqlMultiHostDataSource!
299300
Npgsql.NpgsqlDataSourceBuilder.NpgsqlDataSourceBuilder(string? connectionString = null) -> void
300301
Npgsql.NpgsqlDataSourceBuilder.UsePeriodicPasswordProvider(System.Func<Npgsql.NpgsqlConnectionStringBuilder!, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<string!>>? passwordProvider, System.TimeSpan successRefreshInterval, System.TimeSpan failureRefreshInterval) -> Npgsql.NpgsqlDataSourceBuilder!
301302
Npgsql.NpgsqlDataSourceBuilder.UseLoggerFactory(Microsoft.Extensions.Logging.ILoggerFactory? loggerFactory) -> Npgsql.NpgsqlDataSourceBuilder!

test/Npgsql.DependencyInjection.Tests/DependencyInjectionTests.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,23 @@ public async Task NpgsqlDataSource_is_registered_properly([Values] bool async)
2525
: dataSource.OpenConnection();
2626
}
2727

28+
[Test]
29+
public async Task NpgsqlMultiHostDataSource_is_registered_properly([Values] bool async)
30+
{
31+
var serviceCollection = new ServiceCollection();
32+
serviceCollection.AddMultiHostNpgsqlDataSource(TestUtil.ConnectionString);
33+
34+
await using var serviceProvider = serviceCollection.BuildServiceProvider();
35+
var multiHostDataSource = serviceProvider.GetRequiredService<NpgsqlMultiHostDataSource>();
36+
var dataSource = serviceProvider.GetRequiredService<NpgsqlDataSource>();
37+
38+
Assert.That(dataSource, Is.SameAs(multiHostDataSource));
39+
40+
await using var connection = async
41+
? await dataSource.OpenConnectionAsync()
42+
: dataSource.OpenConnection();
43+
}
44+
2845
[Test]
2946
public void NpgsqlDataSource_is_registered_as_singleton_by_default()
3047
{

0 commit comments

Comments
 (0)