Files
scadalink-design/src/ScadaLink.StoreAndForward/ParkedMessageHandlerActor.cs

161 lines
6.8 KiB
C#

using System.Text.Json;
using Akka.Actor;
using Akka.Event;
using ScadaLink.Commons.Messages.RemoteQuery;
namespace ScadaLink.StoreAndForward;
/// <summary>
/// Akka actor bridge for <see cref="StoreAndForwardService"/> parked-message operations.
/// Receives Query/Retry/Discard requests from the SiteCommunicationActor and replies
/// with the matching response records.
/// </summary>
public class ParkedMessageHandlerActor : ReceiveActor
{
private readonly ILoggingAdapter _log = Context.GetLogger();
private readonly StoreAndForwardService _service;
private readonly string _siteId;
public ParkedMessageHandlerActor(StoreAndForwardService service, string siteId)
{
_service = service;
_siteId = siteId;
Receive<ParkedMessageQueryRequest>(HandleQuery);
Receive<ParkedMessageRetryRequest>(HandleRetry);
Receive<ParkedMessageDiscardRequest>(HandleDiscard);
// Task 5 (#22): central→site Retry/Discard relay for parked cached
// operations. The cached call's S&F buffer message id is the
// TrackedOperationId, so these reuse the same parked-message primitive
// as HandleRetry/HandleDiscard, keyed off the tracked id.
Receive<RetryParkedOperation>(HandleRetryParkedOperation);
Receive<DiscardParkedOperation>(HandleDiscardParkedOperation);
}
private void HandleQuery(ParkedMessageQueryRequest msg)
{
var sender = Sender;
var siteId = _siteId;
// StoreAndForward-007: idiomatic PipeTo with explicit success/failure
// projections instead of ContinueWith. Both projections touch only locals
// (captured before the await), so they are safe to run off the actor thread.
_service.GetParkedMessagesAsync(category: null, msg.PageNumber, msg.PageSize)
.PipeTo(
sender,
success: result =>
{
var entries = result.Messages
.Select(m => new ParkedMessageEntry(
MessageId: m.Id,
TargetSystem: m.Target,
MethodName: ExtractMethodName(m.PayloadJson, m.Category),
ErrorMessage: m.LastError ?? string.Empty,
AttemptCount: m.RetryCount,
OriginalTimestamp: m.CreatedAt,
LastAttemptTimestamp: m.LastAttemptAt ?? m.CreatedAt,
MaxAttempts: m.MaxRetries,
Category: m.Category,
OriginInstance: m.OriginInstanceName))
.ToList();
return new ParkedMessageQueryResponse(
msg.CorrelationId, siteId, entries, result.TotalCount,
msg.PageNumber, msg.PageSize, true, null, DateTimeOffset.UtcNow);
},
failure: ex => new ParkedMessageQueryResponse(
msg.CorrelationId, siteId, [], 0, msg.PageNumber, msg.PageSize,
false, ex.GetBaseException().Message, DateTimeOffset.UtcNow));
}
private void HandleRetry(ParkedMessageRetryRequest msg)
{
var sender = Sender;
_service.RetryParkedMessageAsync(msg.MessageId)
.PipeTo(
sender,
success: retried => new ParkedMessageRetryResponse(
msg.CorrelationId, retried,
retried ? null : "Message not found or no longer parked."),
failure: ex => new ParkedMessageRetryResponse(
msg.CorrelationId, false, ex.GetBaseException().Message));
}
private void HandleDiscard(ParkedMessageDiscardRequest msg)
{
var sender = Sender;
_service.DiscardParkedMessageAsync(msg.MessageId)
.PipeTo(
sender,
success: discarded => new ParkedMessageDiscardResponse(
msg.CorrelationId, discarded,
discarded ? null : "Message not found or no longer parked."),
failure: ex => new ParkedMessageDiscardResponse(
msg.CorrelationId, false, ex.GetBaseException().Message));
}
/// <summary>
/// Task 5 (#22): executes a central-relayed Retry of a parked cached call.
/// The tracked id is the S&amp;F buffer message id, so this reuses
/// <see cref="StoreAndForwardService.RetryParkedMessageAsync"/> — which only
/// touches rows that are actually <c>Parked</c> (a non-parked or unknown
/// operation yields <c>false</c>, a safe no-op). Central never mutates the
/// central <c>SiteCalls</c> mirror; the reset row's corrected state flows
/// back via the normal cached-call telemetry path.
/// </summary>
private void HandleRetryParkedOperation(RetryParkedOperation msg)
{
var sender = Sender;
_service.RetryParkedMessageAsync(msg.TrackedOperationId.ToString())
.PipeTo(
sender,
success: applied => new ParkedOperationActionAck(
msg.CorrelationId, applied, ErrorMessage: null),
failure: ex => new ParkedOperationActionAck(
msg.CorrelationId, Applied: false, ex.GetBaseException().Message));
}
/// <summary>
/// Task 5 (#22): executes a central-relayed Discard of a parked cached call.
/// Mirrors <see cref="HandleRetryParkedOperation"/>; Discard removes the
/// parked S&amp;F buffer row (only when it is actually <c>Parked</c>).
/// </summary>
private void HandleDiscardParkedOperation(DiscardParkedOperation msg)
{
var sender = Sender;
_service.DiscardParkedMessageAsync(msg.TrackedOperationId.ToString())
.PipeTo(
sender,
success: applied => new ParkedOperationActionAck(
msg.CorrelationId, applied, ErrorMessage: null),
failure: ex => new ParkedOperationActionAck(
msg.CorrelationId, Applied: false, ex.GetBaseException().Message));
}
private static string ExtractMethodName(string payloadJson, Commons.Types.Enums.StoreAndForwardCategory category)
{
if (string.IsNullOrEmpty(payloadJson))
return category.ToString();
try
{
using var doc = JsonDocument.Parse(payloadJson);
var root = doc.RootElement;
if (root.TryGetProperty("MethodName", out var method) && method.ValueKind == JsonValueKind.String)
return method.GetString() ?? category.ToString();
if (root.TryGetProperty("Subject", out var subject) && subject.ValueKind == JsonValueKind.String)
return subject.GetString() ?? category.ToString();
}
catch (JsonException)
{
}
return category.ToString();
}
}