fix(site-runtime): resolve SiteRuntime-001/002/003 — route data-sourced writes to DCL, real per-attribute API results, race-free redeploy
This commit is contained in:
@@ -198,10 +198,44 @@ public class InstanceActor : ReceiveActor
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a static attribute in memory and persists the override to SQLite.
|
||||
/// Handles an attribute write (<c>Instance.SetAttribute</c> / Inbound API).
|
||||
/// WP-24: State mutation serialized through this actor's mailbox.
|
||||
///
|
||||
/// The write is routed by the attribute's data binding:
|
||||
/// * Data-sourced attribute → forwards a <see cref="WriteTagRequest"/> to the
|
||||
/// DCL, which writes the physical device. The in-memory value is NOT
|
||||
/// optimistically updated and NO static override is persisted — the
|
||||
/// confirmed device value arrives later via the subscription. Success or
|
||||
/// failure of the device write is returned to the caller.
|
||||
/// * Static attribute → updates the in-memory value and persists the override
|
||||
/// to SQLite.
|
||||
///
|
||||
/// Either way the caller receives a <see cref="SetStaticAttributeResponse"/>.
|
||||
/// </summary>
|
||||
private void HandleSetStaticAttribute(SetStaticAttributeCommand command)
|
||||
{
|
||||
// Resolve the target attribute's data binding from the flattened config.
|
||||
var resolved = _configuration?.Attributes
|
||||
.FirstOrDefault(a => a.CanonicalName == command.AttributeName);
|
||||
|
||||
var isDataSourced = resolved != null
|
||||
&& !string.IsNullOrEmpty(resolved.DataSourceReference)
|
||||
&& !string.IsNullOrEmpty(resolved.BoundDataConnectionName);
|
||||
|
||||
if (isDataSourced)
|
||||
{
|
||||
HandleSetDataAttribute(command, resolved!);
|
||||
return;
|
||||
}
|
||||
|
||||
HandleSetStaticAttributeCore(command);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static attribute write: updates in-memory state, publishes the change,
|
||||
/// persists the override to SQLite, and replies with success.
|
||||
/// </summary>
|
||||
private void HandleSetStaticAttributeCore(SetStaticAttributeCommand command)
|
||||
{
|
||||
_attributes[command.AttributeName] = command.Value;
|
||||
|
||||
@@ -216,8 +250,7 @@ public class InstanceActor : ReceiveActor
|
||||
|
||||
PublishAndNotifyChildren(changed);
|
||||
|
||||
// Persist asynchronously -- fire and forget since the actor is the source of truth
|
||||
// and SetAttribute is called from scripts via Tell (no response consumer).
|
||||
// Persist asynchronously -- fire and forget since the actor is the source of truth.
|
||||
var instanceName = _instanceUniqueName;
|
||||
var attributeName = command.AttributeName;
|
||||
var logger = _logger;
|
||||
@@ -230,6 +263,58 @@ public class InstanceActor : ReceiveActor
|
||||
instanceName,
|
||||
attributeName);
|
||||
}, TaskContinuationOptions.OnlyOnFaulted);
|
||||
|
||||
Sender.Tell(new SetStaticAttributeResponse(
|
||||
command.CorrelationId, _instanceUniqueName, command.AttributeName,
|
||||
true, null, DateTimeOffset.UtcNow));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data-sourced attribute write: forwards a write request to the DCL and pipes
|
||||
/// the device write result back to the caller. The in-memory value is left
|
||||
/// untouched (it is refreshed by the subscription when the device confirms);
|
||||
/// no static override is persisted for a data-sourced attribute.
|
||||
/// </summary>
|
||||
private void HandleSetDataAttribute(SetStaticAttributeCommand command, ResolvedAttribute resolved)
|
||||
{
|
||||
var caller = Sender;
|
||||
var correlationId = command.CorrelationId;
|
||||
var attributeName = command.AttributeName;
|
||||
var instanceName = _instanceUniqueName;
|
||||
|
||||
if (_dclManager == null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"SetAttribute on data-sourced attribute {Instance}.{Attribute} cannot be routed — no DCL manager configured",
|
||||
instanceName, attributeName);
|
||||
caller.Tell(new SetStaticAttributeResponse(
|
||||
correlationId, instanceName, attributeName, false,
|
||||
"Data Connection Layer not available for write.", DateTimeOffset.UtcNow));
|
||||
return;
|
||||
}
|
||||
|
||||
var writeRequest = new WriteTagRequest(
|
||||
correlationId,
|
||||
resolved.BoundDataConnectionName!,
|
||||
resolved.DataSourceReference!,
|
||||
command.Value,
|
||||
DateTimeOffset.UtcNow);
|
||||
|
||||
// Ask the DCL and pipe the result back to the original caller. The DCL
|
||||
// returns the failure synchronously so the script can handle it.
|
||||
_dclManager.Ask<WriteTagResponse>(writeRequest, TimeSpan.FromSeconds(30))
|
||||
.ContinueWith(t =>
|
||||
{
|
||||
if (t.IsCompletedSuccessfully)
|
||||
return new SetStaticAttributeResponse(
|
||||
correlationId, instanceName, attributeName,
|
||||
t.Result.Success, t.Result.ErrorMessage, DateTimeOffset.UtcNow);
|
||||
|
||||
return new SetStaticAttributeResponse(
|
||||
correlationId, instanceName, attributeName, false,
|
||||
t.Exception?.GetBaseException().Message ?? "DCL write timed out",
|
||||
DateTimeOffset.UtcNow);
|
||||
}).PipeTo(caller);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user