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.TestSupport; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors; /// /// Review 2026-10-21 01/SR-P3: boot/failover compile warm was serial within a batch and /// serialised across batches — batch k+1's compiles did not start until batch k's actors existed /// AND the stagger delay had elapsed, so recovery time was the sum of every distinct compile plus /// every delay, on one thread. The next batch is now warmed while the current batch's delay runs, /// and a batch's distinct compile units are warmed in parallel. /// public class StartupBatchPrewarmTests : TestKit, IDisposable { private readonly SiteStorageService _storage; private readonly SiteScriptCompileCache _compileCache = new(); private readonly ScriptCompilationService _compilationService; private readonly SharedScriptLibrary _sharedScriptLibrary; private readonly TestLocalDb _localDb; /// Creates a temporary LocalDb-backed site store and an observable compile cache. public StartupBatchPrewarmTests() { _localDb = TestLocalDb.CreateTemp("startup-prewarm"); _storage = new SiteStorageService(_localDb.Db, NullLogger.Instance); _storage.InitializeAsync().GetAwaiter().GetResult(); _compilationService = new ScriptCompilationService( NullLogger.Instance, _compileCache); _sharedScriptLibrary = new SharedScriptLibrary( _compilationService, NullLogger.Instance); } /// void IDisposable.Dispose() { Shutdown(); var path = _localDb.Path; _localDb.Dispose(); TestLocalDb.DeleteFiles(path); } private static string ConfigWithScript(string name, int distinct) => JsonSerializer.Serialize(new FlattenedConfiguration { InstanceUniqueName = name, Attributes = [new ResolvedAttribute { CanonicalName = "A", Value = "1", DataType = "Int32" }], Scripts = [new ResolvedScript { CanonicalName = "S", Code = $"return {distinct} * 2;" }] }); 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; } /// /// Two one-instance batches with a long stagger delay: batch 2's script is compiled while the /// delay after batch 1 is still running, i.e. BEFORE batch 2's Instance Actor exists. Before /// the fix its compile only started once the delay had elapsed. /// /// A task that represents the asynchronous operation. [Fact] public async Task NextBatch_IsWarmedDuringTheStaggerDelay() { await _storage.StoreDeployedConfigAsync("B0", ConfigWithScript("B0", 1001), "d0", "r0", true); await _storage.StoreDeployedConfigAsync("B1", ConfigWithScript("B1", 1002), "d1", "r1", true); var dm = ActorOf(Props.Create(() => new DeploymentManagerActor( _storage, _compilationService, _sharedScriptLibrary, null, new SiteRuntimeOptions { StartupBatchSize = 1, StartupBatchDelayMs = 6000, InstanceSweepIntervalSeconds = 0 }, NullLogger.Instance, dclManager: null, healthCollector: null, serviceProvider: null, loggerFactory: null, configFetcher: null))); // Batch 1 is up... await AwaitAssertAsync(async () => Assert.True(await IsRunningAsync(dm, "B0") || await IsRunningAsync(dm, "B1")), TimeSpan.FromSeconds(20), TimeSpan.FromMilliseconds(50)); // ...and both scripts are compiled while exactly one instance is running. await AwaitAssertAsync(() => { Assert.True(_compileCache.Count >= 2, $"compile cache holds {_compileCache.Count} entries"); return Task.CompletedTask; }, TimeSpan.FromSeconds(3), TimeSpan.FromMilliseconds(50)); Assert.False(await IsRunningAsync(dm, "B0") && await IsRunningAsync(dm, "B1"), "the second batch was created early — the stagger delay must be unchanged"); // The stagger itself is unchanged: batch 2 still starts after the delay. await AwaitAssertAsync(async () => Assert.True(await IsRunningAsync(dm, "B0") && await IsRunningAsync(dm, "B1")), TimeSpan.FromSeconds(20), TimeSpan.FromMilliseconds(100)); } }