fix(siteruntime): MV-8 review fixes (construct list inside try; dictionary attr lookup; test hygiene)

This commit is contained in:
Joseph Doherty
2026-06-16 15:48:25 -04:00
parent 4765706e94
commit 96e817a7e1
2 changed files with 76 additions and 12 deletions
@@ -60,6 +60,14 @@ public class InstanceActor : ReceiveActor
private readonly Dictionary<string, AlarmStateChanged> _latestAlarmEvents = new();
private FlattenedConfiguration? _configuration;
// MV-8: resolved attributes indexed by canonical name. The TagValueUpdate
// ingest path is the highest-frequency message this actor handles, so the
// attribute lookup must be O(1) rather than a linear scan of
// _configuration.Attributes. Built once in the constructor from the
// deserialized configuration (last-wins on duplicate canonical names,
// mirroring the rest of the actor's by-name dictionaries).
private readonly Dictionary<string, ResolvedAttribute> _resolvedAttributeByName = new();
// DCL manager actor reference for subscribing to tag values
private readonly IActorRef? _dclManager;
// Maps each tag path to every attribute canonical name that references it.
@@ -119,6 +127,10 @@ public class InstanceActor : ReceiveActor
_attributes[attr.CanonicalName] = attr.Value;
_attributeQualities[attr.CanonicalName] =
string.IsNullOrEmpty(attr.DataSourceReference) ? "Good" : "Uncertain";
// MV-8: index resolved attributes for O(1) lookup on the hot
// TagValueUpdate ingest path (last-wins on duplicate names).
_resolvedAttributeByName[attr.CanonicalName] = attr;
}
}
@@ -439,8 +451,8 @@ public class InstanceActor : ReceiveActor
// we resolve and convert per attribute rather than once for the tag.
foreach (var attrName in attrNames)
{
var resolved = _configuration?.Attributes
.FirstOrDefault(a => a.CanonicalName == attrName);
// MV-8: O(1) lookup off the hot ingest path (was a linear FirstOrDefault).
_resolvedAttributeByName.TryGetValue(attrName, out var resolved);
// MV-8: a List-typed attribute coerces the incoming OPC UA array
// (a CLR array/IEnumerable from the SDK) into a typed List<T>. On an
@@ -510,23 +522,32 @@ public class InstanceActor : ReceiveActor
if (incoming is not System.Collections.IEnumerable enumerable || incoming is string)
return false;
var clrType = ListElementClrType(elementType);
var list = (System.Collections.IList)Activator.CreateInstance(
typeof(List<>).MakeGenericType(clrType))!;
try
{
// Construct the typed list INSIDE the try: although the six valid
// element types resolved by ListElementClrType cannot throw today,
// keeping ListElementClrType / MakeGenericType / CreateInstance inside
// the guarded block means any future change that introduces a throw
// here is caught and turned into a Bad-quality result rather than
// escaping into the actor and tripping supervision.
var clrType = ListElementClrType(elementType);
var list = (System.Collections.IList)Activator.CreateInstance(
typeof(List<>).MakeGenericType(clrType))!;
foreach (var element in enumerable)
list.Add(CoerceElement(element, elementType));
typedList = list;
return true;
}
catch (Exception ex) when (ex is FormatException or InvalidCastException
or OverflowException or ArgumentNullException)
catch (Exception ex)
{
// Any coercion / construction failure → Bad quality, never a crash.
_logger.LogWarning(ex,
"Failed to coerce value to List<{Element}> for instance {Instance}; marking quality Bad",
attr.ElementDataType, _instanceUniqueName);
return false;
}
typedList = list;
return true;
}
private static Type ListElementClrType(DataType t) => t switch