Resolve all 46 low-severity architecture review findings
This commit is contained in:
+2
-2
@@ -449,8 +449,8 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
|
||||
DateTime monthBoundary, TimeSpan? commandTimeout = null, CancellationToken ct = default) =>
|
||||
_inner.SwitchOutPartitionAsync(monthBoundary, commandTimeout, ct);
|
||||
public Task<long> PurgeChannelOlderThanAsync(
|
||||
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default) =>
|
||||
_inner.PurgeChannelOlderThanAsync(channel, threshold, batchSize, commandTimeout, ct);
|
||||
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default, int maxRows = 50_000) =>
|
||||
_inner.PurgeChannelOlderThanAsync(channel, threshold, batchSize, commandTimeout, ct, maxRows);
|
||||
public Task<IReadOnlyList<DateTime>> GetPartitionBoundariesOlderThanAsync(
|
||||
DateTime threshold, CancellationToken ct = default) =>
|
||||
_inner.GetPartitionBoundariesOlderThanAsync(threshold, ct);
|
||||
|
||||
@@ -411,7 +411,7 @@ public class AuditLogIngestActorTests : TestKit, IClassFixture<MsSqlMigrationFix
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<long> PurgeChannelOlderThanAsync(
|
||||
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default) =>
|
||||
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default, int maxRows = 50_000) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<long> BackfillSourceNodeAsync(
|
||||
@@ -470,8 +470,8 @@ public class AuditLogIngestActorTests : TestKit, IClassFixture<MsSqlMigrationFix
|
||||
_inner.SwitchOutPartitionAsync(monthBoundary, commandTimeout, ct);
|
||||
|
||||
public Task<long> PurgeChannelOlderThanAsync(
|
||||
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default) =>
|
||||
_inner.PurgeChannelOlderThanAsync(channel, threshold, batchSize, commandTimeout, ct);
|
||||
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default, int maxRows = 50_000) =>
|
||||
_inner.PurgeChannelOlderThanAsync(channel, threshold, batchSize, commandTimeout, ct, maxRows);
|
||||
|
||||
public Task<long> BackfillSourceNodeAsync(
|
||||
string sentinel, DateTime before, int batchSize, CancellationToken ct = default) =>
|
||||
|
||||
@@ -62,7 +62,7 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixture<MsSqlMigrationFixt
|
||||
// M5.5 (T3): records every per-channel purge call as
|
||||
// (channel, threshold, batchSize) so tests can assert which channels the
|
||||
// actor chose to purge and with what window.
|
||||
public List<(string Channel, DateTime Threshold, int BatchSize)> ChannelPurges { get; } = new();
|
||||
public List<(string Channel, DateTime Threshold, int BatchSize, int MaxRows)> ChannelPurges { get; } = new();
|
||||
public Func<string, long> RowsPerChannel { get; set; } = _ => 0L;
|
||||
|
||||
// The actor enumerator returns whichever list is configured here.
|
||||
@@ -96,9 +96,9 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixture<MsSqlMigrationFixt
|
||||
}
|
||||
|
||||
public Task<long> PurgeChannelOlderThanAsync(
|
||||
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default)
|
||||
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default, int maxRows = 50_000)
|
||||
{
|
||||
ChannelPurges.Add((channel, threshold, batchSize));
|
||||
ChannelPurges.Add((channel, threshold, batchSize, maxRows));
|
||||
ChannelTimeouts.Add(commandTimeout);
|
||||
return Task.FromResult(RowsPerChannel(channel));
|
||||
}
|
||||
@@ -479,6 +479,7 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixture<MsSqlMigrationFixt
|
||||
var repo = new RecordingRepo { Boundaries = new List<DateTime>() };
|
||||
var purgeOptions = FastTickOptions();
|
||||
purgeOptions.ChannelPurgeBatchSizeConfigured = 1234;
|
||||
purgeOptions.MaxPurgedRowsPerChannelPerTick = 4321;
|
||||
|
||||
// Build the options OUTSIDE the Props expression tree — a collection/dictionary
|
||||
// initializer is not legal inside an expression-tree lambda (CS8074).
|
||||
@@ -505,6 +506,7 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixture<MsSqlMigrationFixt
|
||||
|
||||
var purge = repo.ChannelPurges.First(p => p.Channel == "ApiOutbound");
|
||||
Assert.Equal(1234, purge.BatchSize);
|
||||
Assert.Equal(4321, purge.MaxRows);
|
||||
|
||||
var expected = DateTime.UtcNow - TimeSpan.FromDays(30);
|
||||
Assert.True(
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ public class CentralAuditWriteFailuresTests : TestKit
|
||||
public Task<long> SwitchOutPartitionAsync(DateTime monthBoundary, TimeSpan? commandTimeout = null, CancellationToken ct = default) =>
|
||||
Task.FromResult(0L);
|
||||
public Task<long> PurgeChannelOlderThanAsync(
|
||||
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default) =>
|
||||
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default, int maxRows = 50_000) =>
|
||||
Task.FromResult(0L);
|
||||
public Task<long> BackfillSourceNodeAsync(
|
||||
string sentinel, DateTime before, int batchSize, CancellationToken ct = default) =>
|
||||
|
||||
+4
-4
@@ -93,7 +93,7 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
Task.FromResult(0L);
|
||||
|
||||
public Task<long> PurgeChannelOlderThanAsync(
|
||||
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default) =>
|
||||
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default, int maxRows = 50_000) =>
|
||||
Task.FromResult(0L);
|
||||
|
||||
public Task<long> BackfillSourceNodeAsync(
|
||||
@@ -159,7 +159,7 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
Task.FromResult(0L);
|
||||
|
||||
public Task<long> PurgeChannelOlderThanAsync(
|
||||
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default) =>
|
||||
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default, int maxRows = 50_000) =>
|
||||
Task.FromResult(0L);
|
||||
|
||||
public Task<long> BackfillSourceNodeAsync(
|
||||
@@ -225,7 +225,7 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
Task.FromResult(0L);
|
||||
|
||||
public Task<long> PurgeChannelOlderThanAsync(
|
||||
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default) =>
|
||||
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default, int maxRows = 50_000) =>
|
||||
Task.FromResult(0L);
|
||||
|
||||
public Task<long> BackfillSourceNodeAsync(
|
||||
@@ -628,7 +628,7 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
|
||||
|
||||
public Task<long> PurgeChannelOlderThanAsync(
|
||||
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null,
|
||||
CancellationToken ct = default)
|
||||
CancellationToken ct = default, int maxRows = 50_000)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task<long> BackfillSourceNodeAsync(
|
||||
|
||||
+9
@@ -12,6 +12,15 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
|
||||
/// </summary>
|
||||
public class SiteAuditReconciliationOptionsValidatorTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(1, true)]
|
||||
[InlineData(2000, true)]
|
||||
[InlineData(2001, false)]
|
||||
[InlineData(int.MaxValue, false)]
|
||||
public void BatchSize_MatchesRpcCeiling(int size, bool valid)
|
||||
=> Assert.Equal(valid, new SiteAuditReconciliationOptionsValidator().Validate(null,
|
||||
new SiteAuditReconciliationOptions { BatchSize = size }).Succeeded);
|
||||
|
||||
[Fact]
|
||||
public void DefaultOptions_AreValid()
|
||||
{
|
||||
|
||||
@@ -128,7 +128,7 @@ public class SiteAuditRetentionServiceTests
|
||||
private sealed class RecordingSiteAuditQueue : ISiteAuditQueue
|
||||
{
|
||||
public Task<IReadOnlyList<AuditEvent>> ReadByExecutionIdsAsync(
|
||||
IReadOnlyCollection<Guid> executionIds, DateTime sinceUtc, int limit, CancellationToken ct = default)
|
||||
IReadOnlyCollection<Guid> executionIds, DateTime sinceUtc, int limit, CancellationToken ct = default, string? afterEventId = null)
|
||||
=> throw new NotSupportedException("not exercised by the retention service");
|
||||
|
||||
private readonly ConcurrentQueue<DateTime> _purgeCalls = new();
|
||||
|
||||
+68
@@ -1,3 +1,4 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.ScadaBridge.AuditLog.Site;
|
||||
@@ -32,6 +33,73 @@ public class SqliteAuditWriterReadByExecutionTests : IDisposable
|
||||
ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, kind, AuditStatus.Delivered,
|
||||
occurredAtUtc: at, executionId: exec, parentExecutionId: parent, sourceSiteId: "site-a");
|
||||
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(64)]
|
||||
public async Task TraceReadPlan_UsesExecutionIndexesInsteadOfRetainedStoreScan(int idCount)
|
||||
{
|
||||
await using var writer = CreateWriter();
|
||||
using var connection = new SqliteConnection($"Data Source={_dbPath}");
|
||||
await connection.OpenAsync();
|
||||
using var command = connection.CreateCommand();
|
||||
var names = Enumerable.Range(0, idCount).Select(i => $"$e{i}").ToArray();
|
||||
command.CommandText = "EXPLAIN QUERY PLAN " + SqliteAuditWriter.BuildTraceReadSql(string.Join(", ", names));
|
||||
foreach (var name in names) command.Parameters.AddWithValue(name, Guid.NewGuid().ToString("D"));
|
||||
command.Parameters.AddWithValue("$since", DateTime.MinValue.ToString("o"));
|
||||
command.Parameters.AddWithValue("$after", DBNull.Value);
|
||||
command.Parameters.AddWithValue("$limit", 2000);
|
||||
var plan = new List<string>();
|
||||
using var reader = await command.ExecuteReaderAsync();
|
||||
while (await reader.ReadAsync()) plan.Add(reader.GetString(3));
|
||||
Assert.Contains(plan, p => p.Contains("SEARCH audit_event USING INDEX IX_audit_event_execution", StringComparison.Ordinal));
|
||||
Assert.Contains(plan, p => p.Contains("SEARCH audit_event USING INDEX IX_audit_event_parent_execution", StringComparison.Ordinal));
|
||||
Assert.DoesNotContain(plan, p => p.StartsWith("SCAN audit_event", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LegacyMalformedDetails_DoNotBreakStartupOrTraceReads()
|
||||
{
|
||||
var root = Guid.NewGuid();
|
||||
var row = Row(T0, root, null);
|
||||
await using (var writer = CreateWriter()) await writer.WriteAsync(row);
|
||||
using (var connection = new SqliteConnection($"Data Source={_dbPath}"))
|
||||
{
|
||||
await connection.OpenAsync();
|
||||
using var command = connection.CreateCommand();
|
||||
// Simulate a pre-upgrade file, including malformed payloads from an older writer.
|
||||
command.CommandText = "DROP INDEX IF EXISTS IX_audit_event_execution; DROP INDEX IF EXISTS IX_audit_event_parent_execution; "
|
||||
+ "INSERT INTO audit_event(EventId,OccurredAtUtc,Actor,Action,Outcome,DetailsJson) VALUES ($id,$at,'legacy','legacy','Success','{broken');";
|
||||
command.Parameters.AddWithValue("$id", Guid.NewGuid().ToString("D"));
|
||||
command.Parameters.AddWithValue("$at", T0.ToString("o"));
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
await using var reopened = CreateWriter();
|
||||
var result = await reopened.ReadByExecutionIdsAsync([root], DateTime.MinValue, 2000);
|
||||
Assert.Equal(row.EventId, Assert.Single(result).EventId);
|
||||
// Index initialization remains idempotent on the upgraded database.
|
||||
await using var reopenedAgain = CreateWriter();
|
||||
Assert.Equal(row.EventId, Assert.Single(await reopenedAgain.ReadByExecutionIdsAsync([root], DateTime.MinValue, 2000)).EventId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cursor_PagesEqualTimestampsWithoutRepeatingOrSkippingRows()
|
||||
{
|
||||
var root = Guid.NewGuid();
|
||||
await using var writer = CreateWriter();
|
||||
var rows = Enumerable.Range(0, 7).Select(_ => Row(T0, root, null)).ToArray();
|
||||
foreach (var row in rows) await writer.WriteAsync(row);
|
||||
var first = await writer.ReadByExecutionIdsAsync([root], T0, 3);
|
||||
// Reflection lets the pre-fix implementation demonstrate its repeated oldest page.
|
||||
var method = typeof(SqliteAuditWriter).GetMethod(nameof(SqliteAuditWriter.ReadByExecutionIdsAsync))!;
|
||||
object[] args = method.GetParameters().Length == 5
|
||||
? [new[] { root }, T0, 3, CancellationToken.None, first[^1].EventId.ToString("D")]
|
||||
: [new[] { root }, T0, 3, CancellationToken.None];
|
||||
var second = await (Task<IReadOnlyList<AuditEvent>>)method.Invoke(writer, args)!;
|
||||
Assert.Equal(6, first.Concat(second).Select(e => e.EventId).Distinct().Count());
|
||||
Assert.Equal(rows.OrderBy(e => e.EventId.ToString("D"), StringComparer.Ordinal).Take(6).Select(e => e.EventId),
|
||||
first.Concat(second).Select(e => e.EventId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MatchesExecutionId_OrParentExecutionId_OrderedAndBounded()
|
||||
{
|
||||
|
||||
+2
-7
@@ -11,13 +11,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Akka.TestKit.Xunit2" />
|
||||
<PackageReference Include="coverlet.collector" />
|
||||
<!--
|
||||
Bundle D D2 needs Microsoft.Data.SqlClient for the MsSqlMigrationFixture
|
||||
(mirroring ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests). Pinning 6.1.1 here for
|
||||
the same reason: EF SqlServer 10.0.7 needs >= 6.1.1 but the central pin
|
||||
is 6.0.2 (production ExternalSystemGateway). Override is test-only.
|
||||
-->
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" VersionOverride="6.1.1" />
|
||||
<!-- Use the central SqlClient pin, matching the production EF dependency floor. -->
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" />
|
||||
<!-- Security pin: GHSA-2m69-gcr7-jv3q. Microsoft.Data.Sqlite pulls the
|
||||
vulnerable native 2.1.11; 2.1.12 patches it. Bumping Sqlite does not help. -->
|
||||
|
||||
Reference in New Issue
Block a user