refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
|
||||
public interface IAuditService
|
||||
{
|
||||
/// <summary>
|
||||
/// Appends an audit log entry recording a user action on an entity.
|
||||
/// </summary>
|
||||
/// <param name="user">The authenticated username performing the action.</param>
|
||||
/// <param name="action">The action performed (e.g., "Create", "Update", "Delete").</param>
|
||||
/// <param name="entityType">The type name of the affected entity.</param>
|
||||
/// <param name="entityId">The string representation of the entity's primary key.</param>
|
||||
/// <param name="entityName">The display name of the affected entity.</param>
|
||||
/// <param name="afterState">The entity state after the action; may be null for deletes.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the log write.</param>
|
||||
Task LogAsync(string user, string action, string entityType, string entityId, string entityName, object? afterState, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Boundary-side abstraction for emitting Audit Log (#23) events.
|
||||
/// Implementations on the site write to local SQLite hot-path; on central they write to MS SQL directly.
|
||||
/// Failures must NEVER abort the user-facing action.
|
||||
/// </summary>
|
||||
public interface IAuditWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Persist an audit event. Best-effort: implementations must swallow/log internal failures
|
||||
/// rather than propagating them to the calling boundary code.
|
||||
/// </summary>
|
||||
/// <param name="evt">The audit event to persist.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task WriteAsync(AuditEvent evt, CancellationToken ct = default);
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (M3 Bundle E — Tasks E4/E5): site-side hook the
|
||||
/// store-and-forward retry loop invokes after every cached-call attempt and
|
||||
/// at terminal-state transitions, so the audit pipeline can emit
|
||||
/// <c>ApiCallCached</c>/<c>DbWriteCached</c> per-attempt rows and the
|
||||
/// <c>CachedResolve</c> terminal row under the original
|
||||
/// <see cref="TrackedOperationId"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The interface deliberately uses <see cref="CachedCallAttemptOutcome"/>
|
||||
/// rather than <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditStatus"/> so the
|
||||
/// S&F project does not need to depend on the audit vocabulary — the
|
||||
/// bridge living in <c>ZB.MOM.WW.ScadaBridge.AuditLog</c> maps the outcome to the right
|
||||
/// audit kind + status when materialising the <c>CachedCallTelemetry</c>
|
||||
/// packet.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Best-effort contract (alog.md §7):</b> implementations MUST swallow
|
||||
/// internal failures rather than propagating to the S&F service — a
|
||||
/// thrown observer must not be misclassified as a transient delivery
|
||||
/// failure and must not corrupt the retry-count bookkeeping.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public interface ICachedCallLifecycleObserver
|
||||
{
|
||||
/// <summary>
|
||||
/// Called by the store-and-forward retry loop after every cached-call
|
||||
/// delivery attempt. Receives the message's TrackedOperationId-bearing id,
|
||||
/// the per-category channel discriminator, retry-count + last-error
|
||||
/// context, and whether the outcome reached a terminal state.
|
||||
/// </summary>
|
||||
/// <param name="context">Per-attempt context including the tracking id, outcome, and audit provenance fields.</param>
|
||||
/// <param name="ct">Cancellation token for the observation operation.</param>
|
||||
Task OnAttemptCompletedAsync(CachedCallAttemptContext context, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-attempt context handed to <see cref="ICachedCallLifecycleObserver"/>.
|
||||
/// </summary>
|
||||
/// <param name="TrackedOperationId">
|
||||
/// Tracking id parsed from the underlying <c>StoreAndForwardMessage.Id</c>.
|
||||
/// </param>
|
||||
/// <param name="Channel">
|
||||
/// Trust-boundary channel string — <c>"ApiOutbound"</c> for ExternalSystem
|
||||
/// cached calls, <c>"DbOutbound"</c> for cached DB writes.
|
||||
/// </param>
|
||||
/// <param name="Target">Human-readable target (system name / DB connection).</param>
|
||||
/// <param name="SourceSite">Site id that submitted the cached call.</param>
|
||||
/// <param name="Outcome">Per-attempt outcome.</param>
|
||||
/// <param name="RetryCount">Number of retries performed so far (S&F bookkeeping).</param>
|
||||
/// <param name="LastError">Most recent error message (null on success).</param>
|
||||
/// <param name="HttpStatus">Most recent HTTP status (null when not applicable).</param>
|
||||
/// <param name="CreatedAtUtc">When the underlying S&F message was first enqueued.</param>
|
||||
/// <param name="OccurredAtUtc">When this attempt completed.</param>
|
||||
/// <param name="DurationMs">Duration of the attempt in milliseconds (null when not measured).</param>
|
||||
/// <param name="SourceInstanceId">Originating instance, when known.</param>
|
||||
/// <param name="ExecutionId">
|
||||
/// Audit Log #23 (ExecutionId Task 4): the originating script execution's
|
||||
/// per-run correlation id, threaded through the store-and-forward buffer from
|
||||
/// the cached-call enqueue path. The audit bridge stamps it onto the
|
||||
/// retry-loop <c>ApiCallCached</c>/<c>DbWriteCached</c> Attempted and
|
||||
/// <c>CachedResolve</c> rows so they correlate with the rest of the run.
|
||||
/// <c>null</c> for rows buffered before Task 4 (back-compat).
|
||||
/// </param>
|
||||
/// <param name="SourceScript">
|
||||
/// Audit Log #23 (ExecutionId Task 4): the originating script identifier,
|
||||
/// threaded alongside <paramref name="ExecutionId"/> so the retry-loop audit
|
||||
/// rows carry the same <c>SourceScript</c> provenance the script-side cached
|
||||
/// rows already do. <c>null</c> when not known.
|
||||
/// </param>
|
||||
/// <param name="ParentExecutionId">
|
||||
/// Audit Log #23 (ParentExecutionId Task 6): the <c>ExecutionId</c> of the
|
||||
/// inbound-API request that spawned the originating script execution,
|
||||
/// threaded through the store-and-forward buffer alongside
|
||||
/// <paramref name="ExecutionId"/>. The audit bridge stamps it onto the
|
||||
/// retry-loop <c>ApiCallCached</c>/<c>DbWriteCached</c> Attempted and
|
||||
/// <c>CachedResolve</c> rows so they correlate back to the spawning run.
|
||||
/// <c>null</c> for a non-routed run and for rows buffered before Task 6
|
||||
/// (back-compat).
|
||||
/// </param>
|
||||
public sealed record CachedCallAttemptContext(
|
||||
TrackedOperationId TrackedOperationId,
|
||||
string Channel,
|
||||
string Target,
|
||||
string SourceSite,
|
||||
CachedCallAttemptOutcome Outcome,
|
||||
int RetryCount,
|
||||
string? LastError,
|
||||
int? HttpStatus,
|
||||
DateTime CreatedAtUtc,
|
||||
DateTime OccurredAtUtc,
|
||||
int? DurationMs,
|
||||
string? SourceInstanceId,
|
||||
Guid? ExecutionId = null,
|
||||
string? SourceScript = null,
|
||||
Guid? ParentExecutionId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Coarse outcome of one cached-call delivery attempt, observed from inside
|
||||
/// the store-and-forward retry loop. The audit bridge maps this to the
|
||||
/// <c>ApiCallCached</c>/<c>DbWriteCached</c> Attempted row and, when terminal,
|
||||
/// the corresponding <c>CachedResolve</c> row.
|
||||
/// </summary>
|
||||
public enum CachedCallAttemptOutcome
|
||||
{
|
||||
/// <summary>Attempt delivered successfully — terminal Delivered state.</summary>
|
||||
Delivered,
|
||||
|
||||
/// <summary>Attempt failed transiently; another retry will follow.</summary>
|
||||
TransientFailure,
|
||||
|
||||
/// <summary>Attempt returned permanent failure — terminal Parked state (S&F semantics).</summary>
|
||||
PermanentFailure,
|
||||
|
||||
/// <summary>Retry budget exhausted — terminal Parked state.</summary>
|
||||
ParkedMaxRetries,
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Site-side fan-out abstraction for cached-call lifecycle telemetry
|
||||
/// (Audit Log #23 / M3). One <see cref="CachedCallTelemetry"/> packet carries
|
||||
/// both an audit row and an operational <c>SiteCalls</c> upsert; the
|
||||
/// implementation routes the audit half through <see cref="IAuditWriter"/>
|
||||
/// and the operational half through the site-local tracking SQLite store.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Defined in Commons so the script runtime (and the StoreAndForward retry
|
||||
/// loop, Bundle E4) can take a dependency on the abstraction rather than on
|
||||
/// the concrete forwarder living inside <c>ZB.MOM.WW.ScadaBridge.AuditLog</c> — the
|
||||
/// existing dependency arrow runs from <c>SiteRuntime</c> to Commons, not to
|
||||
/// AuditLog.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Best-effort contract (alog.md §7):</b> implementations MUST swallow
|
||||
/// internal failures rather than propagating to the calling script.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public interface ICachedCallTelemetryForwarder
|
||||
{
|
||||
/// <summary>
|
||||
/// Fan one combined-telemetry packet out to the audit writer and the
|
||||
/// tracking store. Best-effort — failures on either half are logged and
|
||||
/// swallowed; the returned Task completes when both halves have been
|
||||
/// attempted.
|
||||
/// </summary>
|
||||
/// <param name="telemetry">The combined-telemetry packet to fan out.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task ForwardAsync(CachedCallTelemetry telemetry, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Central-only audit writer for the direct-write path (Notification Outbox dispatch, Inbound API).
|
||||
/// Distinct from <see cref="IAuditWriter"/> so DI binding can differ between site and central hosts.
|
||||
/// </summary>
|
||||
public interface ICentralAuditWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Persist an audit event into the central AuditLog table directly (bypassing site telemetry).
|
||||
/// Best-effort: implementations must swallow/log internal failures rather than propagating them.
|
||||
/// </summary>
|
||||
/// <param name="evt">The audit event to persist.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task WriteAsync(AuditEvent evt, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.Data.Common;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for database access from scripts.
|
||||
/// Implemented by ExternalSystemGateway, consumed by ScriptRuntimeContext.
|
||||
/// </summary>
|
||||
public interface IDatabaseGateway
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns an ADO.NET DbConnection (typically SqlConnection) from the named connection.
|
||||
/// Connection pooling is managed by the underlying provider.
|
||||
/// Caller is responsible for disposing.
|
||||
/// </summary>
|
||||
/// <param name="connectionName">Name of the configured database connection to open.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the async open operation.</param>
|
||||
Task<DbConnection> GetConnectionAsync(
|
||||
string connectionName,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Submits a SQL write to the store-and-forward engine for reliable delivery.
|
||||
/// </summary>
|
||||
/// <param name="trackedOperationId">
|
||||
/// Audit Log #23 (M3): caller-supplied tracking id used as the
|
||||
/// store-and-forward message id so the S&F retry loop can read it
|
||||
/// back via <c>StoreAndForwardMessage.Id</c> and emit per-attempt /
|
||||
/// terminal cached-write telemetry under the same id. Defaults to
|
||||
/// <c>null</c> — when omitted the S&F engine mints a fresh GUID and no
|
||||
/// M3 telemetry is correlated (pre-M3 caller behaviour).
|
||||
/// </param>
|
||||
/// <param name="executionId">
|
||||
/// Audit Log #23 (ExecutionId Task 4): the originating script execution's
|
||||
/// per-run correlation id. When the write is buffered on a transient
|
||||
/// failure this is threaded onto the S&F message so the retry-loop
|
||||
/// cached-write audit rows carry it. <c>null</c> when not threaded.
|
||||
/// </param>
|
||||
/// <param name="sourceScript">
|
||||
/// Audit Log #23 (ExecutionId Task 4): the originating script identifier,
|
||||
/// threaded onto the buffered S&F message alongside
|
||||
/// <paramref name="executionId"/>. <c>null</c> when not known.
|
||||
/// </param>
|
||||
/// <param name="parentExecutionId">
|
||||
/// Audit Log #23 (ParentExecutionId Task 6): the <c>ExecutionId</c> of the
|
||||
/// inbound-API request that spawned the originating script execution.
|
||||
/// When the write is buffered on a transient failure this is threaded onto
|
||||
/// the S&F message alongside <paramref name="executionId"/> so the
|
||||
/// retry-loop cached-write audit rows carry it. <c>null</c> for a
|
||||
/// non-routed run.
|
||||
/// </param>
|
||||
/// <param name="connectionName">Name of the configured database connection to write to.</param>
|
||||
/// <param name="sql">SQL statement to execute as a store-and-forward write.</param>
|
||||
/// <param name="parameters">Optional SQL parameters for the statement.</param>
|
||||
/// <param name="originInstanceName">Optional name of the instance that originated the write.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the buffering operation.</param>
|
||||
Task CachedWriteAsync(
|
||||
string connectionName,
|
||||
string sql,
|
||||
IReadOnlyDictionary<string, object?>? parameters = null,
|
||||
string? originInstanceName = null,
|
||||
CancellationToken cancellationToken = default,
|
||||
TrackedOperationId? trackedOperationId = null,
|
||||
Guid? executionId = null,
|
||||
string? sourceScript = null,
|
||||
Guid? parentExecutionId = null);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for invoking external system HTTP APIs.
|
||||
/// Implemented by ExternalSystemGateway, consumed by ScriptRuntimeContext.
|
||||
/// </summary>
|
||||
public interface IExternalSystemClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Synchronous call to an external system. All failures returned to caller.
|
||||
/// </summary>
|
||||
/// <param name="systemName">The name of the external system.</param>
|
||||
/// <param name="methodName">The name of the method to invoke.</param>
|
||||
/// <param name="parameters">Method parameters as a dictionary, or null if none.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The result of the external call.</returns>
|
||||
Task<ExternalCallResult> CallAsync(
|
||||
string systemName,
|
||||
string methodName,
|
||||
IReadOnlyDictionary<string, object?>? parameters = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Attempt immediate delivery; on transient failure, hand to S&F engine.
|
||||
/// Permanent failures returned to caller.
|
||||
/// </summary>
|
||||
/// <param name="systemName">The name of the external system.</param>
|
||||
/// <param name="methodName">The name of the method to invoke.</param>
|
||||
/// <param name="parameters">Method parameters as a dictionary, or null if none.</param>
|
||||
/// <param name="originInstanceName">The instance name originating the call, or null.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <param name="trackedOperationId">
|
||||
/// Audit Log #23 (M3): caller-supplied tracking id used as the
|
||||
/// store-and-forward message id so the S&F retry loop can read it
|
||||
/// back via <c>StoreAndForwardMessage.Id</c> and emit per-attempt /
|
||||
/// terminal cached-call telemetry under the same id. Defaults to
|
||||
/// <c>null</c> — when omitted the S&F engine mints a fresh GUID and no
|
||||
/// M3 telemetry is correlated (the legacy behaviour pre-M3 callers rely
|
||||
/// on).
|
||||
/// </param>
|
||||
/// <param name="executionId">
|
||||
/// Audit Log #23 (ExecutionId Task 4): the originating script execution's
|
||||
/// per-run correlation id. When the call is buffered on a transient
|
||||
/// failure this is threaded onto the S&F message so the retry-loop
|
||||
/// cached-call audit rows carry it. <c>null</c> when not threaded.
|
||||
/// </param>
|
||||
/// <param name="sourceScript">
|
||||
/// Audit Log #23 (ExecutionId Task 4): the originating script identifier,
|
||||
/// threaded onto the buffered S&F message alongside
|
||||
/// <paramref name="executionId"/>. <c>null</c> when not known.
|
||||
/// </param>
|
||||
/// <param name="parentExecutionId">
|
||||
/// Audit Log #23 (ParentExecutionId Task 6): the <c>ExecutionId</c> of the
|
||||
/// inbound-API request that spawned the originating script execution.
|
||||
/// When the call is buffered on a transient failure this is threaded onto
|
||||
/// the S&F message alongside <paramref name="executionId"/> so the
|
||||
/// retry-loop cached-call audit rows carry it. <c>null</c> for a non-routed
|
||||
/// run.
|
||||
/// </param>
|
||||
/// <returns>The result of the external call.</returns>
|
||||
Task<ExternalCallResult> CachedCallAsync(
|
||||
string systemName,
|
||||
string methodName,
|
||||
IReadOnlyDictionary<string, object?>? parameters = null,
|
||||
string? originInstanceName = null,
|
||||
CancellationToken cancellationToken = default,
|
||||
TrackedOperationId? trackedOperationId = null,
|
||||
Guid? executionId = null,
|
||||
string? sourceScript = null,
|
||||
Guid? parentExecutionId = null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of an external system call.
|
||||
/// </summary>
|
||||
public record ExternalCallResult(
|
||||
bool Success,
|
||||
string? ResponseJson,
|
||||
string? ErrorMessage,
|
||||
bool WasBuffered = false)
|
||||
{
|
||||
// Commons-021: thread-safe lazy parse — `Lazy<T>` with the default
|
||||
// `LazyThreadSafetyMode.ExecutionAndPublication` guarantees that two
|
||||
// concurrent readers see the same `DynamicJsonElement` instance, the
|
||||
// `JsonDocument.Parse` runs at most once, and the published value is
|
||||
// safe under .NET's memory model. The closure captures `ResponseJson`
|
||||
// by reference to the property — the record's positional property is
|
||||
// an init-only field set in the constructor, so the snapshot read at
|
||||
// first-access time is stable for the lifetime of the result.
|
||||
private readonly Lazy<dynamic?> _response = new(() =>
|
||||
string.IsNullOrEmpty(ResponseJson)
|
||||
? null
|
||||
: new DynamicJsonElement(System.Text.Json.JsonDocument.Parse(ResponseJson).RootElement));
|
||||
|
||||
/// <summary>
|
||||
/// Parsed response as a dynamic object. Returns null if ResponseJson is null or empty.
|
||||
/// Access properties directly: result.Response.result, result.Response.items[0].name, etc.
|
||||
/// Thread-safe: concurrent readers share a single parsed instance (Commons-021).
|
||||
/// </summary>
|
||||
public dynamic? Response => _response.Value;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves an instance unique name to its site identifier.
|
||||
/// Used by Inbound API's Route.To() to determine which site to route requests to.
|
||||
/// </summary>
|
||||
public interface IInstanceLocator
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves the site identifier for a given instance unique name.
|
||||
/// Returns null if the instance is not found.
|
||||
/// </summary>
|
||||
/// <param name="instanceUniqueName">System-wide unique name of the instance to look up.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
Task<string?> GetSiteIdForInstanceAsync(
|
||||
string instanceUniqueName,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Surfaces the local node's semantic role-within-cluster name so downstream
|
||||
/// audit writers can stamp it on the SourceNode column.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Conventional values follow the pattern <c>node-a</c>/<c>node-b</c> on site
|
||||
/// nodes and <c>central-a</c>/<c>central-b</c> on central nodes. The value is
|
||||
/// a free-form operator-supplied label — there is no enforced format. When the
|
||||
/// configuration value is missing, empty, or whitespace, implementations
|
||||
/// return <c>null</c> so audit writers can persist NULL rather than an empty
|
||||
/// string.
|
||||
/// </remarks>
|
||||
public interface INodeIdentityProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// The configured semantic node name, trimmed of surrounding whitespace.
|
||||
/// <c>null</c> when unconfigured.
|
||||
/// </summary>
|
||||
string? NodeName { get; }
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
|
||||
// Commons-018: physically lives under Interfaces/Services/ to match the
|
||||
// established subfolder convention (REQ-COM-5b), but the namespace stays
|
||||
// `ZB.MOM.WW.ScadaBridge.Commons.Interfaces` to avoid a cascading update to 9+ consumer
|
||||
// files across ZB.MOM.WW.ScadaBridge.SiteRuntime, ZB.MOM.WW.ScadaBridge.AuditLog and ZB.MOM.WW.ScadaBridge.Host.
|
||||
// Adopting the canonical `ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services` namespace
|
||||
// can be picked up alongside any future Commons-wide namespace tidy-up.
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Site-local source of truth for cached-operation tracking
|
||||
/// (<c>ExternalSystem.CachedCall</c> / <c>Database.CachedWrite</c>) — alongside the
|
||||
/// Store-and-Forward buffer, this is the row that <c>Tracking.Status(id)</c>
|
||||
/// reads (Audit Log #23 / M3). One row per <see cref="TrackedOperationId"/>;
|
||||
/// terminal rows are purged after a configurable retention window
|
||||
/// (default 7 days).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The store is intentionally a thin write-API on top of SQLite — not a
|
||||
/// dispatcher. Status transitions follow
|
||||
/// <c>Submitted → Retrying → Delivered / Parked / Failed / Discarded</c>; rows
|
||||
/// in a terminal state never roll back. Implementations must:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see cref="RecordEnqueueAsync"/> is insert-if-not-exists
|
||||
/// (caller-supplied id is the idempotency key — duplicate enqueues are no-ops).</description></item>
|
||||
/// <item><description><see cref="RecordAttemptAsync"/> only updates non-terminal rows.</description></item>
|
||||
/// <item><description><see cref="RecordTerminalAsync"/> only flips a non-terminal row to terminal.</description></item>
|
||||
/// <item><description><see cref="PurgeTerminalAsync"/> deletes terminal rows whose
|
||||
/// <c>TerminalAtUtc</c> is strictly older than the supplied threshold.</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public interface IOperationTrackingStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Insert a new tracking row in <c>Submitted</c> state with <c>RetryCount = 0</c>.
|
||||
/// Idempotent — a duplicate id is silently ignored (the existing row is left
|
||||
/// untouched), matching the at-least-once semantics of the calling site
|
||||
/// store-and-forward path.
|
||||
/// </summary>
|
||||
/// <param name="id">Unique operation ID (idempotency key).</param>
|
||||
/// <param name="kind">Kind of operation (e.g., cached call type).</param>
|
||||
/// <param name="targetSummary">Optional summary of the operation target.</param>
|
||||
/// <param name="sourceInstanceId">Optional ID of the source instance.</param>
|
||||
/// <param name="sourceScript">Optional name of the source script.</param>
|
||||
/// <param name="sourceNode">Optional source node identifier.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task RecordEnqueueAsync(
|
||||
TrackedOperationId id,
|
||||
string kind,
|
||||
string? targetSummary,
|
||||
string? sourceInstanceId,
|
||||
string? sourceScript,
|
||||
string? sourceNode,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Advance an in-flight tracking row's status, retry counter, and most-
|
||||
/// recent error/HTTP-status. Terminal rows (<see cref="RecordTerminalAsync"/>
|
||||
/// already applied) are NOT mutated — the operation has reached its final
|
||||
/// outcome and any late-arriving attempt telemetry is dropped on the floor.
|
||||
/// </summary>
|
||||
/// <param name="id">Operation ID to update.</param>
|
||||
/// <param name="status">Current operation status.</param>
|
||||
/// <param name="retryCount">Number of retry attempts.</param>
|
||||
/// <param name="lastError">Optional error message from the last attempt.</param>
|
||||
/// <param name="httpStatus">Optional HTTP status code from the last attempt.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task RecordAttemptAsync(
|
||||
TrackedOperationId id,
|
||||
string status,
|
||||
int retryCount,
|
||||
string? lastError,
|
||||
int? httpStatus,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Flip a non-terminal tracking row to terminal — sets
|
||||
/// <c>TerminalAtUtc = now</c> and writes the final status / error. A row
|
||||
/// already in terminal state is left untouched (first-write-wins).
|
||||
/// </summary>
|
||||
/// <param name="id">Operation ID to mark as terminal.</param>
|
||||
/// <param name="status">Final operation status.</param>
|
||||
/// <param name="lastError">Optional final error message.</param>
|
||||
/// <param name="httpStatus">Optional final HTTP status code.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task RecordTerminalAsync(
|
||||
TrackedOperationId id,
|
||||
string status,
|
||||
string? lastError,
|
||||
int? httpStatus,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Return the latest snapshot for the supplied id, or <c>null</c> when no
|
||||
/// tracking row exists (purged or never recorded).
|
||||
/// </summary>
|
||||
/// <param name="id">Operation ID to fetch status for.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Tracking status snapshot, or null if not found.</returns>
|
||||
Task<TrackingStatusSnapshot?> GetStatusAsync(
|
||||
TrackedOperationId id,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Delete terminal rows whose <c>TerminalAtUtc</c> is strictly older than
|
||||
/// <paramref name="olderThanUtc"/>. Non-terminal rows are kept regardless
|
||||
/// of age (the operation is still in flight).
|
||||
/// </summary>
|
||||
/// <param name="olderThanUtc">Cutoff timestamp; rows terminal before this are deleted.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task PurgeTerminalAsync(
|
||||
DateTime olderThanUtc,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Commons-018: physically lives under Interfaces/Services/ to match the
|
||||
// established subfolder convention (REQ-COM-5b), but the namespace stays
|
||||
// `ZB.MOM.WW.ScadaBridge.Commons.Interfaces` to avoid a cascading update to consumers
|
||||
// across ZB.MOM.WW.ScadaBridge.AuditLog and ZB.MOM.WW.ScadaBridge.ConfigurationDatabase. Adopting
|
||||
// the canonical `ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services` namespace can be
|
||||
// picked up alongside any future Commons-wide namespace tidy-up.
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Abstraction over the central AuditLog partition-function roll-forward
|
||||
/// operation. M6-T5 introduces a daily-cadence hosted service
|
||||
/// (<c>AuditLogPartitionMaintenanceService</c>) that calls
|
||||
/// <see cref="EnsureLookaheadAsync"/> to make sure
|
||||
/// <c>pf_AuditLog_Month</c> always has at least <c>LookaheadMonths</c> of
|
||||
/// future boundaries available — otherwise inserts past the highest
|
||||
/// boundary land in a single ever-growing tail partition that
|
||||
/// <c>SwitchOutPartitionAsync</c> cannot purge cleanly.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The interface lives in <c>ZB.MOM.WW.ScadaBridge.Commons</c> so the central hosted
|
||||
/// service in <c>ZB.MOM.WW.ScadaBridge.AuditLog</c> can depend on it without taking a
|
||||
/// reference on <c>ZB.MOM.WW.ScadaBridge.ConfigurationDatabase</c>; the EF-based
|
||||
/// implementation ships in
|
||||
/// <c>ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Maintenance.AuditLogPartitionMaintenance</c>
|
||||
/// and is registered by <c>AddConfigurationDatabase</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Both methods read <c>sys.partition_range_values</c> / mutate
|
||||
/// <c>pf_AuditLog_Month</c> via raw SQL — there is no EF model for a
|
||||
/// partition function. The interface deliberately exposes only the two
|
||||
/// operations the hosted service needs; it is not a general partition-DDL
|
||||
/// surface.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public interface IPartitionMaintenance
|
||||
{
|
||||
/// <summary>
|
||||
/// Splits new monthly boundaries on <c>pf_AuditLog_Month</c> so the
|
||||
/// function covers at least <paramref name="lookaheadMonths"/> future
|
||||
/// months relative to <see cref="DateTime.UtcNow"/>. Idempotent — a
|
||||
/// boundary that already exists is skipped rather than re-issued.
|
||||
/// Returns the boundaries actually added, in chronological order.
|
||||
/// </summary>
|
||||
/// <param name="lookaheadMonths">Number of future monthly boundaries to ensure exist.</param>
|
||||
/// <param name="ct">Cancellation token for the SQL operation.</param>
|
||||
Task<IReadOnlyList<DateTime>> EnsureLookaheadAsync(int lookaheadMonths, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reads the current maximum boundary value from
|
||||
/// <c>sys.partition_range_values</c> for <c>pf_AuditLog_Month</c>.
|
||||
/// Returns <c>null</c> when the partition function does not exist or
|
||||
/// has no boundaries.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token for the SQL operation.</param>
|
||||
Task<DateTime?> GetMaxBoundaryAsync(CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Site-local audit-log queue surface consumed by the site
|
||||
/// <c>SiteAuditTelemetryActor</c> drain loop and the M6
|
||||
/// <c>SiteStreamGrpcServer.PullAuditEvents</c> reconciliation handler.
|
||||
/// Extracted from <c>SqliteAuditWriter</c> so both consumers can be
|
||||
/// unit-tested against a stub without touching SQLite; the
|
||||
/// <c>SqliteAuditWriter</c> production type implements this interface
|
||||
/// and DI wires the same singleton instance to every consumer.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Lives in Commons (rather than alongside <c>SqliteAuditWriter</c> in
|
||||
/// <c>ZB.MOM.WW.ScadaBridge.AuditLog</c>) because <c>ZB.MOM.WW.ScadaBridge.Communication</c> — which
|
||||
/// hosts the M6 gRPC pull handler — must depend on this interface and
|
||||
/// <c>ZB.MOM.WW.ScadaBridge.AuditLog</c> already depends on <c>ZB.MOM.WW.ScadaBridge.Communication</c>.
|
||||
/// Pulling the interface up to Commons breaks the would-be cycle while
|
||||
/// keeping the implementation in the AuditLog component.
|
||||
///
|
||||
/// Only the methods the drain and pull paths need are exposed — the
|
||||
/// hot-path <c>WriteAsync</c> stays on <see cref="IAuditWriter"/>
|
||||
/// (script-thread surface), separated by concern so each side can be
|
||||
/// mocked independently.
|
||||
/// </remarks>
|
||||
public interface ISiteAuditQueue
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns up to <paramref name="limit"/> rows currently in
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Pending"/>,
|
||||
/// oldest first. Idempotent — repeated calls before
|
||||
/// <see cref="MarkForwardedAsync"/> will yield the same rows again.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// AuditLog-001: cached-lifecycle <see cref="AuditEvent.Kind"/>s
|
||||
/// (<see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.CachedSubmit"/>,
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.ApiCallCached"/>,
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.DbWriteCached"/>,
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.CachedResolve"/>) are
|
||||
/// EXCLUDED from this result — they ride the combined-telemetry drain via
|
||||
/// <see cref="ReadPendingCachedTelemetryAsync"/> + the central
|
||||
/// <c>OnCachedTelemetryAsync</c> dual-write transaction. The audit-only
|
||||
/// drain handled by this method covers everything else (sync ApiCall /
|
||||
/// DbWrite, NotifySend, InboundRequest, etc.).
|
||||
/// </remarks>
|
||||
/// <param name="limit">Maximum number of rows to return.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task<IReadOnlyList<AuditEvent>> ReadPendingAsync(int limit, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// AuditLog-001: returns up to <paramref name="limit"/> rows in
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Pending"/>
|
||||
/// whose <see cref="AuditEvent.Kind"/> belongs to the cached-call lifecycle
|
||||
/// vocabulary (<see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.CachedSubmit"/>,
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.ApiCallCached"/>,
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.DbWriteCached"/>,
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.CachedResolve"/>),
|
||||
/// oldest first. The site-side <c>SiteAuditTelemetryActor</c> drains these
|
||||
/// rows separately, joining each with the matching operational tracking row
|
||||
/// (<c>IOperationTrackingStore.GetStatusAsync</c>) before pushing the
|
||||
/// combined <c>CachedTelemetryBatch</c> via
|
||||
/// <c>ISiteStreamAuditClient.IngestCachedTelemetryAsync</c>. Idempotent —
|
||||
/// repeated calls before <see cref="MarkForwardedAsync"/> yield the same
|
||||
/// rows again.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The two-drain partition is the production wiring of the combined-telemetry
|
||||
/// transport specified in Component-AuditLog.md §"Cached Operations —
|
||||
/// Combined Telemetry": cached rows MUST flow with their matching
|
||||
/// <c>SiteCalls</c> upsert through one MS SQL transaction at central. The
|
||||
/// pre-AuditLog-001 implementation drained cached rows through the
|
||||
/// audit-only path, leaving the operational half unsent and the central
|
||||
/// dual-write handler unreachable. Returning them via this dedicated read
|
||||
/// surface lets the new drain join with the tracking store before push.
|
||||
/// </remarks>
|
||||
/// <param name="limit">Maximum number of rows to return.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task<IReadOnlyList<AuditEvent>> ReadPendingCachedTelemetryAsync(int limit, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Flips the supplied EventIds from
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Pending"/> to
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Forwarded"/>.
|
||||
/// Non-existent or already-forwarded ids are silent no-ops.
|
||||
/// </summary>
|
||||
/// <param name="eventIds">Event IDs to mark as forwarded.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task MarkForwardedAsync(IReadOnlyList<Guid> eventIds, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// M6 reconciliation-pull read surface: returns up to <paramref name="batchSize"/>
|
||||
/// rows whose <see cref="AuditEvent.OccurredAtUtc"/> >= <paramref name="sinceUtc"/>
|
||||
/// and whose <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState"/> is still
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Pending"/> or
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Forwarded"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rows in the brief race window between site-Forwarded and central-ingest are
|
||||
/// intentionally included: the central reconciliation puller dedups on
|
||||
/// <see cref="AuditEvent.EventId"/>, so re-shipping is safe and avoids losing rows
|
||||
/// whose telemetry ack was acted on locally but never landed centrally. Ordering
|
||||
/// is oldest <see cref="AuditEvent.OccurredAtUtc"/> first with
|
||||
/// <see cref="AuditEvent.EventId"/> as the deterministic tiebreaker.
|
||||
/// </remarks>
|
||||
/// <param name="sinceUtc">Lower bound timestamp (UTC).</param>
|
||||
/// <param name="batchSize">Maximum number of rows to return.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task<IReadOnlyList<AuditEvent>> ReadPendingSinceAsync(
|
||||
DateTime sinceUtc, int batchSize, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// M6 reconciliation-pull commit surface: flips the supplied EventIds to
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Reconciled"/>,
|
||||
/// but ONLY for rows currently in
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Pending"/> or
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Forwarded"/>.
|
||||
/// Rows already in <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Reconciled"/>
|
||||
/// are left untouched (idempotent re-call). Non-existent ids are silent no-ops.
|
||||
/// </summary>
|
||||
/// <param name="eventIds">Event IDs to mark as reconciled.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task MarkReconciledAsync(IReadOnlyList<Guid> eventIds, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// M6 Bundle E (T6) health-metric surface: returns a point-in-time snapshot
|
||||
/// of the site queue's pending count + oldest pending timestamp + on-disk
|
||||
/// SQLite file size. Surfaced on
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Messages.Health.SiteHealthReport"/> as
|
||||
/// <c>SiteAuditBacklog</c> by the periodic <c>SiteAuditBacklogReporter</c>
|
||||
/// hosted service so a stuck site→central drain is visible on the central
|
||||
/// health dashboard. Safe to call concurrently with hot-path writes —
|
||||
/// implementations are expected to take the same connection lock used by
|
||||
/// the hot-path INSERT batch and the drain queries.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task<SiteAuditBacklogSnapshot> GetBacklogStatsAsync(CancellationToken ct = default);
|
||||
}
|
||||
Reference in New Issue
Block a user