fix(historian): QuestDB final-review fix wave (I-1, M-1, M-3, M-5, M-8, M-9)
- I-1: QuestDbConnection caps Npgsql Maximum Pool Size at 24 (DefaultMaxPoolSize) when the connection string does not set it; an explicit value is kept. The effective value is logged once at Information. - M-1: QuestDbTagCatalog.EnsureAsync waits for the tag table's sequenced commits and re-checks before inserting, so it no longer overwrites a provisioner row (unit/description) that is committed but not yet applied. Integration test reproduces the clobber against real QuestDB (falsified). - M-3: the pre-1970 drop Warning is rate-limited per tag (once per 10 min); DroppedPre1970 still counts every sample. - M-5: QuestDbHistorianProvider is IAsyncDisposable/IDisposable and disposes the shared Npgsql pool; Program.cs registers the provider through a factory and every seam resolves it from the container, so DI disposes it at shutdown. - M-8, M-9: docs fixes (HistoryUpdate transaction wording; folded duplicate HistoryUpdate paragraph). Pool-size docs updated for the new default cap. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
using System.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Npgsql;
|
||||
using NpgsqlTypes;
|
||||
|
||||
@@ -12,8 +14,15 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb;
|
||||
/// failures with <see cref="IsTransient"/> / <see cref="IsAlreadyExists"/>. Every QuestDB server error carries
|
||||
/// SQLSTATE <c>00000</c>, so classification is by message text, never by SQLSTATE.</para>
|
||||
/// </summary>
|
||||
internal sealed class QuestDbConnection : IAsyncDisposable
|
||||
internal sealed class QuestDbConnection : IAsyncDisposable, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Npgsql pool cap applied when the connection string does not set <c>Maximum Pool Size</c> itself. Npgsql's own
|
||||
/// default (100) exceeds QuestDB's default PG-wire connection limit (64), and the server's default read
|
||||
/// concurrency alone can reach 64; an explicit value in the connection string is always kept.
|
||||
/// </summary>
|
||||
internal const int DefaultMaxPoolSize = 24;
|
||||
|
||||
private static readonly long MinSafeTicks = 10; // DateTime.MinValue itself binds as -infinity
|
||||
private static readonly long MaxSafeTicks = DateTime.MaxValue.Ticks - DateTime.MaxValue.Ticks % 10; // MaxValue itself binds as +infinity
|
||||
|
||||
@@ -21,14 +30,40 @@ internal sealed class QuestDbConnection : IAsyncDisposable
|
||||
|
||||
/// <summary>Creates the connection helper; options must already be valid.</summary>
|
||||
/// <param name="options">Validated options.</param>
|
||||
public QuestDbConnection(QuestDbHistorianOptions options)
|
||||
/// <param name="log">Optional logger; the effective pool size is logged once at Information.</param>
|
||||
public QuestDbConnection(QuestDbHistorianOptions options, ILogger? log = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
if (!QuestDbHistorianOptions.IsValidPrefix(options.TablePrefix))
|
||||
throw new ArgumentException("Invalid table prefix.", nameof(options));
|
||||
TablePrefix = options.TablePrefix;
|
||||
CommandTimeoutSeconds = Math.Max(1, (int)Math.Ceiling(options.CommandTimeout.TotalSeconds));
|
||||
_dataSource = new NpgsqlDataSourceBuilder(options.ConnectionString).Build();
|
||||
var csb = new NpgsqlConnectionStringBuilder(options.ConnectionString);
|
||||
var explicitPool = HasExplicitMaxPoolSize(options.ConnectionString);
|
||||
if (!explicitPool) csb.MaxPoolSize = DefaultMaxPoolSize;
|
||||
EffectiveMaxPoolSize = csb.MaxPoolSize;
|
||||
(log ?? NullLogger.Instance).LogInformation(
|
||||
"QuestDB historian connection pool: Maximum Pool Size = {MaxPoolSize} ({Source}).",
|
||||
EffectiveMaxPoolSize, explicitPool ? "from the connection string" : "default cap");
|
||||
_dataSource = new NpgsqlDataSourceBuilder(csb.ConnectionString).Build();
|
||||
}
|
||||
|
||||
/// <summary>The Npgsql <c>Maximum Pool Size</c> in effect (the connection string's own value, else <see cref="DefaultMaxPoolSize"/>).</summary>
|
||||
internal int EffectiveMaxPoolSize { get; }
|
||||
|
||||
// Npgsql accepts "Maximum Pool Size" and "MaxPoolSize" (case-insensitive); a generic parse keeps the caller's own
|
||||
// keywords so "was it set" is answered by the string itself, not by the builder's default.
|
||||
private static bool HasExplicitMaxPoolSize(string connectionString)
|
||||
{
|
||||
var generic = new System.Data.Common.DbConnectionStringBuilder { ConnectionString = connectionString };
|
||||
foreach (string key in generic.Keys)
|
||||
{
|
||||
var k = key.Replace(" ", "", StringComparison.Ordinal);
|
||||
if (string.Equals(k, "MaxPoolSize", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(k, "MaximumPoolSize", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>The validated table prefix.</summary>
|
||||
@@ -70,6 +105,9 @@ internal sealed class QuestDbConnection : IAsyncDisposable
|
||||
/// <inheritdoc/>
|
||||
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose() => _dataSource.Dispose();
|
||||
|
||||
// ---- timestamps (R6) -------------------------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user