Centralize Postgres connection string policy across all modules

Extract connection string building into PostgresConnectionStringPolicy so all
services use consistent pooling, application_name, and timeout settings.
Adopt the new policy in 20+ module DataSource/ServiceCollectionExtensions classes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-06 08:51:04 +03:00
parent 517fa0a92d
commit ccdfd41e4f
64 changed files with 625 additions and 178 deletions

View File

@@ -20,7 +20,7 @@ namespace StellaOps.Scanner.Reachability.Cache;
/// </summary>
public sealed class PostgresReachabilityCache : IReachabilityCache
{
private readonly string _connectionString;
private readonly Services.PostgresReachabilityDataSourceProvider _dataSourceProvider;
private readonly ILogger<PostgresReachabilityCache> _logger;
private readonly TimeProvider _timeProvider;
@@ -28,8 +28,16 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
string connectionString,
ILogger<PostgresReachabilityCache> logger,
TimeProvider? timeProvider = null)
: this(new Services.PostgresReachabilityDataSourceProvider(connectionString), logger, timeProvider)
{
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
}
internal PostgresReachabilityCache(
Services.PostgresReachabilityDataSourceProvider dataSourceProvider,
ILogger<PostgresReachabilityCache> logger,
TimeProvider? timeProvider = null)
{
_dataSourceProvider = dataSourceProvider ?? throw new ArgumentNullException(nameof(dataSourceProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
}
@@ -40,8 +48,7 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
string graphHash,
CancellationToken cancellationToken = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(cancellationToken);
await using var conn = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
// Get cache entry
const string entrySql = """
@@ -120,8 +127,7 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
{
ArgumentNullException.ThrowIfNull(entry);
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(cancellationToken);
await using var conn = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var tx = await conn.BeginTransactionAsync(cancellationToken);
try
@@ -202,8 +208,7 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
string sinkMethodKey,
CancellationToken cancellationToken = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(cancellationToken);
await using var conn = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
const string sql = """
SELECT p.is_reachable, p.path_length, p.confidence, p.computed_at
@@ -246,8 +251,7 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
IEnumerable<string> affectedMethodKeys,
CancellationToken cancellationToken = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(cancellationToken);
await using var conn = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
// For now, invalidate entire cache for service
// More granular invalidation would require additional indices
@@ -283,8 +287,7 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
string serviceId,
CancellationToken cancellationToken = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(cancellationToken);
await using var conn = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
const string sql = """
SELECT total_hits, total_misses, full_recomputes, incremental_computes,
@@ -392,4 +395,7 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
await cmd.ExecuteNonQueryAsync(cancellationToken);
}
private ValueTask<NpgsqlConnection> OpenConnectionAsync(CancellationToken cancellationToken)
=> _dataSourceProvider.OpenConnectionAsync(cancellationToken);
}

View File

@@ -34,10 +34,12 @@ public static class ServiceCollectionExtensions
ArgumentNullException.ThrowIfNull(services);
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
services.TryAddSingleton(_ => new PostgresReachabilityDataSourceProvider(connectionString));
// CVE-Symbol Mapping Service
services.TryAddSingleton<ICveSymbolMappingService>(sp =>
new PostgresCveSymbolMappingRepository(
connectionString,
sp.GetRequiredService<PostgresReachabilityDataSourceProvider>(),
sp.GetRequiredService<Microsoft.Extensions.Logging.ILogger<PostgresCveSymbolMappingRepository>>()));
// Stack Evaluator (already exists, ensure registered)

View File

@@ -15,14 +15,21 @@ namespace StellaOps.Scanner.Reachability.Services;
/// </summary>
public sealed class PostgresCveSymbolMappingRepository : ICveSymbolMappingService
{
private readonly string _connectionString;
private readonly PostgresReachabilityDataSourceProvider _dataSourceProvider;
private readonly ILogger<PostgresCveSymbolMappingRepository> _logger;
public PostgresCveSymbolMappingRepository(
string connectionString,
ILogger<PostgresCveSymbolMappingRepository> logger)
: this(new PostgresReachabilityDataSourceProvider(connectionString), logger)
{
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
}
internal PostgresCveSymbolMappingRepository(
PostgresReachabilityDataSourceProvider dataSourceProvider,
ILogger<PostgresCveSymbolMappingRepository> logger)
{
_dataSourceProvider = dataSourceProvider ?? throw new ArgumentNullException(nameof(dataSourceProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -206,9 +213,7 @@ public sealed class PostgresCveSymbolMappingRepository : ICveSymbolMappingServic
private async Task<NpgsqlConnection> OpenConnectionAsync(CancellationToken ct)
{
var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(ct);
return conn;
return await _dataSourceProvider.OpenConnectionAsync(ct).ConfigureAwait(false);
}
private static CveSinkMapping MapFromReader(NpgsqlDataReader reader)

View File

@@ -0,0 +1,37 @@
using Npgsql;
namespace StellaOps.Scanner.Reachability.Services;
internal sealed class PostgresReachabilityDataSourceProvider : IAsyncDisposable
{
private readonly Lazy<NpgsqlDataSource> _dataSource;
public PostgresReachabilityDataSourceProvider(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
_dataSource = new Lazy<NpgsqlDataSource>(
() => CreateDataSource(connectionString),
isThreadSafe: true);
}
public ValueTask<NpgsqlConnection> OpenConnectionAsync(CancellationToken cancellationToken)
=> _dataSource.Value.OpenConnectionAsync(cancellationToken);
public async ValueTask DisposeAsync()
{
if (_dataSource.IsValueCreated)
{
await _dataSource.Value.DisposeAsync().ConfigureAwait(false);
}
}
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-scanner-reachability",
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
}

View File

@@ -4,6 +4,7 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: added a named reachability datasource provider and routed PostgreSQL-backed reachability services through it. |
| QA-SCANNER-VERIFY-005 | DONE | SPRINT_20260212_002 run-001: `api-gateway-boundary-extractor` passed Tier 0/1/2 and moved to `docs/features/checked/scanner/`. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |