docs(comments): strip internal task/milestone/bundle bookkeeping from code comments
Remove project bookkeeping citations from shipped code comments across the solution: hyphenated task IDs (WP-14, StoreAndForward-025), milestone/task/ issue refs (M3, Task 4, Audit Log #23, #21), Bundle X task-bundle labels, and C/D/K/S/T phase labels. Comment text only — no code logic, string/log literals, or XML-doc structure changed. Genuine descriptions are preserved (only the citation is stripped), and technical lookalikes are retained (UTF-8, SHA-256, T00:00:00, M365, UTC-5, pre-C4/pre-C5 schema versions). Flagged by the new CommentChecker TaskReferenceInComment / TrackingReferenceInComment checks plus targeted grep passes; full solution builds clean, append-only guard tests pass.
This commit is contained in:
@@ -19,21 +19,21 @@ using ZB.MOM.WW.ScadaBridge.StoreAndForward;
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
|
||||
/// <summary>
|
||||
/// WP-18: Script Runtime API — injected into Script/Alarm Execution Actors.
|
||||
/// Script Runtime API — injected into Script/Alarm Execution Actors.
|
||||
/// Provides the API surface that user scripts interact with:
|
||||
/// Instance.GetAttribute("name")
|
||||
/// Instance.SetAttribute("name", value)
|
||||
/// Instance.CallScript("scriptName", params)
|
||||
/// Scripts.CallShared("scriptName", params)
|
||||
///
|
||||
/// WP-13 (Phase 7): Integration surface APIs:
|
||||
/// (Phase 7): Integration surface APIs:
|
||||
/// ExternalSystem.Call("systemName", "methodName", params)
|
||||
/// ExternalSystem.CachedCall("systemName", "methodName", params)
|
||||
/// Database.Connection("name")
|
||||
/// Database.CachedWrite("name", "sql", params)
|
||||
/// Notify.To("listName").Send("subject", "message")
|
||||
///
|
||||
/// WP-20: Recursion Limit — call depth tracked and enforced.
|
||||
/// Recursion Limit — call depth tracked and enforced.
|
||||
/// </summary>
|
||||
public class ScriptRuntimeContext
|
||||
{
|
||||
@@ -57,12 +57,12 @@ public class ScriptRuntimeContext
|
||||
private readonly CancellationToken _scriptTimeoutToken;
|
||||
|
||||
/// <summary>
|
||||
/// WP-13: External system client for ExternalSystem.Call/CachedCall.
|
||||
/// External system client for ExternalSystem.Call/CachedCall.
|
||||
/// </summary>
|
||||
private readonly IExternalSystemClient? _externalSystemClient;
|
||||
|
||||
/// <summary>
|
||||
/// WP-13: Database gateway for Database.Connection/CachedWrite.
|
||||
/// Database gateway for Database.Connection/CachedWrite.
|
||||
/// </summary>
|
||||
private readonly IDatabaseGateway? _databaseGateway;
|
||||
|
||||
@@ -84,7 +84,7 @@ public class ScriptRuntimeContext
|
||||
private readonly string _siteId;
|
||||
|
||||
/// <summary>
|
||||
/// SourceNode-stamping (Task 13/14): the cluster node name supplied by
|
||||
/// SourceNode-stamping: the cluster node name supplied by
|
||||
/// <c>INodeIdentityProvider</c> on the local host — <c>node-a</c>/<c>node-b</c>
|
||||
/// for site nodes. Stamped onto <c>NotificationSubmit.SourceNode</c> by
|
||||
/// <see cref="NotifyTarget.Send"/> and onto <c>SiteCallOperational.SourceNode</c>
|
||||
@@ -106,21 +106,21 @@ public class ScriptRuntimeContext
|
||||
private readonly string? _sourceScript;
|
||||
|
||||
/// <summary>
|
||||
/// M2.12 (#25): site event logger for recording recursion-limit violations
|
||||
/// Site event logger for recording recursion-limit violations
|
||||
/// to the local SQLite event log. Optional — when null the emission is
|
||||
/// skipped; the existing <c>_logger.LogError</c> + throw path is unchanged.
|
||||
/// </summary>
|
||||
private readonly ISiteEventLogger? _siteEventLogger;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23: best-effort emitter for boundary-crossing actions executed
|
||||
/// Best-effort emitter for boundary-crossing actions executed
|
||||
/// by the script. Optional — when null the helpers degrade to a no-op audit
|
||||
/// path so tests / contexts that do not need the audit pipeline still work.
|
||||
/// </summary>
|
||||
private readonly IAuditWriter? _auditWriter;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (M3): site-local tracking store consulted by
|
||||
/// Site-local tracking store consulted by
|
||||
/// <c>Tracking.Status(TrackedOperationId)</c>. Optional — when null the
|
||||
/// helper throws on access, mirroring the existing
|
||||
/// "service-not-wired" behaviour of the other integration helpers.
|
||||
@@ -128,7 +128,7 @@ public class ScriptRuntimeContext
|
||||
private readonly IOperationTrackingStore? _operationTrackingStore;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (M3 Bundle E — Task E3): site-side dual emitter for
|
||||
/// Site-side dual emitter for
|
||||
/// cached-call lifecycle telemetry. Optional — when null
|
||||
/// <c>ExternalSystem.CachedCall</c> / <c>Database.CachedWrite</c> still
|
||||
/// return a <see cref="TrackedOperationId"/> and invoke the underlying
|
||||
@@ -138,7 +138,7 @@ public class ScriptRuntimeContext
|
||||
private readonly ICachedCallTelemetryForwarder? _cachedForwarder;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23: the per-execution id for this script run. Every
|
||||
/// The per-execution id for this script run. Every
|
||||
/// trust-boundary audit row emitted by this script execution
|
||||
/// (sync <c>ApiCall</c>/<c>DbWrite</c>, cached-call lifecycle rows,
|
||||
/// <c>NotifySend</c>) is stamped into <c>AuditEvent.ExecutionId</c> with
|
||||
@@ -149,15 +149,14 @@ public class ScriptRuntimeContext
|
||||
private readonly Guid _executionId;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (ParentExecutionId): the spawning execution's
|
||||
/// (ParentExecutionId): the spawning execution's
|
||||
/// <see cref="_executionId"/> when this script run was spawned by another
|
||||
/// execution — for an inbound-API-routed call this is the inbound request's
|
||||
/// per-request execution id. <c>null</c> for normal (tag-change /
|
||||
/// timer-triggered) runs and nested <c>CallScript</c> invocations. The
|
||||
/// routed script still mints its OWN fresh <see cref="_executionId"/>; this
|
||||
/// field records the spawner so a spawned execution's audit rows can point
|
||||
/// back at the execution that spawned it. (Task 5 wires the emitter that
|
||||
/// stamps this onto <c>AuditEvent.ParentExecutionId</c>.)
|
||||
/// back at the execution that spawned it.
|
||||
/// </summary>
|
||||
private readonly Guid? _parentExecutionId;
|
||||
|
||||
@@ -183,14 +182,14 @@ public class ScriptRuntimeContext
|
||||
/// <param name="operationTrackingStore">Optional store for tracking cached operation status.</param>
|
||||
/// <param name="cachedForwarder">Optional forwarder for cached call telemetry.</param>
|
||||
/// <param name="executionId">
|
||||
/// Audit Log #23: the per-execution id for this script run. When omitted
|
||||
/// The per-execution id for this script run. When omitted
|
||||
/// (tag-change / timer-triggered executions) a fresh id is generated; an
|
||||
/// inbound caller may supply one to tie the execution to an upstream
|
||||
/// request. Stamped into <c>AuditEvent.ExecutionId</c> on every
|
||||
/// trust-boundary audit row this execution emits.
|
||||
/// </param>
|
||||
/// <param name="parentExecutionId">
|
||||
/// Audit Log #23 (ParentExecutionId): the spawning execution's
|
||||
/// (ParentExecutionId): the spawning execution's
|
||||
/// <c>ExecutionId</c> — supplied for an inbound-API-routed call (the
|
||||
/// inbound request's per-request id), <c>null</c> for normal (tag-change /
|
||||
/// timer-triggered) runs. The routed script still generates its own fresh
|
||||
@@ -198,7 +197,7 @@ public class ScriptRuntimeContext
|
||||
/// </param>
|
||||
/// <param name="sourceNode">Optional cluster node identifier (node-a/node-b) for audit trail stamping.</param>
|
||||
/// <param name="siteEventLogger">
|
||||
/// M2.12 (#25): optional site event logger. When supplied, recursion-limit
|
||||
/// Optional site event logger. When supplied, recursion-limit
|
||||
/// violations at <c>CallScript</c> and <c>CallShared</c> emit a
|
||||
/// <c>script</c> Error event in addition to the existing
|
||||
/// <c>ILogger.LogError</c> + throw. When null the existing behaviour is
|
||||
@@ -252,16 +251,16 @@ public class ScriptRuntimeContext
|
||||
_auditWriter = auditWriter;
|
||||
_operationTrackingStore = operationTrackingStore;
|
||||
_cachedForwarder = cachedForwarder;
|
||||
// SourceNode-stamping (Task 13/14): the local node name read from
|
||||
// SourceNode-stamping: the local node name read from
|
||||
// INodeIdentityProvider at the ScriptExecutionActor; null when no
|
||||
// provider was wired so the downstream callsites pass null through
|
||||
// verbatim — leaving central SourceNode as NULL.
|
||||
_sourceNode = sourceNode;
|
||||
_executionId = executionId ?? Guid.NewGuid();
|
||||
// Audit Log #23 (ParentExecutionId): stored verbatim — no `?? NewGuid()`
|
||||
// (ParentExecutionId): stored verbatim — no `?? NewGuid()`
|
||||
// fallback. A non-routed run legitimately has no parent and stays null.
|
||||
_parentExecutionId = parentExecutionId;
|
||||
// M2.12 (#25): optional — null when not wired (tests / AlarmExecutionActor).
|
||||
// Optional — null when not wired (tests / AlarmExecutionActor).
|
||||
_siteEventLogger = siteEventLogger;
|
||||
// WaitForAttribute (spec §4.3): default(CancellationToken) == None when
|
||||
// not threaded in — the WaitAsync Ask is then bounded only by its own timeout.
|
||||
@@ -269,20 +268,20 @@ public class ScriptRuntimeContext
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (M5.4): this run's own per-execution id. Exposed so a
|
||||
/// This run's own per-execution id. Exposed so a
|
||||
/// nested <c>Scripts.CallShared</c> can record it as the spawned shared
|
||||
/// script's <c>ParentExecutionId</c>, forming a true execution tree.
|
||||
/// </summary>
|
||||
internal Guid ExecutionId => _executionId;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (M5.4): the spawning execution's id for this run (null for
|
||||
/// The spawning execution's id for this run (null for
|
||||
/// a root run). Exposed for test assertions on the execution tree.
|
||||
/// </summary>
|
||||
internal Guid? ParentExecutionId => _parentExecutionId;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (M5.4 — ParentExecutionId tag-cascade): builds a child
|
||||
/// (ParentExecutionId tag-cascade): builds a child
|
||||
/// <see cref="ScriptRuntimeContext"/> for an inline <c>Scripts.CallShared</c>
|
||||
/// invocation. The shared script runs inline (no actor hop) but is modelled
|
||||
/// as its OWN execution node in the audit tree: it mints a fresh
|
||||
@@ -326,9 +325,9 @@ public class ScriptRuntimeContext
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M2.12 (#25): fire-and-forget emission of a <c>script</c> Error site event
|
||||
/// Fire-and-forget emission of a <c>script</c> Error site event
|
||||
/// for a recursion-limit violation. Mirrors the call shape used by
|
||||
/// <c>ScriptExecutionActor</c>'s catch blocks (WP-32 / M1.8). A fault from
|
||||
/// <c>ScriptExecutionActor</c>'s catch blocks. A fault from
|
||||
/// the site-event logger is observed-and-dropped (best-effort) via
|
||||
/// <c>ContinueWith(OnlyOnFaulted)</c> — it never blocks or faults the
|
||||
/// <c>_logger.LogError</c> + throw path that follows. A null logger is a no-op.
|
||||
@@ -565,8 +564,8 @@ public class ScriptRuntimeContext
|
||||
|
||||
/// <summary>
|
||||
/// Calls a sibling script on the same instance by name (Ask pattern).
|
||||
/// WP-20: Enforces recursion limit.
|
||||
/// WP-22: Uses Ask pattern for CallScript.
|
||||
/// Enforces recursion limit.
|
||||
/// Uses Ask pattern for CallScript.
|
||||
/// <paramref name="parameters"/> may be a dictionary or an anonymous object
|
||||
/// (<c>new { name = "Bob" }</c>) — see <see cref="ScriptArgs"/>.
|
||||
/// </summary>
|
||||
@@ -581,7 +580,7 @@ public class ScriptRuntimeContext
|
||||
var msg = $"Script call depth exceeded maximum of {_maxCallDepth}. " +
|
||||
$"CallScript('{scriptName}') rejected at depth {nextDepth}.";
|
||||
_logger.LogError(msg);
|
||||
// M2.12 (#25): emit to site event log in addition to ILogger; fire-and-forget.
|
||||
// Emit to site event log in addition to ILogger; fire-and-forget.
|
||||
EmitRecursionLimitEventAsync(msg);
|
||||
throw new InvalidOperationException(msg);
|
||||
}
|
||||
@@ -592,7 +591,7 @@ public class ScriptRuntimeContext
|
||||
ScriptArgs.Normalize(parameters),
|
||||
nextDepth,
|
||||
correlationId,
|
||||
// Audit Log #23 (M5.4 — ParentExecutionId tag-cascade): the child
|
||||
// (ParentExecutionId tag-cascade): the child
|
||||
// script run is a NEW execution spawned BY this run. Its parent is
|
||||
// THIS run's own ExecutionId — NOT the inherited _parentExecutionId.
|
||||
// So A → CallScript(B) yields B.ParentExecutionId == A.ExecutionId,
|
||||
@@ -618,25 +617,25 @@ public class ScriptRuntimeContext
|
||||
public ScriptCallHelper Scripts => new(_sharedScriptLibrary, this, _currentCallDepth, _maxCallDepth, _logger);
|
||||
|
||||
/// <summary>
|
||||
/// WP-13: Provides access to external system calls.
|
||||
/// Provides access to external system calls.
|
||||
/// ExternalSystem.Call("systemName", "methodName", params)
|
||||
/// ExternalSystem.CachedCall("systemName", "methodName", params)
|
||||
/// </summary>
|
||||
public ExternalSystemHelper ExternalSystem => new(
|
||||
_externalSystemClient, _instanceName, _logger, _executionId, _auditWriter, _siteId, _sourceScript,
|
||||
// Audit Log #23 (M3 Bundle E — Task E3): emit CachedSubmit telemetry
|
||||
// Emit CachedSubmit telemetry
|
||||
// on every ExternalSystem.CachedCall enqueue.
|
||||
_cachedForwarder,
|
||||
// Audit Log #23 (ParentExecutionId): the spawning execution's id,
|
||||
// (ParentExecutionId): the spawning execution's id,
|
||||
// threaded alongside _executionId. Null for non-routed runs.
|
||||
_parentExecutionId,
|
||||
// SourceNode-stamping (Task 14): the local node name (node-a/node-b),
|
||||
// SourceNode-stamping: the local node name (node-a/node-b),
|
||||
// threaded so the cached-call telemetry construction sites can stamp
|
||||
// it onto SiteCallOperational.SourceNode.
|
||||
_sourceNode);
|
||||
|
||||
/// <summary>
|
||||
/// WP-13: Provides access to database operations.
|
||||
/// Provides access to database operations.
|
||||
/// Database.Connection("name")
|
||||
/// Database.CachedWrite("name", "sql", params)
|
||||
/// </summary>
|
||||
@@ -645,20 +644,20 @@ public class ScriptRuntimeContext
|
||||
_instanceName,
|
||||
_logger,
|
||||
_executionId,
|
||||
// Audit Log #23 (M4 Bundle A): wire the IAuditWriter so
|
||||
// Wire the IAuditWriter so
|
||||
// Database.Connection(name) returns an auditing decorator that
|
||||
// emits one DbOutbound/DbWrite row per script-initiated
|
||||
// Execute / ExecuteScalar / ExecuteReader.
|
||||
_auditWriter,
|
||||
_siteId,
|
||||
_sourceScript,
|
||||
// Audit Log #23 (M3 Bundle E — Task E6): emit CachedSubmit telemetry on
|
||||
// Emit CachedSubmit telemetry on
|
||||
// every Database.CachedWrite enqueue.
|
||||
_cachedForwarder,
|
||||
// Audit Log #23 (ParentExecutionId): the spawning execution's id,
|
||||
// (ParentExecutionId): the spawning execution's id,
|
||||
// threaded alongside _executionId. Null for non-routed runs.
|
||||
_parentExecutionId,
|
||||
// SourceNode-stamping (Task 14): the local node name (node-a/node-b),
|
||||
// SourceNode-stamping: the local node name (node-a/node-b),
|
||||
// threaded so Database.CachedWrite's CachedSubmit telemetry can
|
||||
// stamp it onto SiteCallOperational.SourceNode.
|
||||
_sourceNode);
|
||||
@@ -670,7 +669,7 @@ public class ScriptRuntimeContext
|
||||
/// <c>Notify.Status(id)</c> queries the delivery status of that notification.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Audit Log #23 (M4 Bundle C): the <see cref="IAuditWriter"/> is threaded
|
||||
/// The <see cref="IAuditWriter"/> is threaded
|
||||
/// through so <c>Notify.To(list).Send(...)</c> emits one
|
||||
/// <c>Notification</c>/<c>NotifySend</c> audit row per accepted submission.
|
||||
/// Best-effort per alog.md §7 — a thrown writer never aborts the script's
|
||||
@@ -679,15 +678,15 @@ public class ScriptRuntimeContext
|
||||
public NotifyHelper Notify => new(
|
||||
_storeAndForward, _siteCommunicationActor, _siteId, _instanceName, _sourceScript, _askTimeout, _logger,
|
||||
_executionId, _auditWriter,
|
||||
// Audit Log #23 (ParentExecutionId): the spawning execution's id,
|
||||
// (ParentExecutionId): the spawning execution's id,
|
||||
// threaded alongside _executionId. Null for non-routed runs.
|
||||
_parentExecutionId,
|
||||
// SourceNode-stamping (Task 13): the local node name (node-a/node-b),
|
||||
// SourceNode-stamping: the local node name (node-a/node-b),
|
||||
// threaded so NotifyTarget.Send can stamp it onto NotificationSubmit.
|
||||
_sourceNode);
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (M3): site-local tracking-status API for cached operations.
|
||||
/// Site-local tracking-status API for cached operations.
|
||||
/// <c>Tracking.Status(trackedOperationId)</c> reads the site SQLite tracking row
|
||||
/// directly (authoritative source of truth — no central round-trip) and
|
||||
/// returns a <see cref="TrackingStatusSnapshot"/>, or <c>null</c> when the
|
||||
@@ -732,8 +731,8 @@ public class ScriptRuntimeContext
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WP-17: Executes a shared script inline (direct method call, not actor message).
|
||||
/// WP-20: Enforces recursion limit.
|
||||
/// Executes a shared script inline (direct method call, not actor message).
|
||||
/// Enforces recursion limit.
|
||||
/// <paramref name="parameters"/> may be a dictionary or an anonymous
|
||||
/// object (<c>new { name = "Bob" }</c>) — see <see cref="ScriptArgs"/>.
|
||||
/// </summary>
|
||||
@@ -752,13 +751,13 @@ public class ScriptRuntimeContext
|
||||
var msg = $"Script call depth exceeded maximum of {_maxCallDepth}. " +
|
||||
$"CallShared('{scriptName}') rejected at depth {nextDepth}.";
|
||||
_logger.LogError(msg);
|
||||
// M2.12 (#25): emit to site event log via the parent context's
|
||||
// Emit to site event log via the parent context's
|
||||
// helper — single emission path, fire-and-forget.
|
||||
_context.EmitRecursionLimitEventAsync(msg);
|
||||
throw new InvalidOperationException(msg);
|
||||
}
|
||||
|
||||
// Audit Log #23 (M5.4 — ParentExecutionId tag-cascade): the shared
|
||||
// (ParentExecutionId tag-cascade): the shared
|
||||
// script runs inline, but is modelled as its OWN execution node — a
|
||||
// child context mints a fresh ExecutionId parented to the caller's
|
||||
// ExecutionId, so its audit rows chain under the calling run.
|
||||
@@ -770,10 +769,10 @@ public class ScriptRuntimeContext
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WP-13: Helper for ExternalSystem.Call/CachedCall syntax.
|
||||
/// Helper for ExternalSystem.Call/CachedCall syntax.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Audit Log #23 (M2 Bundle F): every <see cref="Call"/> invocation emits
|
||||
/// Every <see cref="Call"/> invocation emits
|
||||
/// one <c>ApiOutbound</c>/<c>ApiCall</c> audit row via <see cref="IAuditWriter"/>.
|
||||
/// The audit emission is wrapped in a try/catch that swallows every exception
|
||||
/// — the audit pipeline is best-effort and must NEVER abort the script's
|
||||
@@ -795,10 +794,9 @@ public class ScriptRuntimeContext
|
||||
private readonly Guid _executionId;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (ParentExecutionId): the spawning execution's id when
|
||||
/// (ParentExecutionId): the spawning execution's id when
|
||||
/// this run was inbound-API-routed; <c>null</c> for non-routed runs.
|
||||
/// Threaded alongside <see cref="_executionId"/> ready for the Task 5
|
||||
/// emitter — no audit row carries it yet.
|
||||
/// Threaded alongside <see cref="_executionId"/> for audit-row stamping.
|
||||
/// </summary>
|
||||
private readonly Guid? _parentExecutionId;
|
||||
|
||||
@@ -808,7 +806,7 @@ public class ScriptRuntimeContext
|
||||
private readonly ICachedCallTelemetryForwarder? _cachedForwarder;
|
||||
|
||||
/// <summary>
|
||||
/// SourceNode-stamping (Task 14): the local cluster node name on
|
||||
/// SourceNode-stamping: the local cluster node name on
|
||||
/// which this script is executing (<c>node-a</c>/<c>node-b</c>).
|
||||
/// Stamped onto <c>SiteCallOperational.SourceNode</c> on the three
|
||||
/// cached-call telemetry construction sites (CachedSubmit + the two
|
||||
@@ -883,7 +881,7 @@ public class ScriptRuntimeContext
|
||||
if (_client == null)
|
||||
throw new InvalidOperationException("External system client not available");
|
||||
|
||||
// Audit Log #23 (M2 Bundle F): wrap the outbound call so every
|
||||
// Wrap the outbound call so every
|
||||
// attempt emits exactly one ApiOutbound/ApiCall row. The wrapper
|
||||
// mirrors the existing call-site behaviour — the original result
|
||||
// OR original exception flows back to the script untouched; the
|
||||
@@ -911,11 +909,11 @@ public class ScriptRuntimeContext
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submit a cached outbound API call (Audit Log #23 / M3). Mints a
|
||||
/// Submit a cached outbound API call. Mints a
|
||||
/// fresh <see cref="TrackedOperationId"/>, emits the lifecycle's first
|
||||
/// <c>CachedSubmit</c> telemetry packet, hands the call to the
|
||||
/// store-and-forward retry loop (which emits per-attempt and terminal
|
||||
/// telemetry under the same id — Bundle E Tasks E4/E5), and returns
|
||||
/// telemetry under the same id), and returns
|
||||
/// the id immediately so the script can later query
|
||||
/// <c>Tracking.Status(id)</c>.
|
||||
/// </summary>
|
||||
@@ -953,15 +951,15 @@ public class ScriptRuntimeContext
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// Hand off to the existing cached-call path. The TrackedOperationId
|
||||
// becomes the S&F message id so the retry loop (Bundle E Tasks
|
||||
// E4/E5) can read it back via StoreAndForwardMessage.Id.
|
||||
// becomes the S&F message id so the retry loop can read it back
|
||||
// via StoreAndForwardMessage.Id.
|
||||
//
|
||||
// M3 Bundle F (F2): the result is now retained because the
|
||||
// The result is now retained because the
|
||||
// immediate-success path (WasBuffered=false) bypasses S&F entirely
|
||||
// — no retry loop, no ICachedCallLifecycleObserver fire. The
|
||||
// helper must emit the Attempted + CachedResolve terminal rows
|
||||
// itself, otherwise Tracking.Status(id) would stay in Submitted
|
||||
// forever and the audit log would be missing the M3 lifecycle.
|
||||
// forever and the audit log would be missing the lifecycle.
|
||||
// The WasBuffered=true path is unaffected — the S&F retry loop
|
||||
// owns the Attempted + Resolve emissions in that case.
|
||||
ExternalCallResult? result;
|
||||
@@ -974,12 +972,12 @@ public class ScriptRuntimeContext
|
||||
_instanceName,
|
||||
cancellationToken,
|
||||
trackedId,
|
||||
// Audit Log #23 (ExecutionId Task 4): thread the script
|
||||
// (ExecutionId): thread the script
|
||||
// execution's ExecutionId + SourceScript so a buffered
|
||||
// cached call's retry-loop audit rows carry them.
|
||||
executionId: _executionId,
|
||||
sourceScript: _sourceScript,
|
||||
// Audit Log #23 (ParentExecutionId Task 6): thread the
|
||||
// (ParentExecutionId): thread the
|
||||
// spawning inbound-API request's ExecutionId so a buffered
|
||||
// cached call's retry-loop audit rows carry it too. Null
|
||||
// for a non-routed run.
|
||||
@@ -998,7 +996,7 @@ public class ScriptRuntimeContext
|
||||
throw;
|
||||
}
|
||||
|
||||
// M3 Bundle F (F2): immediate-completion lifecycle — emit the
|
||||
// Immediate-completion lifecycle — emit the
|
||||
// missing Attempted + CachedResolve rows when the underlying call
|
||||
// resolved without engaging the store-and-forward retry loop.
|
||||
if (result is { WasBuffered: false })
|
||||
@@ -1045,7 +1043,7 @@ public class ScriptRuntimeContext
|
||||
// per-execution id shared across this script run.
|
||||
correlationId: trackedId.Value,
|
||||
executionId: _executionId,
|
||||
// Audit Log #23 (ParentExecutionId): the spawning
|
||||
// (ParentExecutionId): the spawning
|
||||
// execution's id; null for non-routed runs.
|
||||
parentExecutionId: _parentExecutionId,
|
||||
sourceSiteId: string.IsNullOrEmpty(_siteId) ? null : _siteId,
|
||||
@@ -1058,7 +1056,7 @@ public class ScriptRuntimeContext
|
||||
Channel: "ApiOutbound",
|
||||
Target: target,
|
||||
SourceSite: _siteId,
|
||||
// SourceNode-stamping (Task 14): the local node name
|
||||
// SourceNode-stamping: the local node name
|
||||
// (node-a/node-b) — threaded through INodeIdentityProvider
|
||||
// at the ScriptExecutionActor; null when no provider was
|
||||
// wired so central persists SiteCalls.SourceNode as NULL.
|
||||
@@ -1093,11 +1091,11 @@ public class ScriptRuntimeContext
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M3 Bundle F (F2): emit the Attempted + CachedResolve lifecycle
|
||||
/// Emit the Attempted + CachedResolve lifecycle
|
||||
/// rows for an immediate-completion <c>CachedCall</c> (WasBuffered=false).
|
||||
/// The S&F retry loop never engaged, so the
|
||||
/// <c>ICachedCallLifecycleObserver</c> never fires — the helper must
|
||||
/// produce both rows itself to keep the M3 audit contract whole
|
||||
/// produce both rows itself to keep the audit contract whole
|
||||
/// (Submit → Attempted → Resolve under one TrackedOperationId).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
@@ -1162,7 +1160,7 @@ public class ScriptRuntimeContext
|
||||
// ExecutionId = per-execution id for this script run.
|
||||
correlationId: trackedId.Value,
|
||||
executionId: _executionId,
|
||||
// Audit Log #23 (ParentExecutionId): the spawning
|
||||
// (ParentExecutionId): the spawning
|
||||
// execution's id; null for non-routed runs.
|
||||
parentExecutionId: _parentExecutionId,
|
||||
sourceSiteId: string.IsNullOrEmpty(_siteId) ? null : _siteId,
|
||||
@@ -1177,7 +1175,7 @@ public class ScriptRuntimeContext
|
||||
Channel: "ApiOutbound",
|
||||
Target: target,
|
||||
SourceSite: _siteId,
|
||||
// SourceNode-stamping (Task 14): the local node name
|
||||
// SourceNode-stamping: the local node name
|
||||
// (node-a/node-b) — threaded through INodeIdentityProvider
|
||||
// at the ScriptExecutionActor; null when no provider was
|
||||
// wired so central persists SiteCalls.SourceNode as NULL.
|
||||
@@ -1230,7 +1228,7 @@ public class ScriptRuntimeContext
|
||||
// ExecutionId = per-execution id for this script run.
|
||||
correlationId: trackedId.Value,
|
||||
executionId: _executionId,
|
||||
// Audit Log #23 (ParentExecutionId): the spawning
|
||||
// (ParentExecutionId): the spawning
|
||||
// execution's id; null for non-routed runs.
|
||||
parentExecutionId: _parentExecutionId,
|
||||
sourceSiteId: string.IsNullOrEmpty(_siteId) ? null : _siteId,
|
||||
@@ -1245,7 +1243,7 @@ public class ScriptRuntimeContext
|
||||
Channel: "ApiOutbound",
|
||||
Target: target,
|
||||
SourceSite: _siteId,
|
||||
// SourceNode-stamping (Task 14): the local node name
|
||||
// SourceNode-stamping: the local node name
|
||||
// (node-a/node-b) — threaded through INodeIdentityProvider
|
||||
// at the ScriptExecutionActor; null when no provider was
|
||||
// wired so central persists SiteCalls.SourceNode as NULL.
|
||||
@@ -1367,7 +1365,7 @@ public class ScriptRuntimeContext
|
||||
// Status: Delivered on a Success result; Failed otherwise (the
|
||||
// ExternalSystemClient already maps HTTP non-2xx + transient
|
||||
// exceptions into Success=false on the result, or surfaces a raw
|
||||
// exception). M2 makes no distinction between transient + permanent
|
||||
// exception). No distinction is made between transient + permanent
|
||||
// failure here — both manifest as Status.Failed on the sync path.
|
||||
var status = (thrown == null && result != null && result.Success)
|
||||
? AuditStatus.Delivered
|
||||
@@ -1409,13 +1407,13 @@ public class ScriptRuntimeContext
|
||||
// (e.g. a shared script running inline).
|
||||
actor: _sourceScript,
|
||||
target: $"{systemName}.{methodName}",
|
||||
// Audit Log #23: a sync one-shot call has no operation
|
||||
// A sync one-shot call has no operation
|
||||
// lifecycle, so CorrelationId is null. ExecutionId carries the
|
||||
// per-execution id so all the sync ApiCall/DbWrite rows from
|
||||
// one script run can be correlated together.
|
||||
correlationId: null,
|
||||
executionId: _executionId,
|
||||
// Audit Log #23 (ParentExecutionId): the spawning execution's
|
||||
// (ParentExecutionId): the spawning execution's
|
||||
// id; null for non-routed runs.
|
||||
parentExecutionId: _parentExecutionId,
|
||||
sourceSiteId: string.IsNullOrEmpty(_siteId) ? null : _siteId,
|
||||
@@ -1461,10 +1459,10 @@ public class ScriptRuntimeContext
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WP-13: Helper for Database.Connection/CachedWrite syntax.
|
||||
/// Helper for Database.Connection/CachedWrite syntax.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Audit Log #23 (M3 Bundle E — Task E6): <see cref="CachedWrite"/> mirrors
|
||||
/// <see cref="CachedWrite"/> mirrors
|
||||
/// <see cref="ExternalSystemHelper.CachedCall"/> — mints a
|
||||
/// <see cref="TrackedOperationId"/>, emits the lifecycle's first
|
||||
/// CachedSubmit packet (Channel <c>DbOutbound</c>), hands off to the S&F
|
||||
@@ -1479,10 +1477,9 @@ public class ScriptRuntimeContext
|
||||
private readonly Guid _executionId;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (ParentExecutionId): the spawning execution's id when
|
||||
/// (ParentExecutionId): the spawning execution's id when
|
||||
/// this run was inbound-API-routed; <c>null</c> for non-routed runs.
|
||||
/// Threaded alongside <see cref="_executionId"/> ready for the Task 5
|
||||
/// emitter — no audit row carries it yet.
|
||||
/// Threaded alongside <see cref="_executionId"/> for audit-row stamping.
|
||||
/// </summary>
|
||||
private readonly Guid? _parentExecutionId;
|
||||
|
||||
@@ -1491,7 +1488,7 @@ public class ScriptRuntimeContext
|
||||
private readonly ICachedCallTelemetryForwarder? _cachedForwarder;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (M4 Bundle A): best-effort emitter for synchronous
|
||||
/// Best-effort emitter for synchronous
|
||||
/// <c>Database.Connection</c>-routed Execute / ExecuteScalar /
|
||||
/// ExecuteReader calls. When wired, <see cref="Connection"/> returns
|
||||
/// an <see cref="AuditingDbConnection"/> that intercepts each command
|
||||
@@ -1503,7 +1500,7 @@ public class ScriptRuntimeContext
|
||||
private readonly IAuditWriter? _auditWriter;
|
||||
|
||||
/// <summary>
|
||||
/// SourceNode-stamping (Task 14): the local cluster node name on
|
||||
/// SourceNode-stamping: the local cluster node name on
|
||||
/// which this script is executing (<c>node-a</c>/<c>node-b</c>).
|
||||
/// Stamped onto <c>SiteCallOperational.SourceNode</c> at the
|
||||
/// <c>Database.CachedWrite</c> CachedSubmit telemetry construction
|
||||
@@ -1567,7 +1564,7 @@ public class ScriptRuntimeContext
|
||||
|
||||
var inner = await _gateway.GetConnectionAsync(name, cancellationToken);
|
||||
|
||||
// Audit Log #23 (M4 Bundle A): wrap in an auditing decorator so
|
||||
// Wrap in an auditing decorator so
|
||||
// every script-initiated Execute* / ExecuteReader on the returned
|
||||
// connection emits one DbOutbound/DbWrite audit row. The wrapper
|
||||
// delegates all other ADO.NET behaviour to the inner connection
|
||||
@@ -1588,7 +1585,7 @@ public class ScriptRuntimeContext
|
||||
sourceScript: _sourceScript,
|
||||
logger: _logger,
|
||||
executionId: _executionId,
|
||||
// Audit Log #23 (ParentExecutionId): the spawning execution's
|
||||
// (ParentExecutionId): the spawning execution's
|
||||
// id, threaded alongside _executionId. Null for non-routed runs.
|
||||
parentExecutionId: _parentExecutionId);
|
||||
}
|
||||
@@ -1623,7 +1620,7 @@ public class ScriptRuntimeContext
|
||||
name, trackedId, target, occurredAtUtc, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// M2.3 (#7): the gateway now attempts the write immediately and
|
||||
// The gateway now attempts the write immediately and
|
||||
// classifies the outcome (mirroring ExternalSystem.CachedCall). The
|
||||
// result is retained because the immediate paths (WasBuffered=false —
|
||||
// immediate success OR a synchronous permanent failure) bypass the
|
||||
@@ -1638,12 +1635,12 @@ public class ScriptRuntimeContext
|
||||
{
|
||||
result = await _gateway.CachedWriteAsync(
|
||||
name, sql, parameters, _instanceName, cancellationToken, trackedId,
|
||||
// Audit Log #23 (ExecutionId Task 4): thread the script
|
||||
// (ExecutionId): thread the script
|
||||
// execution's ExecutionId + SourceScript so a buffered
|
||||
// cached write's retry-loop audit rows carry them.
|
||||
executionId: _executionId,
|
||||
sourceScript: _sourceScript,
|
||||
// Audit Log #23 (ParentExecutionId Task 6): thread the
|
||||
// (ParentExecutionId): thread the
|
||||
// spawning inbound-API request's ExecutionId so a buffered
|
||||
// cached write's retry-loop audit rows carry it too. Null
|
||||
// for a non-routed run.
|
||||
@@ -1658,7 +1655,7 @@ public class ScriptRuntimeContext
|
||||
throw;
|
||||
}
|
||||
|
||||
// M2.3 (#7): immediate-completion lifecycle — emit the missing
|
||||
// Immediate-completion lifecycle — emit the missing
|
||||
// Attempted + CachedResolve rows when the underlying write resolved
|
||||
// without engaging the store-and-forward retry loop (immediate
|
||||
// success or a synchronous permanent failure).
|
||||
@@ -1673,7 +1670,7 @@ public class ScriptRuntimeContext
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M2.3 (#7): best-effort emission of the immediate-completion lifecycle
|
||||
/// Best-effort emission of the immediate-completion lifecycle
|
||||
/// for a <c>Database.CachedWrite</c> that resolved without the S&F
|
||||
/// retry loop — emits an <c>Attempted</c> row then a terminal
|
||||
/// <c>CachedResolve</c> row (<c>Delivered</c> on success, <c>Failed</c> on
|
||||
@@ -1826,7 +1823,7 @@ public class ScriptRuntimeContext
|
||||
// (TrackedOperationId); ExecutionId = per-execution id.
|
||||
correlationId: trackedId.Value,
|
||||
executionId: _executionId,
|
||||
// Audit Log #23 (ParentExecutionId): the spawning
|
||||
// (ParentExecutionId): the spawning
|
||||
// execution's id; null for non-routed runs.
|
||||
parentExecutionId: _parentExecutionId,
|
||||
sourceSiteId: string.IsNullOrEmpty(_siteId) ? null : _siteId,
|
||||
@@ -1837,7 +1834,7 @@ public class ScriptRuntimeContext
|
||||
Channel: "DbOutbound",
|
||||
Target: target,
|
||||
SourceSite: _siteId,
|
||||
// SourceNode-stamping (Task 14): the local node name
|
||||
// SourceNode-stamping: the local node name
|
||||
// (node-a/node-b) — threaded through INodeIdentityProvider
|
||||
// at the ScriptExecutionActor; null when no provider was
|
||||
// wired so central persists SiteCalls.SourceNode as NULL.
|
||||
@@ -1892,31 +1889,30 @@ public class ScriptRuntimeContext
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23: the per-execution id for this script run, stamped
|
||||
/// The per-execution id for this script run, stamped
|
||||
/// into <c>AuditEvent.ExecutionId</c> on the <c>NotifySend</c> row.
|
||||
/// </summary>
|
||||
private readonly Guid _executionId;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (ParentExecutionId): the spawning execution's id when
|
||||
/// (ParentExecutionId): the spawning execution's id when
|
||||
/// this run was inbound-API-routed; <c>null</c> for non-routed runs.
|
||||
/// Threaded alongside <see cref="_executionId"/> ready for the Task 5
|
||||
/// emitter — no audit row carries it yet.
|
||||
/// Threaded alongside <see cref="_executionId"/> for audit-row stamping.
|
||||
/// </summary>
|
||||
private readonly Guid? _parentExecutionId;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (M4 Bundle C): best-effort emitter for the
|
||||
/// Best-effort emitter for the
|
||||
/// <c>Notification</c>/<c>NotifySend</c> row produced when the script
|
||||
/// calls <c>Notify.To(list).Send(...)</c>. Optional — when null the
|
||||
/// <see cref="NotifyTarget"/> degrades to a no-op audit path so tests
|
||||
/// / minimal hosts that don't wire AddAuditLog still work (mirrors the
|
||||
/// M2 Bundle F <c>IExternalSystemClient</c> wrapper).
|
||||
/// <c>IExternalSystemClient</c> wrapper).
|
||||
/// </summary>
|
||||
private readonly IAuditWriter? _auditWriter;
|
||||
|
||||
/// <summary>
|
||||
/// SourceNode-stamping (Task 13): the cluster node name on which this
|
||||
/// SourceNode-stamping: the cluster node name on which this
|
||||
/// script is executing — <c>node-a</c>/<c>node-b</c>. Stamped onto
|
||||
/// <c>NotificationSubmit.SourceNode</c> by <see cref="NotifyTarget.Send"/>
|
||||
/// so central can persist it on the <c>Notifications</c> row.
|
||||
@@ -1975,16 +1971,16 @@ public class ScriptRuntimeContext
|
||||
{
|
||||
return new NotifyTarget(
|
||||
listName, _storeAndForward, _siteId, _instanceName, _sourceScript, _logger,
|
||||
// Audit Log #23: the per-execution id stamped into the
|
||||
// The per-execution id stamped into the
|
||||
// NotifySend row's ExecutionId column.
|
||||
_executionId,
|
||||
// Audit Log #23 (M4 Bundle C): forward the writer so Send()
|
||||
// Forward the writer so Send()
|
||||
// can emit one NotifySend(Submitted) row per accepted submission.
|
||||
_auditWriter,
|
||||
// Audit Log #23 (ParentExecutionId): the spawning execution's
|
||||
// (ParentExecutionId): the spawning execution's
|
||||
// id, threaded alongside _executionId. Null for non-routed runs.
|
||||
_parentExecutionId,
|
||||
// SourceNode-stamping (Task 13): the local node name, stamped
|
||||
// SourceNode-stamping: the local node name, stamped
|
||||
// onto NotificationSubmit.SourceNode in Send().
|
||||
_sourceNode);
|
||||
}
|
||||
@@ -2064,21 +2060,20 @@ public class ScriptRuntimeContext
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23: the per-execution id for this script run, stamped
|
||||
/// The per-execution id for this script run, stamped
|
||||
/// into <c>AuditEvent.ExecutionId</c> on the <c>NotifySend</c> row.
|
||||
/// </summary>
|
||||
private readonly Guid _executionId;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (ParentExecutionId): the spawning execution's id when
|
||||
/// (ParentExecutionId): the spawning execution's id when
|
||||
/// this run was inbound-API-routed; <c>null</c> for non-routed runs.
|
||||
/// Threaded alongside <see cref="_executionId"/> ready for the Task 5
|
||||
/// emitter — no audit row carries it yet.
|
||||
/// Threaded alongside <see cref="_executionId"/> for audit-row stamping.
|
||||
/// </summary>
|
||||
private readonly Guid? _parentExecutionId;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (M4 Bundle C): best-effort emitter for the
|
||||
/// Best-effort emitter for the
|
||||
/// <c>Notification</c>/<c>NotifySend</c> row written immediately after
|
||||
/// the underlying S&F enqueue accepts the submission. Optional —
|
||||
/// when null no audit row is emitted (no-op path).
|
||||
@@ -2086,7 +2081,7 @@ public class ScriptRuntimeContext
|
||||
private readonly IAuditWriter? _auditWriter;
|
||||
|
||||
/// <summary>
|
||||
/// SourceNode-stamping (Task 13): the cluster node name on which this
|
||||
/// SourceNode-stamping: the cluster node name on which this
|
||||
/// script is executing (<c>node-a</c>/<c>node-b</c>). Stamped onto the
|
||||
/// <c>NotificationSubmit.SourceNode</c> field in <see cref="Send"/> so
|
||||
/// the central <c>NotificationOutboxActor</c> can persist it on the
|
||||
@@ -2175,18 +2170,18 @@ public class ScriptRuntimeContext
|
||||
// central audit trail. Null when no single script owns the context.
|
||||
SourceScript: _sourceScript,
|
||||
SiteEnqueuedAt: DateTimeOffset.UtcNow,
|
||||
// OriginExecutionId (Audit Log #23): the SAME per-execution id stamped
|
||||
// OriginExecutionId: the SAME per-execution id stamped
|
||||
// onto this run's NotifySend audit row. It rides inside the serialized
|
||||
// payload through the S&F buffer to central, where the dispatcher echoes
|
||||
// it onto the NotifyDeliver rows so all rows for one run share an id.
|
||||
OriginExecutionId: _executionId,
|
||||
// OriginParentExecutionId (Audit Log #23): the SAME parent-execution id
|
||||
// OriginParentExecutionId: the SAME parent-execution id
|
||||
// stamped onto this run's NotifySend audit row — the spawning run's id
|
||||
// for an inbound-API-routed execution, null otherwise. It rides through
|
||||
// the S&F buffer to central, where the dispatcher echoes it onto the
|
||||
// NotifyDeliver rows so the central rows carry the routed run's parent id.
|
||||
OriginParentExecutionId: _parentExecutionId,
|
||||
// SourceNode-stamping (Task 13): the cluster node name on which this
|
||||
// SourceNode-stamping: the cluster node name on which this
|
||||
// notification was emitted (node-a/node-b). Stamped from the local
|
||||
// INodeIdentityProvider via ScriptExecutionActor. Rides inside the
|
||||
// serialized payload through the S&F buffer to central, where
|
||||
@@ -2210,7 +2205,7 @@ public class ScriptRuntimeContext
|
||||
"Notify enqueued notification {NotificationId} to list '{List}' for central delivery",
|
||||
notificationId, _listName);
|
||||
|
||||
// Audit Log #23 (M4 Bundle C): emit one Notification/NotifySend
|
||||
// Emit one Notification/NotifySend
|
||||
// (Submitted) row per accepted submission. The emission is wired
|
||||
// AFTER the EnqueueAsync returns so we only audit submissions the
|
||||
// S&F engine accepted — a failed enqueue throws, never produces an
|
||||
@@ -2248,7 +2243,7 @@ public class ScriptRuntimeContext
|
||||
// mints the id via Guid.NewGuid().ToString("N") so the parse
|
||||
// is expected to succeed; on the off-chance the format
|
||||
// changes / a caller injects an unparseable value, leave it
|
||||
// null per Bundle B's pattern rather than fail the emission.
|
||||
// null rather than fail the emission.
|
||||
Guid? correlationId = Guid.TryParse(notificationId, out var parsed) ? parsed : (Guid?)null;
|
||||
|
||||
// Capture the request summary — {"subject": "...", "body": "..."}.
|
||||
@@ -2274,7 +2269,7 @@ public class ScriptRuntimeContext
|
||||
// lifecycle id; ExecutionId carries the per-execution id.
|
||||
correlationId: correlationId,
|
||||
executionId: _executionId,
|
||||
// Audit Log #23 (ParentExecutionId): the spawning
|
||||
// (ParentExecutionId): the spawning
|
||||
// execution's id; null for non-routed runs.
|
||||
parentExecutionId: _parentExecutionId,
|
||||
sourceSiteId: string.IsNullOrEmpty(_siteId) ? null : _siteId,
|
||||
@@ -2338,7 +2333,7 @@ public class ScriptRuntimeContext
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (M3): script-side accessor for cached-operation tracking.
|
||||
/// Script-side accessor for cached-operation tracking.
|
||||
/// <c>Tracking.Status(trackedOperationId)</c> reads the site-local SQLite
|
||||
/// row directly via <see cref="IOperationTrackingStore.GetStatusAsync"/> —
|
||||
/// the site is the single source of truth for cached-call status, so no
|
||||
|
||||
Reference in New Issue
Block a user