chore(management): log debug-stream push failures; fix event-log filter docs; refresh stale SourceNode note (arch-review S5/C7/C8)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -191,7 +191,7 @@ Other peers in the `scadaproj` family (see `scadaproj/CLAUDE.md` for details): `
|
|||||||
- Cookie+JWT hybrid sessions: HttpOnly/Secure cookie carries an embedded JWT (HMAC-SHA256 shared symmetric key), 15-minute expiry with sliding refresh, 30-minute idle timeout. Cookies are the correct transport for Blazor Server (SignalR circuits).
|
- Cookie+JWT hybrid sessions: HttpOnly/Secure cookie carries an embedded JWT (HMAC-SHA256 shared symmetric key), 15-minute expiry with sliding refresh, 30-minute idle timeout. Cookies are the correct transport for Blazor Server (SignalR circuits).
|
||||||
- LDAP failure: new logins fail; active sessions continue with current roles.
|
- LDAP failure: new logins fail; active sessions continue with current roles.
|
||||||
- Load balancer in front of central UI; cookie-embedded JWT + shared Data Protection keys for failover transparency.
|
- Load balancer in front of central UI; cookie-embedded JWT + shared Data Protection keys for failover transparency.
|
||||||
- Two-person MxGateway secured writes (M7): two new global roles — `Operator` (initiates) + `Verifier` (approves) — added alongside the canonical `Administrator`/`Designer`/`Deployer`/`Viewer`, with `RequireOperator`/`RequireVerifier` policies. An Operator submits a secured write from the Central UI Secured Writes page (`/operations/secured-writes`); it stays a `Pending` `PendingSecuredWrite` row until a *distinct* Verifier approves it (no-self-approval enforced server-side in the ManagementActor, plus a compare-and-swap race guard). Approval relays a `WriteTagRequest` to the site MxGateway; MxGateway-protocol connections only; each lifecycle event (submit/approve/reject/execute) emits a best-effort `AuditChannel.SecuredWrite` / `AuditKind.SecuredWrite*` central-direct-write row sharing the row id as `CorrelationId`. (SecuredWrite audit rows currently leave `SourceNode` NULL — a logged follow-up.) Pending secured writes expire server-side after a configurable TTL (`ManagementServiceOptions.SecuredWritePendingTtl`, default 24 h): an overdue `Pending` row is CAS'd to `Expired` (never relayed) — enforced at approve/reject and swept opportunistically on list (arch-review S2, `AuditKind.SecuredWriteExpire`).
|
- Two-person MxGateway secured writes (M7): two new global roles — `Operator` (initiates) + `Verifier` (approves) — added alongside the canonical `Administrator`/`Designer`/`Deployer`/`Viewer`, with `RequireOperator`/`RequireVerifier` policies. An Operator submits a secured write from the Central UI Secured Writes page (`/operations/secured-writes`); it stays a `Pending` `PendingSecuredWrite` row until a *distinct* Verifier approves it (no-self-approval enforced server-side in the ManagementActor, plus a compare-and-swap race guard). Approval relays a `WriteTagRequest` to the site MxGateway; MxGateway-protocol connections only; each lifecycle event (submit/approve/reject/execute) emits a best-effort `AuditChannel.SecuredWrite` / `AuditKind.SecuredWrite*` central-direct-write row sharing the row id as `CorrelationId`. (SecuredWrite audit rows stamp `SourceNode` via `ICentralAuditWriter`/`INodeIdentityProvider`.) Pending secured writes expire server-side after a configurable TTL (`ManagementServiceOptions.SecuredWritePendingTtl`, default 24 h): an overdue `Pending` row is CAS'd to `Expired` (never relayed) — enforced at approve/reject and swept opportunistically on list (arch-review S2, `AuditKind.SecuredWriteExpire`).
|
||||||
|
|
||||||
### Cluster & Failover
|
### Cluster & Failover
|
||||||
- Keep-oldest split-brain resolver with `down-if-alone = on`, 15s stable-after.
|
- Keep-oldest split-brain resolver with `down-if-alone = on`, 15s stable-after.
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.RemoteQuery;
|
|||||||
/// Supports filtering by event type, severity, instance, time range, and keyword search.
|
/// Supports filtering by event type, severity, instance, time range, and keyword search.
|
||||||
/// Uses keyset pagination via continuation token (last event ID).
|
/// Uses keyset pagination via continuation token (last event ID).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="InstanceId">
|
||||||
|
/// Instance filter matched against the site event log's <c>instance_id</c> column,
|
||||||
|
/// which stores the instance <b>UniqueName</b> (InstanceActor.LogLifecycleEvent passes
|
||||||
|
/// <c>_instanceUniqueName</c>; EventLogQueryService matches <c>instance_id = $instanceId</c>).
|
||||||
|
/// </param>
|
||||||
public record EventLogQueryRequest(
|
public record EventLogQueryRequest(
|
||||||
string CorrelationId,
|
string CorrelationId,
|
||||||
string SiteId,
|
string SiteId,
|
||||||
|
|||||||
@@ -198,13 +198,15 @@ public class DebugStreamHub : Hub
|
|||||||
// Use IHubContext for callbacks — the hub instance is transient (disposed after method returns),
|
// Use IHubContext for callbacks — the hub instance is transient (disposed after method returns),
|
||||||
// but IHubContext is a singleton that remains valid for the lifetime of the connection.
|
// but IHubContext is a singleton that remains valid for the lifetime of the connection.
|
||||||
var hubClients = _hubContext.Clients;
|
var hubClients = _hubContext.Clients;
|
||||||
|
// Capture the logger into a local: the callbacks outlive this (transient)
|
||||||
|
// hub instance, and ILogger is a singleton, so this is safe to hold.
|
||||||
|
var logger = _logger;
|
||||||
|
|
||||||
var session = await _debugStreamService.StartStreamAsync(
|
var session = await _debugStreamService.StartStreamAsync(
|
||||||
instanceId,
|
instanceId,
|
||||||
onEvent: evt =>
|
onEvent: evt =>
|
||||||
{
|
{
|
||||||
// Fire-and-forget — if the client disconnects, SendAsync will fail silently
|
var send = evt switch
|
||||||
_ = evt switch
|
|
||||||
{
|
{
|
||||||
AttributeValueChanged changed =>
|
AttributeValueChanged changed =>
|
||||||
hubClients.Client(connectionId).SendAsync("OnAttributeChanged", changed),
|
hubClients.Client(connectionId).SendAsync("OnAttributeChanged", changed),
|
||||||
@@ -214,10 +216,20 @@ public class DebugStreamHub : Hub
|
|||||||
hubClients.Client(connectionId).SendAsync("OnSnapshot", snapshot),
|
hubClients.Client(connectionId).SendAsync("OnSnapshot", snapshot),
|
||||||
_ => Task.CompletedTask
|
_ => Task.CompletedTask
|
||||||
};
|
};
|
||||||
|
// Fire-and-forget (deliberate — a slow/dead client must not block the
|
||||||
|
// stream), but log the push failure rather than swallowing it silently.
|
||||||
|
_ = send.ContinueWith(
|
||||||
|
t => logger.LogWarning(t.Exception?.GetBaseException(),
|
||||||
|
"Debug stream push to connection {ConnectionId} failed", connectionId),
|
||||||
|
CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);
|
||||||
},
|
},
|
||||||
onTerminated: () =>
|
onTerminated: () =>
|
||||||
{
|
{
|
||||||
_ = hubClients.Client(connectionId).SendAsync("OnStreamTerminated", "Site disconnected");
|
_ = hubClients.Client(connectionId).SendAsync("OnStreamTerminated", "Site disconnected")
|
||||||
|
.ContinueWith(
|
||||||
|
t => logger.LogWarning(t.Exception?.GetBaseException(),
|
||||||
|
"Debug stream termination push to connection {ConnectionId} failed", connectionId),
|
||||||
|
CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);
|
||||||
});
|
});
|
||||||
|
|
||||||
Context.Items[SessionIdKey] = session.SessionId;
|
Context.Items[SessionIdKey] = session.SessionId;
|
||||||
|
|||||||
@@ -3007,7 +3007,10 @@ public class ManagementActor : ReceiveActor
|
|||||||
cmd.SiteIdentifier,
|
cmd.SiteIdentifier,
|
||||||
cmd.From, cmd.To,
|
cmd.From, cmd.To,
|
||||||
cmd.EventType, cmd.Severity,
|
cmd.EventType, cmd.Severity,
|
||||||
cmd.InstanceName, // InstanceId filter
|
// Instance unique-name filter — the site event log's instance_id column
|
||||||
|
// stores the instance UniqueName (InstanceActor.LogLifecycleEvent passes
|
||||||
|
// _instanceUniqueName; EventLogQueryService matches instance_id = $instanceId).
|
||||||
|
cmd.InstanceName,
|
||||||
cmd.Keyword,
|
cmd.Keyword,
|
||||||
null, // ContinuationToken
|
null, // ContinuationToken
|
||||||
cmd.PageSize,
|
cmd.PageSize,
|
||||||
|
|||||||
Reference in New Issue
Block a user