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));
}
}