Resolve all high-severity architecture review findings
This commit is contained in:
+10
-1
@@ -181,8 +181,16 @@ VALUES
|
||||
@LastError, @HttpStatus, @CreatedAtUtc, @UpdatedAtUtc, @TerminalAtUtc, @IngestedAtUtc);";
|
||||
|
||||
// Leg 2 of the upsert: the monotonic guard. Also re-run standalone from the
|
||||
// duplicate-key catch, which is why it lives in its own constant.
|
||||
// duplicate-key catch, which is why it lives in its own constant. Ownership
|
||||
// is checked here as well as in the UPDATE predicate: an insert-race loser
|
||||
// must not advance another site's operation. Throwing rolls back the paired
|
||||
// audit/operational ingest transaction instead of acknowledging forged state.
|
||||
private const string MonotonicUpdateSql = @"
|
||||
IF EXISTS (SELECT 1 FROM dbo.SiteCalls
|
||||
WHERE TrackedOperationId = @Id
|
||||
AND SourceSite COLLATE Latin1_General_100_BIN2 <> @SourceSite COLLATE Latin1_General_100_BIN2)
|
||||
THROW 51001, 'Site-call operation ownership denied.', 1;
|
||||
|
||||
UPDATE dbo.SiteCalls
|
||||
SET Status = @Status,
|
||||
RetryCount = @RetryCount,
|
||||
@@ -193,6 +201,7 @@ SET Status = @Status,
|
||||
IngestedAtUtc = @IngestedAtUtc,
|
||||
SourceNode = COALESCE(@SourceNode, SourceNode)
|
||||
WHERE TrackedOperationId = @Id
|
||||
AND SourceSite COLLATE Latin1_General_100_BIN2 = @SourceSite COLLATE Latin1_General_100_BIN2
|
||||
AND ( @Rank > (CASE Status
|
||||
WHEN 'Submitted' THEN 0
|
||||
WHEN 'Forwarded' THEN 1
|
||||
|
||||
@@ -74,6 +74,10 @@ public static class ServiceCollectionExtensions
|
||||
// registration of the same interface is idempotent.
|
||||
services.TryAddSingleton<ITemplateGraphWatermark, TemplateGraphWatermark>();
|
||||
|
||||
services.AddSingleton<IConfigurationCommandTransaction, ConfigurationCommandTransaction>();
|
||||
services.AddScoped<ConfigurationCommitActions>();
|
||||
services.AddScoped<IConfigurationCommitActions>(sp => sp.GetRequiredService<ConfigurationCommitActions>());
|
||||
services.AddScoped<IConfigurationReadSession, ConfigurationReadSession>();
|
||||
services.AddScoped<ISecurityRepository, SecurityRepository>();
|
||||
services.AddScoped<ICentralUiRepository, CentralUiRepository>();
|
||||
services.AddScoped<ITemplateEngineRepository, TemplateEngineRepository>();
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Services;
|
||||
|
||||
/// <summary>Owns the SQL transaction spanning configuration saves and the final audit flush.</summary>
|
||||
public sealed class ConfigurationCommandTransaction(IServiceScopeFactory scopes) : IConfigurationCommandTransaction
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<object?> ExecuteAsync(Func<IServiceProvider, Task<object?>> operation)
|
||||
{
|
||||
using var strategyScope = scopes.CreateScope();
|
||||
var strategy = strategyScope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>().Database.CreateExecutionStrategy();
|
||||
var commitStarted = false;
|
||||
var result = await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
// A lost commit acknowledgment must never automatically replay a create.
|
||||
if (commitStarted) throw new InvalidOperationException("Configuration commit outcome is uncertain; inspect the configuration audit before retrying.");
|
||||
await using var attempt = scopes.CreateAsyncScope();
|
||||
var db = attempt.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
||||
await using var transaction = await db.Database.BeginTransactionAsync();
|
||||
var value = await operation(attempt.ServiceProvider);
|
||||
await db.SaveChangesAsync(); // AuditService stages rows; flush them inside this transaction.
|
||||
commitStarted = true;
|
||||
await transaction.CommitAsync();
|
||||
await attempt.ServiceProvider.GetRequiredService<ConfigurationCommitActions>().RunAsync();
|
||||
return value;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Per-attempt postcommit actions; rolled-back attempts never run them.</summary>
|
||||
public sealed class ConfigurationCommitActions(ILogger<ConfigurationCommitActions> logger) : IConfigurationCommitActions
|
||||
{
|
||||
private readonly List<Func<Task>> _actions = [];
|
||||
/// <inheritdoc />
|
||||
public void Add(Func<Task> action) => _actions.Add(action);
|
||||
internal async Task RunAsync()
|
||||
{
|
||||
foreach (var action in _actions)
|
||||
{
|
||||
try { await action(); }
|
||||
catch (Exception ex) { logger.LogError(ex, "Configuration committed, but local cache maintenance failed"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Refreshes circuit-held read entities after a command owned by another scope.</summary>
|
||||
public sealed class ConfigurationReadSession(ScadaBridgeDbContext db) : IConfigurationReadSession
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Reset() => db.ChangeTracker.Clear();
|
||||
}
|
||||
Reference in New Issue
Block a user