Add Galaxy platform scope filter so multi-node deployments can restrict the OPC UA address space to only objects hosted by the local platform, reducing memory footprint and MXAccess subscription count from the full Galaxy (49 objects / 4206 attributes) down to the local subtree (3 objects / 386 attributes on the dev Galaxy).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -109,6 +109,22 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration
|
||||
SanitizeConnectionString(config.GalaxyRepository.ConnectionString), config.GalaxyRepository.ChangeDetectionIntervalSeconds,
|
||||
config.GalaxyRepository.CommandTimeoutSeconds, config.GalaxyRepository.ExtendedAttributes);
|
||||
|
||||
var effectivePlatformName = string.IsNullOrWhiteSpace(config.GalaxyRepository.PlatformName)
|
||||
? Environment.MachineName
|
||||
: config.GalaxyRepository.PlatformName;
|
||||
Log.Information(
|
||||
"GalaxyRepository.Scope={Scope}, PlatformName={PlatformName}",
|
||||
config.GalaxyRepository.Scope,
|
||||
config.GalaxyRepository.Scope == GalaxyScope.LocalPlatform
|
||||
? effectivePlatformName
|
||||
: "(n/a)");
|
||||
|
||||
if (config.GalaxyRepository.Scope == GalaxyScope.LocalPlatform &&
|
||||
string.IsNullOrWhiteSpace(config.GalaxyRepository.PlatformName))
|
||||
Log.Information(
|
||||
"GalaxyRepository.PlatformName not set — using Environment.MachineName '{MachineName}'",
|
||||
Environment.MachineName);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.GalaxyRepository.ConnectionString))
|
||||
{
|
||||
Log.Error("GalaxyRepository.ConnectionString must not be empty");
|
||||
|
||||
@@ -25,5 +25,18 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration
|
||||
/// Gets or sets a value indicating whether extended Galaxy attribute metadata should be loaded into the OPC UA model.
|
||||
/// </summary>
|
||||
public bool ExtendedAttributes { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the scope of Galaxy objects loaded into the OPC UA address space.
|
||||
/// <c>Galaxy</c> loads all deployed objects (default). <c>LocalPlatform</c> loads only
|
||||
/// objects hosted by the platform deployed on this machine.
|
||||
/// </summary>
|
||||
public GalaxyScope Scope { get; set; } = GalaxyScope.Galaxy;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an explicit platform node name for <see cref="GalaxyScope.LocalPlatform" /> filtering.
|
||||
/// When <see langword="null" />, the local machine name (<c>Environment.MachineName</c>) is used.
|
||||
/// </summary>
|
||||
public string? PlatformName { get; set; }
|
||||
}
|
||||
}
|
||||
18
src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/GalaxyScope.cs
Normal file
18
src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/GalaxyScope.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Controls how much of the Galaxy object hierarchy is loaded into the OPC UA address space.
|
||||
/// </summary>
|
||||
public enum GalaxyScope
|
||||
{
|
||||
/// <summary>
|
||||
/// Load all deployed objects from the entire Galaxy (default, backward-compatible behavior).
|
||||
/// </summary>
|
||||
Galaxy,
|
||||
|
||||
/// <summary>
|
||||
/// Load only objects hosted by the local platform and the structural areas needed to reach them.
|
||||
/// </summary>
|
||||
LocalPlatform
|
||||
}
|
||||
}
|
||||
18
src/ZB.MOM.WW.LmxOpcUa.Host/Domain/PlatformInfo.cs
Normal file
18
src/ZB.MOM.WW.LmxOpcUa.Host/Domain/PlatformInfo.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps a deployed Galaxy platform to the hostname where it executes.
|
||||
/// </summary>
|
||||
public class PlatformInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the gobject_id of the platform object in the Galaxy repository.
|
||||
/// </summary>
|
||||
public int GobjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the hostname (node_name) where the platform is deployed.
|
||||
/// </summary>
|
||||
public string NodeName { get; set; } = "";
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository
|
||||
|
||||
private readonly GalaxyRepositoryConfiguration _config;
|
||||
|
||||
/// <summary>
|
||||
/// When <see cref="Configuration.GalaxyScope.LocalPlatform" /> filtering is active, caches the set of
|
||||
/// gobject_ids that passed the hierarchy filter so <see cref="GetAttributesAsync" /> can apply the same scope.
|
||||
/// Populated by <see cref="GetHierarchyAsync" /> and consumed by <see cref="GetAttributesAsync" />.
|
||||
/// </summary>
|
||||
private HashSet<int>? _scopeFilteredGobjectIds;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new repository service that reads Galaxy metadata from the configured SQL database.
|
||||
/// </summary>
|
||||
@@ -77,6 +84,18 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository
|
||||
else
|
||||
Log.Information("GetHierarchyAsync returned {Count} objects", results.Count);
|
||||
|
||||
if (_config.Scope == GalaxyScope.LocalPlatform)
|
||||
{
|
||||
var platforms = await GetPlatformsAsync(ct);
|
||||
var platformName = string.IsNullOrWhiteSpace(_config.PlatformName)
|
||||
? Environment.MachineName
|
||||
: _config.PlatformName;
|
||||
var (filtered, gobjectIds) = PlatformScopeFilter.Filter(results, platforms, platformName);
|
||||
_scopeFilteredGobjectIds = gobjectIds;
|
||||
return filtered;
|
||||
}
|
||||
|
||||
_scopeFilteredGobjectIds = null;
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -102,6 +121,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository
|
||||
|
||||
Log.Information("GetAttributesAsync returned {Count} attributes (extended={Extended})", results.Count,
|
||||
extended);
|
||||
|
||||
if (_config.Scope == GalaxyScope.LocalPlatform && _scopeFilteredGobjectIds != null)
|
||||
return PlatformScopeFilter.FilterAttributes(results, _scopeFilteredGobjectIds);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -147,6 +170,33 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queries the platform table for deployed platform-to-hostname mappings used by
|
||||
/// <see cref="Configuration.GalaxyScope.LocalPlatform" /> filtering.
|
||||
/// </summary>
|
||||
private async Task<List<PlatformInfo>> GetPlatformsAsync(CancellationToken ct = default)
|
||||
{
|
||||
var results = new List<PlatformInfo>();
|
||||
|
||||
using var conn = new SqlConnection(_config.ConnectionString);
|
||||
await conn.OpenAsync(ct);
|
||||
|
||||
using var cmd = new SqlCommand(PlatformLookupSql, conn) { CommandTimeout = _config.CommandTimeoutSeconds };
|
||||
using var reader = await cmd.ExecuteReaderAsync(ct);
|
||||
|
||||
while (await reader.ReadAsync(ct))
|
||||
{
|
||||
results.Add(new PlatformInfo
|
||||
{
|
||||
GobjectId = Convert.ToInt32(reader.GetValue(0)),
|
||||
NodeName = reader.IsDBNull(1) ? "" : reader.GetString(1)
|
||||
});
|
||||
}
|
||||
|
||||
Log.Information("GetPlatformsAsync returned {Count} platform(s)", results.Count);
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a row from the standard attributes query (12 columns).
|
||||
/// Columns: gobject_id, tag_name, attribute_name, full_tag_reference, mx_data_type,
|
||||
@@ -464,6 +514,12 @@ FROM (
|
||||
) all_attributes
|
||||
ORDER BY tag_name, primitive_name, attribute_name";
|
||||
|
||||
private const string PlatformLookupSql = @"
|
||||
SELECT p.platform_gobject_id, p.node_name
|
||||
FROM platform p
|
||||
INNER JOIN gobject g ON g.gobject_id = p.platform_gobject_id
|
||||
WHERE g.is_template = 0 AND g.deployed_package_id <> 0";
|
||||
|
||||
private const string ChangeDetectionSql = "SELECT time_of_last_deploy FROM galaxy";
|
||||
|
||||
private const string TestConnectionSql = "SELECT 1";
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Serilog;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Filters a Galaxy object hierarchy to retain only objects hosted by a specific platform
|
||||
/// and the structural areas needed to keep the browse tree connected.
|
||||
/// </summary>
|
||||
public static class PlatformScopeFilter
|
||||
{
|
||||
private static readonly ILogger Log = Serilog.Log.ForContext(typeof(PlatformScopeFilter));
|
||||
|
||||
private const int CategoryWinPlatform = 1;
|
||||
private const int CategoryAppEngine = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Filters the hierarchy to objects hosted by the platform whose <c>node_name</c> matches
|
||||
/// <paramref name="platformName" />, plus ancestor areas that keep the tree connected.
|
||||
/// </summary>
|
||||
/// <param name="hierarchy">The full Galaxy object hierarchy.</param>
|
||||
/// <param name="platforms">Deployed platform-to-hostname mappings from the <c>platform</c> table.</param>
|
||||
/// <param name="platformName">The target hostname to match (case-insensitive).</param>
|
||||
/// <returns>
|
||||
/// The filtered hierarchy and the set of included gobject_ids (for attribute filtering).
|
||||
/// When no matching platform is found, returns an empty list and empty set.
|
||||
/// </returns>
|
||||
public static (List<GalaxyObjectInfo> Hierarchy, HashSet<int> GobjectIds) Filter(
|
||||
List<GalaxyObjectInfo> hierarchy,
|
||||
List<PlatformInfo> platforms,
|
||||
string platformName)
|
||||
{
|
||||
// Find the platform gobject_id that matches the target hostname.
|
||||
var matchingPlatform = platforms.FirstOrDefault(
|
||||
p => string.Equals(p.NodeName, platformName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (matchingPlatform == null)
|
||||
{
|
||||
Log.Warning(
|
||||
"Scope filter found no deployed platform matching node name '{PlatformName}'; " +
|
||||
"available platforms: [{Available}]",
|
||||
platformName,
|
||||
string.Join(", ", platforms.Select(p => $"{p.NodeName} (gobject_id={p.GobjectId})")));
|
||||
return (new List<GalaxyObjectInfo>(), new HashSet<int>());
|
||||
}
|
||||
|
||||
var platformGobjectId = matchingPlatform.GobjectId;
|
||||
Log.Information(
|
||||
"Scope filter targeting platform '{PlatformName}' (gobject_id={GobjectId})",
|
||||
platformName, platformGobjectId);
|
||||
|
||||
// Build a lookup for the hierarchy by gobject_id.
|
||||
var byId = hierarchy.ToDictionary(o => o.GobjectId);
|
||||
|
||||
// Step 1: Collect all host gobject_ids under this platform.
|
||||
// Walk outward from the platform to find AppEngines (and any deeper hosting objects).
|
||||
var hostIds = new HashSet<int> { platformGobjectId };
|
||||
bool changed;
|
||||
do
|
||||
{
|
||||
changed = false;
|
||||
foreach (var obj in hierarchy)
|
||||
{
|
||||
if (hostIds.Contains(obj.GobjectId))
|
||||
continue;
|
||||
if (obj.HostedByGobjectId != 0 && hostIds.Contains(obj.HostedByGobjectId)
|
||||
&& (obj.CategoryId == CategoryAppEngine || obj.CategoryId == CategoryWinPlatform))
|
||||
{
|
||||
hostIds.Add(obj.GobjectId);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
} while (changed);
|
||||
|
||||
// Step 2: Include all non-area objects hosted by any host in the set, plus the hosts themselves.
|
||||
var includedIds = new HashSet<int>(hostIds);
|
||||
foreach (var obj in hierarchy)
|
||||
{
|
||||
if (includedIds.Contains(obj.GobjectId))
|
||||
continue;
|
||||
if (!obj.IsArea && obj.HostedByGobjectId != 0 && hostIds.Contains(obj.HostedByGobjectId))
|
||||
includedIds.Add(obj.GobjectId);
|
||||
}
|
||||
|
||||
// Step 3: Walk ParentGobjectId chains upward to include ancestor areas so the tree stays connected.
|
||||
var toWalk = new Queue<int>(includedIds);
|
||||
while (toWalk.Count > 0)
|
||||
{
|
||||
var id = toWalk.Dequeue();
|
||||
if (!byId.TryGetValue(id, out var obj))
|
||||
continue;
|
||||
var parentId = obj.ParentGobjectId;
|
||||
if (parentId != 0 && byId.ContainsKey(parentId) && includedIds.Add(parentId))
|
||||
toWalk.Enqueue(parentId);
|
||||
}
|
||||
|
||||
// Step 4: Return the filtered hierarchy preserving original order.
|
||||
var filtered = hierarchy.Where(o => includedIds.Contains(o.GobjectId)).ToList();
|
||||
|
||||
Log.Information(
|
||||
"Scope filter retained {FilteredCount} of {TotalCount} objects for platform '{PlatformName}'",
|
||||
filtered.Count, hierarchy.Count, platformName);
|
||||
|
||||
return (filtered, includedIds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters attributes to retain only those belonging to objects in the given set.
|
||||
/// </summary>
|
||||
public static List<GalaxyAttributeInfo> FilterAttributes(
|
||||
List<GalaxyAttributeInfo> attributes,
|
||||
HashSet<int> gobjectIds)
|
||||
{
|
||||
var filtered = attributes.Where(a => gobjectIds.Contains(a.GobjectId)).ToList();
|
||||
Log.Information(
|
||||
"Scope filter retained {FilteredCount} of {TotalCount} attributes",
|
||||
filtered.Count, attributes.Count);
|
||||
return filtered;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,9 @@
|
||||
"ConnectionString": "Server=localhost;Database=ZB;Integrated Security=true;",
|
||||
"ChangeDetectionIntervalSeconds": 30,
|
||||
"CommandTimeoutSeconds": 30,
|
||||
"ExtendedAttributes": false
|
||||
"ExtendedAttributes": false,
|
||||
"Scope": "Galaxy",
|
||||
"PlatformName": null
|
||||
},
|
||||
"Dashboard": {
|
||||
"Enabled": true,
|
||||
|
||||
Reference in New Issue
Block a user