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; /// /// Review 2026-10-21 01/SR-P1: the instance reconcile sweep compared only name / enabled / /// revision, yet materialised every row's config_json 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. /// 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; /// Creates a temporary LocalDb-backed site store per test. public InstanceSweepMetadataReadTests() { _localDb = TestLocalDb.CreateTemp("sweep-metadata-read"); _storage = new SiteStorageService(_localDb.Db, NullLogger.Instance); _storage.InitializeAsync().GetAwaiter().GetResult(); _compilationService = new ScriptCompilationService(NullLogger.Instance); _sharedScriptLibrary = new SharedScriptLibrary( _compilationService, NullLogger.Instance); } /// 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> CountingFullLoad() { Interlocked.Increment(ref _fullTableReads); return await _storage.GetAllDeployedConfigsAsync(); } private async Task CountingSingleRowLoad(string name) { Interlocked.Increment(ref _singleRowReads); return await _storage.GetDeployedConfigAsync(name); } private IActorRef CreateBootedDeploymentManager() { var health = new DeploymentManagerStartupProbe(); Func>> fullLoad = CountingFullLoad; Func> rowLoad = CountingSingleRowLoad; var dm = ActorOf(Props.Create(() => new DeploymentManagerActor( _storage, _compilationService, _sharedScriptLibrary, null, new SiteRuntimeOptions { InstanceSweepIntervalSeconds = 1, StartupBatchDelayMs = 10 }, NullLogger.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 IsRunningAsync(IActorRef dm, string name) { var snapshot = await dm.Ask( new DebugSnapshotRequest(name, Guid.NewGuid().ToString("N")), TimeSpan.FromSeconds(5)); return !snapshot.InstanceNotFound; } /// A healthy site's sweep never reads a config blob: one full read (boot), zero row reads. /// A task that represents the asynchronous operation. [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)); } /// /// 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. /// /// A task that represents the asynchronous operation. [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)); } }