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:
Joseph Doherty
2026-05-16 19:57:28 -04:00
parent 1ae11d1135
commit 09b4bd5dfa
9 changed files with 575 additions and 52 deletions

View File

@@ -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>