perf(runtime): O(1) attribute resolution, precomputed types, coalesced static writes, shared JSON options
This commit is contained in:
@@ -242,6 +242,77 @@ public class InstanceActorTests : TestKit, IDisposable
|
||||
Assert.Equal("100.0", overrides["Temperature"]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perf remediation (arch-review WP1.5): a burst of rapid static-attribute writes must
|
||||
/// coalesce into ONE batched SQLite transaction on a short single-shot timer — mirroring
|
||||
/// NativeAlarmActor's MarkDirtyUpsert/FlushDirtyUpserts (P4) shape — rather than a write
|
||||
/// per <see cref="SetStaticAttributeCommand"/>. Pinned two ways: (1) nothing is persisted
|
||||
/// before the flush interval elapses, even though every write already replied
|
||||
/// synchronously and updated in-memory state; (2) only the LATEST value per attribute
|
||||
/// survives, across writes to two different attributes coalesced into the same flush.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task InstanceActor_RapidStaticWrites_CoalesceIntoSingleFlush()
|
||||
{
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "PumpCoalesce1",
|
||||
Attributes =
|
||||
[
|
||||
new ResolvedAttribute { CanonicalName = "Counter", Value = "0", DataType = "Int32" },
|
||||
new ResolvedAttribute { CanonicalName = "Label", Value = "Idle", DataType = "String" }
|
||||
]
|
||||
};
|
||||
|
||||
var flushInterval = TimeSpan.FromSeconds(1);
|
||||
var actor = ActorOf(Props.Create(() => new InstanceActor(
|
||||
"PumpCoalesce1",
|
||||
JsonSerializer.Serialize(config),
|
||||
_storage,
|
||||
_compilationService,
|
||||
_sharedScriptLibrary,
|
||||
null,
|
||||
_options,
|
||||
NullLogger<InstanceActor>.Instance,
|
||||
null, // dclManager
|
||||
null, // healthCollector
|
||||
null, // serviceProvider
|
||||
flushInterval)));
|
||||
|
||||
// Five rapid writes to "Counter" plus one to "Label", all sent back-to-back with no
|
||||
// delay between them (well inside the 1s coalescing window).
|
||||
for (var i = 1; i <= 5; i++)
|
||||
{
|
||||
actor.Tell(new SetStaticAttributeCommand(
|
||||
$"corr-counter-{i}", "PumpCoalesce1", "Counter", i.ToString(), DateTimeOffset.UtcNow));
|
||||
}
|
||||
actor.Tell(new SetStaticAttributeCommand(
|
||||
"corr-label", "PumpCoalesce1", "Label", "Running", DateTimeOffset.UtcNow));
|
||||
|
||||
// Every write replies synchronously (in-memory state is authoritative regardless of
|
||||
// when the buffered persist lands) — six writes, six responses.
|
||||
for (var i = 0; i < 6; i++)
|
||||
{
|
||||
var response = ExpectMsg<SetStaticAttributeResponse>(TimeSpan.FromSeconds(5));
|
||||
Assert.True(response.Success);
|
||||
}
|
||||
|
||||
// Well before the flush interval elapses, NOTHING has reached SQLite yet — proves the
|
||||
// writes did not each fire their own persist (the old per-write fire-and-forget shape
|
||||
// would already show a row here).
|
||||
await Task.Delay(200);
|
||||
var beforeFlush = await _storage.GetStaticOverridesAsync("PumpCoalesce1");
|
||||
Assert.Empty(beforeFlush);
|
||||
|
||||
// After the flush interval elapses, exactly one coalesced write has landed: only the
|
||||
// LATEST value per attribute survives.
|
||||
await Task.Delay(1500);
|
||||
var afterFlush = await _storage.GetStaticOverridesAsync("PumpCoalesce1");
|
||||
Assert.Equal(2, afterFlush.Count);
|
||||
Assert.Equal("5", afterFlush["Counter"]);
|
||||
Assert.Equal("Running", afterFlush["Label"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InstanceActor_LoadsStaticOverridesFromSQLite()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user