20cf3bfec7
Both projectors memoized one full result list per filter signature with no limit, and the signature is client-controlled (tag_name_glob '*', '**', ...), so distinct globs retained memory until the next Galaxy deploy. A bounded FIFO ProjectionMemo<T> now backs both memos; an evicted signature is recomputed. - tests: ProjectionMemoBoundTests (500 distinct globs per projector, re-query after eviction) - Directory.Build.props 0.2.0 -> 0.2.1 (NOT packed or published) - code-reviews: GalaxyRepository-001 Resolved; README regenerated Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
78 lines
3.1 KiB
C#
78 lines
3.1 KiB
C#
using ZB.MOM.WW.GalaxyRepository;
|
|
using ZB.MOM.WW.GalaxyRepository.Grpc;
|
|
|
|
namespace ZB.MOM.WW.GalaxyRepository.Tests;
|
|
|
|
/// <summary>
|
|
/// GalaxyRepository-001 (scadaproj#41): the per-entry filter memo was unbounded, so a client
|
|
/// issuing distinct <c>tag_name_glob</c> values (<c>*</c>, <c>**</c>, ...) retained one full
|
|
/// result list per glob until the next Galaxy deploy. The memo must stay capped.
|
|
/// </summary>
|
|
public sealed class ProjectionMemoBoundTests
|
|
{
|
|
private const int DistinctFilters = 500;
|
|
|
|
[Fact]
|
|
public void DiscoverHierarchy_memo_is_bounded_under_distinct_client_globs()
|
|
{
|
|
GalaxyHierarchyCacheEntry entry = CreateEntry();
|
|
|
|
for (int i = 1; i <= DistinctFilters; i++)
|
|
{
|
|
GalaxyHierarchyProjector.Project(entry, new DiscoverHierarchyRequest { TagNameGlob = new string('*', i) }, null, 0, 1);
|
|
}
|
|
|
|
Assert.InRange(GalaxyHierarchyProjector.MemoCount(entry), 1, ProjectionMemo<object>.Capacity);
|
|
}
|
|
|
|
[Fact]
|
|
public void BrowseChildren_memo_is_bounded_under_distinct_client_globs()
|
|
{
|
|
GalaxyHierarchyCacheEntry entry = CreateEntry();
|
|
|
|
for (int i = 1; i <= DistinctFilters; i++)
|
|
{
|
|
GalaxyBrowseProjector.ProjectChildren(entry, new BrowseChildrenRequest { TagNameGlob = new string('*', i) }, null, 0, 1);
|
|
}
|
|
|
|
Assert.InRange(GalaxyBrowseProjector.MemoCount(entry), 1, ProjectionMemo<object>.Capacity);
|
|
}
|
|
|
|
[Fact]
|
|
public void Evicted_signature_is_recomputed_with_the_same_answer()
|
|
{
|
|
GalaxyHierarchyCacheEntry entry = CreateEntry();
|
|
DiscoverHierarchyRequest first = new() { TagNameGlob = "Plant.*" };
|
|
GalaxyHierarchyQueryResult before = GalaxyHierarchyProjector.Project(entry, first, null, 0, 10);
|
|
|
|
for (int i = 1; i <= DistinctFilters; i++)
|
|
{
|
|
GalaxyHierarchyProjector.Project(entry, new DiscoverHierarchyRequest { TagNameGlob = new string('*', i) }, null, 0, 1);
|
|
}
|
|
|
|
GalaxyHierarchyQueryResult after = GalaxyHierarchyProjector.Project(entry, first, null, 0, 10);
|
|
Assert.Equal(before.TotalObjectCount, after.TotalObjectCount);
|
|
Assert.Equal(before.FilterSignature, after.FilterSignature);
|
|
Assert.Equal(before.Objects.Select(o => o.GobjectId), after.Objects.Select(o => o.GobjectId));
|
|
}
|
|
|
|
private static GalaxyHierarchyCacheEntry CreateEntry()
|
|
{
|
|
GalaxyObject[] objects =
|
|
[
|
|
new() { GobjectId = 1, ParentGobjectId = 0, IsArea = true, ContainedName = "Plant", BrowseName = "Plant", TagName = "Plant" },
|
|
new() { GobjectId = 2, ParentGobjectId = 1, ContainedName = "Pump", BrowseName = "Pump", TagName = "Plant.Pump" },
|
|
new() { GobjectId = 3, ParentGobjectId = 1, ContainedName = "Valve", BrowseName = "Valve", TagName = "Plant.Valve" },
|
|
];
|
|
return GalaxyHierarchyCacheEntry.Empty with
|
|
{
|
|
Status = GalaxyCacheStatus.Healthy,
|
|
Sequence = 1,
|
|
LastSuccessAt = DateTimeOffset.UtcNow,
|
|
Objects = objects,
|
|
Index = GalaxyHierarchyIndex.Build(objects),
|
|
ObjectCount = objects.Length,
|
|
};
|
|
}
|
|
}
|