feat(siteruntime): event-driven Attributes.WriteBatchAndWaitAsync (batched DCL write + trigger + existing WaitForAttribute waiter) + compile mirror
This commit is contained in:
@@ -188,6 +188,12 @@ public class InstanceActor : ReceiveActor
|
||||
Receive<WaitForAttributeRequest>(HandleWaitForAttribute);
|
||||
Receive<WaitForAttributeTimeout>(HandleWaitForAttributeTimeout);
|
||||
|
||||
// Batch write + (event-driven) wait: resolves a set of data-sourced
|
||||
// attributes to one DCL connection and forwards a single WriteTagBatchRequest.
|
||||
// Backs the script-facing Attributes.WriteBatchAndWaitAsync helper; the wait
|
||||
// half is the WaitForAttribute waiter above.
|
||||
Receive<WriteAttributeBatchRequest>(HandleWriteAttributeBatch);
|
||||
|
||||
// Handle tag value updates from DCL — convert to AttributeValueChanged
|
||||
Receive<TagValueUpdate>(HandleTagValueUpdate);
|
||||
Receive<SubscribeTagsResponse>(_ => { }); // Ack from DCL subscribe — no action needed
|
||||
@@ -499,6 +505,118 @@ public class InstanceActor : ReceiveActor
|
||||
}).PipeTo(caller);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch write: resolves a SET of data-sourced attributes (and an optional trigger
|
||||
/// attribute) to their device bindings, enforces a single data connection across the
|
||||
/// whole batch, decodes List values (same rule as <see cref="HandleSetDataAttribute"/>),
|
||||
/// and forwards ONE <see cref="WriteTagBatchRequest"/> to the DCL — replacing N
|
||||
/// sequential per-attribute writes with a single gateway round-trip. The write outcome
|
||||
/// is returned synchronously to the caller; the EVENT-DRIVEN wait for the response
|
||||
/// attribute is performed separately by the caller via the existing WaitForAttribute
|
||||
/// waiter (this handler does not wait). Resolution mirrors the data-sourced
|
||||
/// SetAttribute path: an attribute is data-sourced only when it resolves AND has both a
|
||||
/// <see cref="ResolvedAttribute.DataSourceReference"/> and a
|
||||
/// <see cref="ResolvedAttribute.BoundDataConnectionName"/>.
|
||||
/// </summary>
|
||||
private void HandleWriteAttributeBatch(WriteAttributeBatchRequest request)
|
||||
{
|
||||
var caller = Sender;
|
||||
var cid = request.CorrelationId;
|
||||
|
||||
if (_dclManager == null)
|
||||
{
|
||||
caller.Tell(new WriteAttributeBatchResponse(
|
||||
cid, false, "Data Connection Layer not available for write."));
|
||||
return;
|
||||
}
|
||||
|
||||
var values = new Dictionary<string, object?>();
|
||||
string? connName = null;
|
||||
|
||||
foreach (var kv in request.AttributeEncodedValues)
|
||||
{
|
||||
if (!_resolvedAttributeByName.TryGetValue(kv.Key, out var resolved)
|
||||
|| string.IsNullOrEmpty(resolved.DataSourceReference)
|
||||
|| string.IsNullOrEmpty(resolved.BoundDataConnectionName))
|
||||
{
|
||||
caller.Tell(new WriteAttributeBatchResponse(
|
||||
cid, false, $"Attribute '{kv.Key}' is not a data-sourced attribute."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (connName == null)
|
||||
connName = resolved.BoundDataConnectionName;
|
||||
else if (!string.Equals(connName, resolved.BoundDataConnectionName, StringComparison.Ordinal))
|
||||
{
|
||||
caller.Tell(new WriteAttributeBatchResponse(
|
||||
cid, false, "Batch write spans multiple data connections; not supported."));
|
||||
return;
|
||||
}
|
||||
|
||||
object? wv = kv.Value;
|
||||
// MV: a data-sourced List attribute's encoded value is the canonical JSON
|
||||
// array string — decode it to a typed List<T> so the DCL/Variant write
|
||||
// produces a real array (same poison-rejection rule as HandleSetDataAttribute).
|
||||
if (IsListAttribute(resolved) && !string.IsNullOrWhiteSpace(kv.Value))
|
||||
{
|
||||
var decoded = DecodeAttributeValue(resolved, kv.Value);
|
||||
if (decoded == null)
|
||||
{
|
||||
caller.Tell(new WriteAttributeBatchResponse(
|
||||
cid, false, $"Invalid list value for attribute '{kv.Key}'"));
|
||||
return;
|
||||
}
|
||||
wv = decoded;
|
||||
}
|
||||
|
||||
values[resolved.DataSourceReference!] = wv;
|
||||
}
|
||||
|
||||
string? triggerPath = null;
|
||||
object? triggerVal = null;
|
||||
if (!string.IsNullOrEmpty(request.TriggerAttribute))
|
||||
{
|
||||
if (!_resolvedAttributeByName.TryGetValue(request.TriggerAttribute, out var tr)
|
||||
|| string.IsNullOrEmpty(tr.DataSourceReference)
|
||||
|| string.IsNullOrEmpty(tr.BoundDataConnectionName))
|
||||
{
|
||||
caller.Tell(new WriteAttributeBatchResponse(
|
||||
cid, false, $"Trigger attribute '{request.TriggerAttribute}' is not data-sourced."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (connName != null && !string.Equals(connName, tr.BoundDataConnectionName, StringComparison.Ordinal))
|
||||
{
|
||||
caller.Tell(new WriteAttributeBatchResponse(
|
||||
cid, false, "Trigger attribute is on a different data connection."));
|
||||
return;
|
||||
}
|
||||
|
||||
connName ??= tr.BoundDataConnectionName;
|
||||
triggerPath = tr.DataSourceReference;
|
||||
triggerVal = request.TriggerEncodedValue;
|
||||
}
|
||||
|
||||
if (connName == null)
|
||||
{
|
||||
caller.Tell(new WriteAttributeBatchResponse(cid, false, "No attributes to write."));
|
||||
return;
|
||||
}
|
||||
|
||||
var dclReq = new WriteTagBatchRequest(
|
||||
cid, connName!, values, triggerPath, triggerVal, DateTimeOffset.UtcNow);
|
||||
|
||||
// Ask the DCL and pipe the batch outcome back to the original caller. The DCL
|
||||
// bounds its own write with WriteTimeout; this Ask is bounded a bit longer so a
|
||||
// device timeout is surfaced as a DCL failure rather than an Ask timeout.
|
||||
_dclManager.Ask<WriteTagBatchResponse>(dclReq, TimeSpan.FromSeconds(30))
|
||||
.ContinueWith(t => t.IsCompletedSuccessfully
|
||||
? new WriteAttributeBatchResponse(cid, t.Result.Success, t.Result.ErrorMessage)
|
||||
: new WriteAttributeBatchResponse(
|
||||
cid, false, t.Exception?.GetBaseException().Message ?? "DCL batch write timed out"))
|
||||
.PipeTo(caller);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WP-15: Routes script call requests to the appropriate Script Actor.
|
||||
/// Uses Ask pattern (WP-22).
|
||||
|
||||
Reference in New Issue
Block a user