960c35060c
Adds SiteStorageService.GetAllDeployedConfigMetadataAsync. The sweep decides from the projection, then fetches single rows for its starts/restarts off- thread and re-validates them (generation guard, revision match, pipeline/ running guards), releasing the per-revision claim of any repair that does not act. Boot load unchanged.
137 lines
5.4 KiB
C#
137 lines
5.4 KiB
C#
using System.Text.Json;
|
|
using Akka.Actor;
|
|
using Akka.TestKit.Xunit2;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
|
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
|
|
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;
|
|
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
|
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.TestSupport;
|
|
using ZB.MOM.WW.ScadaBridge.TestSupport;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors;
|
|
|
|
/// <summary>
|
|
/// Review 2026-10-21 01/SR-P1: the instance reconcile sweep compared only name / enabled /
|
|
/// revision, yet materialised every row's <c>config_json</c> on every tick — tens of MB of LOH
|
|
/// strings per minute on a healthy large site. It now reads a metadata-only projection and
|
|
/// fetches a full row only for an instance it actually starts or restarts.
|
|
/// </summary>
|
|
public class InstanceSweepMetadataReadTests : TestKit, IDisposable
|
|
{
|
|
private readonly SiteStorageService _storage;
|
|
private readonly ScriptCompilationService _compilationService;
|
|
private readonly SharedScriptLibrary _sharedScriptLibrary;
|
|
private readonly TestLocalDb _localDb;
|
|
|
|
private int _fullTableReads;
|
|
private int _singleRowReads;
|
|
|
|
/// <summary>Creates a temporary LocalDb-backed site store per test.</summary>
|
|
public InstanceSweepMetadataReadTests()
|
|
{
|
|
_localDb = TestLocalDb.CreateTemp("sweep-metadata-read");
|
|
_storage = new SiteStorageService(_localDb.Db, NullLogger<SiteStorageService>.Instance);
|
|
_storage.InitializeAsync().GetAwaiter().GetResult();
|
|
_compilationService = new ScriptCompilationService(NullLogger<ScriptCompilationService>.Instance);
|
|
_sharedScriptLibrary = new SharedScriptLibrary(
|
|
_compilationService, NullLogger<SharedScriptLibrary>.Instance);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
void IDisposable.Dispose()
|
|
{
|
|
Shutdown();
|
|
var path = _localDb.Path;
|
|
_localDb.Dispose();
|
|
TestLocalDb.DeleteFiles(path);
|
|
}
|
|
|
|
private static string ConfigJson(string name) => JsonSerializer.Serialize(new FlattenedConfiguration
|
|
{
|
|
InstanceUniqueName = name,
|
|
Attributes = [new ResolvedAttribute { CanonicalName = "A", Value = "1", DataType = "Int32" }]
|
|
});
|
|
|
|
private async Task<List<DeployedInstance>> CountingFullLoad()
|
|
{
|
|
Interlocked.Increment(ref _fullTableReads);
|
|
return await _storage.GetAllDeployedConfigsAsync();
|
|
}
|
|
|
|
private async Task<DeployedInstance?> CountingSingleRowLoad(string name)
|
|
{
|
|
Interlocked.Increment(ref _singleRowReads);
|
|
return await _storage.GetDeployedConfigAsync(name);
|
|
}
|
|
|
|
private IActorRef CreateBootedDeploymentManager()
|
|
{
|
|
var health = new DeploymentManagerStartupProbe();
|
|
Func<Task<List<DeployedInstance>>> fullLoad = CountingFullLoad;
|
|
Func<string, Task<DeployedInstance?>> rowLoad = CountingSingleRowLoad;
|
|
var dm = ActorOf(Props.Create(() => new DeploymentManagerActor(
|
|
_storage,
|
|
_compilationService,
|
|
_sharedScriptLibrary,
|
|
null,
|
|
new SiteRuntimeOptions { InstanceSweepIntervalSeconds = 1, StartupBatchDelayMs = 10 },
|
|
NullLogger<DeploymentManagerActor>.Instance,
|
|
null,
|
|
health,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
fullLoad,
|
|
null,
|
|
null,
|
|
null,
|
|
rowLoad,
|
|
null)));
|
|
AwaitCondition(() => health.ReportCount > 0, TimeSpan.FromSeconds(20), TimeSpan.FromMilliseconds(25));
|
|
return dm;
|
|
}
|
|
|
|
private async Task<bool> IsRunningAsync(IActorRef dm, string name)
|
|
{
|
|
var snapshot = await dm.Ask<DebugViewSnapshot>(
|
|
new DebugSnapshotRequest(name, Guid.NewGuid().ToString("N")), TimeSpan.FromSeconds(5));
|
|
return !snapshot.InstanceNotFound;
|
|
}
|
|
|
|
/// <summary>A healthy site's sweep never reads a config blob: one full read (boot), zero row reads.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task HealthySweep_ReadsNoConfigJson()
|
|
{
|
|
await _storage.StoreDeployedConfigAsync("P1", ConfigJson("P1"), "d1", "r1", true);
|
|
await _storage.StoreDeployedConfigAsync("P2", ConfigJson("P2"), "d2", "r2", true);
|
|
var dm = CreateBootedDeploymentManager();
|
|
await AwaitAssertAsync(async () => Assert.True(await IsRunningAsync(dm, "P2")), TimeSpan.FromSeconds(10));
|
|
|
|
await Task.Delay(TimeSpan.FromSeconds(4)); // several sweep ticks
|
|
|
|
Assert.Equal(1, Volatile.Read(ref _fullTableReads));
|
|
Assert.Equal(0, Volatile.Read(ref _singleRowReads));
|
|
}
|
|
|
|
/// <summary>
|
|
/// A repair still works: a row that appears after boot (as CDC replication would deliver it)
|
|
/// is started, its config fetched by a single-row read.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task SweepStart_FetchesOnlyTheRowItStarts()
|
|
{
|
|
var dm = CreateBootedDeploymentManager();
|
|
await _storage.StoreDeployedConfigAsync("Late1", ConfigJson("Late1"), "d1", "r1", true);
|
|
|
|
await AwaitAssertAsync(async () => Assert.True(await IsRunningAsync(dm, "Late1")), TimeSpan.FromSeconds(15));
|
|
|
|
Assert.Equal(1, Volatile.Read(ref _fullTableReads));
|
|
Assert.Equal(1, Volatile.Read(ref _singleRowReads));
|
|
}
|
|
}
|