Resolve all 46 low-severity architecture review findings
This commit is contained in:
@@ -292,7 +292,8 @@ public class AuditLogPurgeActor : ReceiveActor
|
||||
channel,
|
||||
channelThreshold,
|
||||
_purgeOptions.ChannelPurgeBatchSize,
|
||||
_purgeOptions.ResolvedMaintenanceCommandTimeout)
|
||||
_purgeOptions.ResolvedMaintenanceCommandTimeout,
|
||||
maxRows: _purgeOptions.MaxPurgedRowsPerChannelPerTick)
|
||||
.ConfigureAwait(false);
|
||||
sw.Stop();
|
||||
|
||||
|
||||
@@ -30,13 +30,16 @@ public sealed class AuditLogPurgeOptions
|
||||
/// <summary>Period of the purge tick in hours (default 24).</summary>
|
||||
public int IntervalHours { get; set; } = 24;
|
||||
|
||||
/// <summary>Maximum rows deleted for each channel in one purge tick (default 50,000); remaining backlog resumes on later ticks.</summary>
|
||||
public int MaxPurgedRowsPerChannelPerTick { get; set; } = 50_000;
|
||||
|
||||
/// <summary>
|
||||
/// Batch size for the per-channel retention-override row DELETE
|
||||
/// (<see cref="ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories.IAuditLogRepository.PurgeChannelOlderThanAsync"/>).
|
||||
/// Each <c>DELETE TOP (@batch)</c> caps the transaction-log and lock footprint
|
||||
/// per statement; the repository loops batches until no rows remain. Default
|
||||
/// 5000 keeps individual deletes short on a busy central DB while still draining
|
||||
/// a large backlog within a tick. Clamped to a sane minimum in
|
||||
/// per statement; the repository stops at <see cref="MaxPurgedRowsPerChannelPerTick"/>
|
||||
/// so a backlogged channel yields to later channels. Default 5000 keeps individual
|
||||
/// deletes short on a busy central DB. Clamped to a sane minimum in
|
||||
/// <see cref="ChannelPurgeBatchSize"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
|
||||
@@ -19,6 +19,9 @@ public sealed class AuditLogPurgeOptionsValidator : OptionsValidatorBase<AuditLo
|
||||
$"AuditLog:Purge:{nameof(AuditLogPurgeOptions.IntervalHours)} " +
|
||||
$"({options.IntervalHours}) must be > 0.");
|
||||
|
||||
builder.RequireThat(options.MaxPurgedRowsPerChannelPerTick > 0,
|
||||
"AuditLog:Purge:MaxPurgedRowsPerChannelPerTick must be > 0.");
|
||||
|
||||
// Backing field for the documented "ChannelPurgeBatchSize" config key.
|
||||
builder.RequireThat(options.ChannelPurgeBatchSizeConfigured > 0,
|
||||
$"AuditLog:Purge:{nameof(AuditLogPurgeOptions.ChannelPurgeBatchSizeConfigured)} " +
|
||||
|
||||
@@ -44,7 +44,7 @@ public sealed class GrpcPullTraceEventsClient : IPullTraceEventsClient
|
||||
IReadOnlyCollection<Guid> executionIds,
|
||||
DateTime sinceUtc,
|
||||
int maxEvents,
|
||||
CancellationToken ct)
|
||||
CancellationToken ct, string? afterEventId = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(executionIds);
|
||||
if (executionIds.Count == 0 || maxEvents <= 0)
|
||||
@@ -67,6 +67,7 @@ public sealed class GrpcPullTraceEventsClient : IPullTraceEventsClient
|
||||
{
|
||||
SinceUtc = Timestamp.FromDateTime(EnsureUtc(sinceUtc)),
|
||||
MaxEvents = maxEvents,
|
||||
AfterId = afterEventId ?? string.Empty,
|
||||
};
|
||||
foreach (var id in executionIds)
|
||||
{
|
||||
|
||||
+2
-2
@@ -20,9 +20,9 @@ public sealed class SiteAuditReconciliationOptionsValidator
|
||||
$"AuditLog:Reconciliation:{nameof(SiteAuditReconciliationOptions.ReconciliationIntervalSeconds)} " +
|
||||
$"({options.ReconciliationIntervalSeconds}) must be > 0.");
|
||||
|
||||
builder.RequireThat(options.BatchSize > 0,
|
||||
builder.RequireThat(options.BatchSize is > 0 and <= Communication.Grpc.SiteStreamGrpcServer.MaxReconciliationRowsPerPull,
|
||||
$"AuditLog:Reconciliation:{nameof(SiteAuditReconciliationOptions.BatchSize)} " +
|
||||
$"({options.BatchSize}) must be > 0.");
|
||||
$"({options.BatchSize}) must be 1..2000.");
|
||||
|
||||
builder.RequireThat(options.StalledAfterNonDrainingCycles > 0,
|
||||
$"AuditLog:Reconciliation:{nameof(SiteAuditReconciliationOptions.StalledAfterNonDrainingCycles)} " +
|
||||
|
||||
@@ -271,6 +271,15 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
|
||||
-- to the window. Additive and idempotent.
|
||||
CREATE INDEX IF NOT EXISTS IX_audit_event_occurred
|
||||
ON audit_event (OccurredAtUtc);
|
||||
|
||||
-- Fresh execution IDs permit clock-independent trace reads. Partial indexes
|
||||
-- exclude malformed legacy details before evaluating either JSON expression.
|
||||
CREATE INDEX IF NOT EXISTS IX_audit_event_execution
|
||||
ON audit_event (json_extract(DetailsJson, '$.executionId'), OccurredAtUtc, EventId)
|
||||
WHERE json_valid(DetailsJson);
|
||||
CREATE INDEX IF NOT EXISTS IX_audit_event_parent_execution
|
||||
ON audit_event (json_extract(DetailsJson, '$.parentExecutionId'), OccurredAtUtc, EventId)
|
||||
WHERE json_valid(DetailsJson);
|
||||
""";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
@@ -859,7 +868,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
|
||||
|
||||
/// <inheritdoc />
|
||||
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)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(executionIds);
|
||||
if (limit <= 0)
|
||||
@@ -895,18 +904,10 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
|
||||
}
|
||||
var inList = string.Join(", ", names);
|
||||
|
||||
cmd.CommandText = $"""
|
||||
SELECT EventId, OccurredAtUtc, Actor, Action, Outcome,
|
||||
Category, Target, SourceNode, CorrelationId, DetailsJson
|
||||
FROM audit_event
|
||||
WHERE OccurredAtUtc >= $since
|
||||
AND (json_extract(DetailsJson, '$.executionId') IN ({inList})
|
||||
OR json_extract(DetailsJson, '$.parentExecutionId') IN ({inList}))
|
||||
ORDER BY OccurredAtUtc ASC, EventId ASC
|
||||
LIMIT $limit;
|
||||
""";
|
||||
cmd.CommandText = BuildTraceReadSql(inList);
|
||||
cmd.Parameters.AddWithValue("$since", EnsureUtc(sinceUtc).ToString(
|
||||
"o", System.Globalization.CultureInfo.InvariantCulture));
|
||||
cmd.Parameters.AddWithValue("$after", (object?)afterEventId ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("$limit", limit);
|
||||
|
||||
var rows = new List<AuditEvent>(Math.Min(limit, 256));
|
||||
@@ -919,6 +920,33 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads matching keys through both expression indexes before loading the bounded payload page.
|
||||
/// UNION deduplicates rows matching both IDs. Keeping payloads outside the union bounds sorting
|
||||
/// to IDs/timestamps and avoids the optimizer preferring a whole-store timestamp scan.
|
||||
/// </summary>
|
||||
internal static string BuildTraceReadSql(string inList) => $"""
|
||||
SELECT e.EventId, e.OccurredAtUtc, e.Actor, e.Action, e.Outcome,
|
||||
e.Category, e.Target, e.SourceNode, e.CorrelationId, e.DetailsJson
|
||||
FROM audit_event AS e
|
||||
JOIN (
|
||||
SELECT EventId, OccurredAtUtc
|
||||
FROM audit_event INDEXED BY IX_audit_event_execution
|
||||
WHERE json_valid(DetailsJson)
|
||||
AND json_extract(DetailsJson, '$.executionId') IN ({inList})
|
||||
AND (OccurredAtUtc > $since OR (OccurredAtUtc = $since AND ($after IS NULL OR EventId > $after)))
|
||||
UNION
|
||||
SELECT EventId, OccurredAtUtc
|
||||
FROM audit_event INDEXED BY IX_audit_event_parent_execution
|
||||
WHERE json_valid(DetailsJson)
|
||||
AND json_extract(DetailsJson, '$.parentExecutionId') IN ({inList})
|
||||
AND (OccurredAtUtc > $since OR (OccurredAtUtc = $since AND ($after IS NULL OR EventId > $after)))
|
||||
ORDER BY OccurredAtUtc, EventId
|
||||
LIMIT $limit
|
||||
) AS matched ON matched.EventId = e.EventId
|
||||
ORDER BY matched.OccurredAtUtc, matched.EventId;
|
||||
""";
|
||||
|
||||
/// <summary>
|
||||
/// Highest <c>audit_forward_state.rowid</c> this instance has ever SERVED from
|
||||
/// <see cref="ReadPendingSinceAsync"/> — i.e. the insertion-order high-water mark of
|
||||
|
||||
Reference in New Issue
Block a user