Resolve all high-severity architecture review findings

This commit is contained in:
Joseph Doherty
2026-09-09 16:13:58 -04:00
parent 8bfe65512e
commit 146a13238f
192 changed files with 3160 additions and 753 deletions
@@ -58,6 +58,7 @@ internal sealed class AuditingDbCommand : DbCommand
private readonly ILogger _logger;
private DbConnection? _wrappingConnection;
private DbTransaction? _wrappingTransaction;
/// <summary>
/// Initializes a new auditing database command decorator.
@@ -171,8 +172,13 @@ internal sealed class AuditingDbCommand : DbCommand
/// <inheritdoc />
protected override DbTransaction? DbTransaction
{
get => _inner.Transaction;
set => _inner.Transaction = value;
get => _inner.Transaction is null ? null : _wrappingTransaction;
set
{
// Preserve the public wrapper, while giving the provider its own transaction.
_inner.Transaction = value is AuditingDbTransaction auditing ? auditing.Inner : value;
_wrappingTransaction = value;
}
}
/// <inheritdoc />
@@ -17,8 +17,9 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
/// </summary>
/// <remarks>
/// <para>
/// All other <see cref="DbConnection"/> members forward to the inner connection
/// unchanged so the script keeps full ADO.NET semantics (transactions, state
/// Transactions and command connection readback retain this wrapper so they cannot
/// expose an unaudited provider connection. Other members forward to the inner connection
/// so the script keeps full ADO.NET semantics (transactions, state
/// transitions, server-version queries, etc.). Disposing the wrapper disposes
/// the inner connection — the caller is still responsible for disposal per
/// the <see cref="IDatabaseGateway"/> contract.
@@ -147,7 +148,13 @@ internal sealed class AuditingDbConnection : DbConnection
/// <inheritdoc />
protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel)
=> _inner.BeginTransaction(isolationLevel);
=> new AuditingDbTransaction(_inner.BeginTransaction(isolationLevel), this);
/// <inheritdoc />
protected override async ValueTask<DbTransaction> BeginDbTransactionAsync(
IsolationLevel isolationLevel, CancellationToken cancellationToken)
=> new AuditingDbTransaction(
await _inner.BeginTransactionAsync(isolationLevel, cancellationToken).ConfigureAwait(false), this);
/// <inheritdoc />
protected override DbCommand CreateDbCommand()
@@ -168,7 +175,7 @@ internal sealed class AuditingDbConnection : DbConnection
// threaded alongside _executionId. Null for non-routed runs.
_parentExecutionId,
// Flow Debugger: the DbWrite rows ride the inline collector.
_onAudit);
_onAudit) { Connection = this };
}
/// <inheritdoc />
@@ -0,0 +1,70 @@
using System.Data;
using System.Data.Common;
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
/// <summary>
/// Keeps transaction-derived commands inside the auditing connection boundary.
/// Only the command decorator can unwrap the provider transaction for assignment.
/// </summary>
internal sealed class AuditingDbTransaction(DbTransaction inner, AuditingDbConnection connection) : DbTransaction
{
/// <summary>The provider transaction, for internal command assignment only.</summary>
internal DbTransaction Inner => inner;
/// <inheritdoc />
protected override DbConnection? DbConnection => inner.Connection is null ? null : connection;
/// <inheritdoc />
public override IsolationLevel IsolationLevel => inner.IsolationLevel;
/// <inheritdoc />
public override bool SupportsSavepoints => inner.SupportsSavepoints;
/// <inheritdoc />
public override void Commit() => inner.Commit();
/// <inheritdoc />
public override Task CommitAsync(CancellationToken cancellationToken = default) => inner.CommitAsync(cancellationToken);
/// <inheritdoc />
public override void Rollback() => inner.Rollback();
/// <inheritdoc />
public override Task RollbackAsync(CancellationToken cancellationToken = default) => inner.RollbackAsync(cancellationToken);
/// <inheritdoc />
public override void Save(string savepointName) => inner.Save(savepointName);
/// <inheritdoc />
public override Task SaveAsync(string savepointName, CancellationToken cancellationToken = default)
=> inner.SaveAsync(savepointName, cancellationToken);
/// <inheritdoc />
public override void Rollback(string savepointName) => inner.Rollback(savepointName);
/// <inheritdoc />
public override Task RollbackAsync(string savepointName, CancellationToken cancellationToken = default)
=> inner.RollbackAsync(savepointName, cancellationToken);
/// <inheritdoc />
public override void Release(string savepointName) => inner.Release(savepointName);
/// <inheritdoc />
public override Task ReleaseAsync(string savepointName, CancellationToken cancellationToken = default)
=> inner.ReleaseAsync(savepointName, cancellationToken);
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (disposing) inner.Dispose();
base.Dispose(disposing);
}
/// <inheritdoc />
public override ValueTask DisposeAsync()
{
GC.SuppressFinalize(this);
return inner.DisposeAsync();
}
}
@@ -11,7 +11,9 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
/// <para>
/// Thread-model note: the indexer get/set block synchronously
/// on the Instance Actor Ask (and, for data-connected attributes, the DCL
/// round-trip). This is safe because script bodies execute on the dedicated
/// round-trip). Runtime helper continuations use ConfigureAwait(false), so they
/// can complete even when every dedicated worker is blocked in an indexer.
/// Script bodies execute on the dedicated
/// <see cref="ScriptExecutionScheduler"/> threads, not the
/// shared <see cref="System.Threading.ThreadPool"/> — so a blocked accessor
/// cannot starve unrelated Akka dispatchers or HTTP request handling. The async
@@ -701,7 +701,7 @@ public class ScriptRuntimeContext
var request = new GetAttributeRequest(
correlationId, _instanceName, attributeName, DateTimeOffset.UtcNow);
var response = await _instanceActor.Ask<GetAttributeResponse>(request, _askTimeout);
var response = await _instanceActor.Ask<GetAttributeResponse>(request, _askTimeout).ConfigureAwait(false);
if (!response.Found)
{
@@ -762,7 +762,7 @@ public class ScriptRuntimeContext
public async Task<bool> WaitAttribute(
string name, string? targetValueEncoded, Func<object?, bool>? predicate, TimeSpan timeout,
bool requireGoodQuality = false)
=> (await WaitInternal(name, targetValueEncoded, predicate, timeout, requireGoodQuality)).Matched;
=> (await WaitInternal(name, targetValueEncoded, predicate, timeout, requireGoodQuality).ConfigureAwait(false)).Matched;
/// <summary>
/// WaitForAttribute (spec §3): the richer overload backing <c>Attributes.WaitForAsync</c>
@@ -780,7 +780,7 @@ public class ScriptRuntimeContext
string name, string? targetValueEncoded, Func<object?, bool>? predicate, TimeSpan timeout,
bool requireGoodQuality = false)
{
var r = await WaitInternal(name, targetValueEncoded, predicate, timeout, requireGoodQuality);
var r = await WaitInternal(name, targetValueEncoded, predicate, timeout, requireGoodQuality).ConfigureAwait(false);
return new WaitResult(r.Matched, r.Value, r.Quality, r.TimedOut);
}
@@ -816,7 +816,7 @@ public class ScriptRuntimeContext
try
{
response = await _instanceActor.Ask<WaitForAttributeResponse>(
req, timeout + _askTimeout, _scriptTimeoutToken);
req, timeout + _askTimeout, _scriptTimeoutToken).ConfigureAwait(false);
}
catch (AskTimeoutException)
{
@@ -911,7 +911,7 @@ public class ScriptRuntimeContext
{
// Ask — mutation serialized through the Instance Actor mailbox; the reply
// carries the device-write outcome for data-connected attributes.
response = await _instanceActor.Ask<SetStaticAttributeResponse>(command, _askTimeout);
response = await _instanceActor.Ask<SetStaticAttributeResponse>(command, _askTimeout).ConfigureAwait(false);
}
catch (Exception ex) when (sw is not null)
{
@@ -979,7 +979,7 @@ public class ScriptRuntimeContext
Guid? stepId = _isTraced ? Guid.NewGuid() : null;
using var step = stepId is { } batchStepId ? TraceStepScope.Enter(batchStepId) : null;
var batchResp = await _instanceActor.Ask<WriteAttributeBatchResponse>(
batchReq, TimeSpan.FromSeconds(35), _scriptTimeoutToken);
batchReq, TimeSpan.FromSeconds(35), _scriptTimeoutToken).ConfigureAwait(false);
if (sw is not null)
{
@@ -1003,7 +1003,7 @@ public class ScriptRuntimeContext
throw new InvalidOperationException(
$"WriteBatchAndWait write failed: {batchResp.ErrorMessage}");
return await WaitAttribute(responseAttr, responseEncoded, null, timeout);
return await WaitAttribute(responseAttr, responseEncoded, null, timeout).ConfigureAwait(false);
}
/// <summary>