galaxy: add cycle guard to HasMatchingDescendant
This commit is contained in:
@@ -163,10 +163,17 @@ public static class GalaxyBrowseProjector
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Defend against pathological cycles in Galaxy data (e.g. a corrupt A→B→A chain).
|
||||||
|
// BuildContainedPath uses the same visited-id pattern; mirror it so this walk
|
||||||
|
// terminates even when ChildrenByParent forms a cycle.
|
||||||
|
HashSet<int> visited = new() { parent.Object.GobjectId };
|
||||||
Stack<GalaxyObjectView> stack = new();
|
Stack<GalaxyObjectView> stack = new();
|
||||||
foreach (GalaxyObjectView child in children)
|
foreach (GalaxyObjectView child in children)
|
||||||
{
|
{
|
||||||
stack.Push(child);
|
if (visited.Add(child.Object.GobjectId))
|
||||||
|
{
|
||||||
|
stack.Push(child);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
while (stack.Count > 0)
|
while (stack.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -180,7 +187,10 @@ public static class GalaxyBrowseProjector
|
|||||||
{
|
{
|
||||||
foreach (GalaxyObjectView grandchild in grandchildren)
|
foreach (GalaxyObjectView grandchild in grandchildren)
|
||||||
{
|
{
|
||||||
stack.Push(grandchild);
|
if (visited.Add(grandchild.Object.GobjectId))
|
||||||
|
{
|
||||||
|
stack.Push(grandchild);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,6 +223,77 @@ public sealed class GalaxyBrowseProjectorTests
|
|||||||
Assert.Equal("Plant.Line_A", result.Children[0].TagName);
|
Assert.Equal("Plant.Line_A", result.Children[0].TagName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies <see cref="GalaxyBrowseProjector"/> terminates when the Galaxy data
|
||||||
|
/// contains a cyclic parent chain (A→B→C→A). Without the visited-id guard in
|
||||||
|
/// <c>HasMatchingDescendant</c>, the depth-first walk would loop forever; the
|
||||||
|
/// 5-second xUnit timeout asserts termination.
|
||||||
|
/// </summary>
|
||||||
|
[Fact(Timeout = 5000)]
|
||||||
|
public async Task Project_CyclicDescendants_DoesNotInfiniteLoop()
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
// Construct a 3-node cycle: A(10)→B(11)→C(12)→A. Each node's ParentGobjectId
|
||||||
|
// points to the next, so GalaxyHierarchyIndex.ChildrenByParent has
|
||||||
|
// [12] = [A], [10] = [B], [11] = [C].
|
||||||
|
// None of them are historized, so HistorizedOnly=true forces the projector to
|
||||||
|
// call HasMatchingDescendant on every direct child, exercising the cycle walk.
|
||||||
|
GalaxyObject a = new()
|
||||||
|
{
|
||||||
|
GobjectId = 10,
|
||||||
|
ParentGobjectId = 12,
|
||||||
|
ContainedName = "A",
|
||||||
|
BrowseName = "A",
|
||||||
|
TagName = "A",
|
||||||
|
};
|
||||||
|
GalaxyObject b = new()
|
||||||
|
{
|
||||||
|
GobjectId = 11,
|
||||||
|
ParentGobjectId = 10,
|
||||||
|
ContainedName = "B",
|
||||||
|
BrowseName = "B",
|
||||||
|
TagName = "B",
|
||||||
|
};
|
||||||
|
GalaxyObject c = new()
|
||||||
|
{
|
||||||
|
GobjectId = 12,
|
||||||
|
ParentGobjectId = 11,
|
||||||
|
ContainedName = "C",
|
||||||
|
BrowseName = "C",
|
||||||
|
TagName = "C",
|
||||||
|
};
|
||||||
|
|
||||||
|
IReadOnlyList<GalaxyObject> objects = new[] { a, b, c };
|
||||||
|
GalaxyHierarchyCacheEntry entry = GalaxyHierarchyCacheEntry.Empty with
|
||||||
|
{
|
||||||
|
Status = GalaxyCacheStatus.Healthy,
|
||||||
|
Sequence = 1,
|
||||||
|
LastSuccessAt = DateTimeOffset.UtcNow,
|
||||||
|
Objects = objects,
|
||||||
|
Index = GalaxyHierarchyIndex.Build(objects),
|
||||||
|
DashboardSummary = DashboardGalaxySummary.Unknown with
|
||||||
|
{
|
||||||
|
Status = DashboardGalaxyStatus.Healthy,
|
||||||
|
ObjectCount = objects.Count,
|
||||||
|
},
|
||||||
|
ObjectCount = objects.Count,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Browse children of A (id=10). Its direct child B fails HistorizedOnly, so the
|
||||||
|
// projector falls back to HasMatchingDescendant(B), which walks B→C→A→B…
|
||||||
|
// without the visited-id guard. With the guard, the walk terminates and returns
|
||||||
|
// an empty page (no historized descendants exist anywhere in the cycle).
|
||||||
|
GalaxyBrowseChildrenResult result = GalaxyBrowseProjector.ProjectChildren(
|
||||||
|
entry,
|
||||||
|
new BrowseChildrenRequest { ParentGobjectId = 10, HistorizedOnly = true },
|
||||||
|
browseSubtreeGlobs: null,
|
||||||
|
offset: 0,
|
||||||
|
pageSize: 10);
|
||||||
|
|
||||||
|
Assert.Empty(result.Children);
|
||||||
|
Assert.Equal(0, result.TotalChildCount);
|
||||||
|
}
|
||||||
|
|
||||||
private static GalaxyHierarchyCacheEntry CreateEntry()
|
private static GalaxyHierarchyCacheEntry CreateEntry()
|
||||||
{
|
{
|
||||||
IReadOnlyList<GalaxyObject> objects = CreateObjects();
|
IReadOnlyList<GalaxyObject> objects = CreateObjects();
|
||||||
|
|||||||
Reference in New Issue
Block a user