perf(siteruntime): projected single-row deployed-config reads (P1-4)
HandleEnsureInstanceStarted, HandleDeploymentStateQuery, and HandleEnable each called GetAllDeployedConfigsAsync — materializing every deployed config, including every ConfigJson blob — just to FirstOrDefault one row. A volume-restore reconcile healing hundreds of instances turned that into hundreds of full-table reads. Add SiteStorageService.GetDeployedConfigAsync(name) (single-row, full projection incl. ConfigJson, for callers that (re)start an Instance Actor) and GetDeployedConfigMetadataAsync(name) (single-row, ConfigJson-free projection, for callers that only need DeploymentId/RevisionHash/IsEnabled). Switch the three call sites; leave decisions/semantics identical. HandleEnsureInstanceStarted gets its own _deployedConfigLoader seam, deliberately separate from the existing _configLoader (still used by the boot-time load, which legitimately needs every row) so a test overriding one cannot accidentally gate the other. Generation fencing (StoreGenerationAtRead, Medium wave T1) is untouched: the generation is still captured on the actor thread before the read dispatches. Tests: new SiteStorageServiceTests lock in that both projections return the same values GetAllDeployedConfigsAsync().FirstOrDefault(...) did. The three row-101 ensure-started race tests (ReconcileStartsInstanceTests) are ported to the new seam and still pass, proving generation fencing survives the projection. Proof of failure: since this is a pure I/O-cost fix with no observable behavior change, the new/changed tests were confirmed to fail to even compile against the unfixed code (stash/unstash) — GetDeployedConfigAsync, GetDeployedConfigMetadataAsync, and the deployedConfigLoader ctor param don't exist there.
This commit is contained in:
+18
-13
@@ -92,7 +92,8 @@ public class ReconcileStartsInstanceTests : TestKit, IDisposable
|
||||
/// </summary>
|
||||
private IActorRef CreateDeploymentManager(
|
||||
IServiceProvider? serviceProvider = null,
|
||||
Func<Task<List<DeployedInstance>>>? configLoader = null)
|
||||
Func<Task<List<DeployedInstance>>>? configLoader = null,
|
||||
Func<string, Task<DeployedInstance?>>? deployedConfigLoader = null)
|
||||
{
|
||||
var health = new DeploymentManagerStartupProbe();
|
||||
var dm = ActorOf(Props.Create(() => new DeploymentManagerActor(
|
||||
@@ -109,7 +110,11 @@ public class ReconcileStartsInstanceTests : TestKit, IDisposable
|
||||
configFetcher: null,
|
||||
// In position: an expression tree rejects a named argument that skips a parameter.
|
||||
startupLoadRetryInterval: null,
|
||||
configLoader: configLoader)));
|
||||
configLoader: configLoader,
|
||||
externalSystemCache: null,
|
||||
scriptScheduler: null,
|
||||
connectionDefinitionLoader: null,
|
||||
deployedConfigLoader: deployedConfigLoader)));
|
||||
|
||||
AwaitCondition(() => health.ReportCount > 0, TimeSpan.FromSeconds(20),
|
||||
TimeSpan.FromMilliseconds(25));
|
||||
@@ -409,7 +414,7 @@ public class ReconcileStartsInstanceTests : TestKit, IDisposable
|
||||
{
|
||||
const string instanceName = "RacedDisablePump";
|
||||
var loader = new GatedConfigLoader(_storage);
|
||||
var dm = CreateDeploymentManager(configLoader: loader.LoadAsync);
|
||||
var dm = CreateDeploymentManager(deployedConfigLoader: loader.LoadAsync);
|
||||
|
||||
await _storage.StoreDeployedConfigAsync(
|
||||
instanceName, MakeConfigJson(instanceName), "dep-rd", "rev-rd", true);
|
||||
@@ -451,7 +456,7 @@ public class ReconcileStartsInstanceTests : TestKit, IDisposable
|
||||
{
|
||||
const string instanceName = "RacedDeletePump";
|
||||
var loader = new GatedConfigLoader(_storage);
|
||||
var dm = CreateDeploymentManager(configLoader: loader.LoadAsync);
|
||||
var dm = CreateDeploymentManager(deployedConfigLoader: loader.LoadAsync);
|
||||
|
||||
await _storage.StoreDeployedConfigAsync(
|
||||
instanceName, MakeConfigJson(instanceName), "dep-rx", "rev-rx", true);
|
||||
@@ -487,7 +492,7 @@ public class ReconcileStartsInstanceTests : TestKit, IDisposable
|
||||
{
|
||||
const string instanceName = "UnracedPump";
|
||||
var loader = new GatedConfigLoader(_storage);
|
||||
var dm = CreateDeploymentManager(configLoader: loader.LoadAsync);
|
||||
var dm = CreateDeploymentManager(deployedConfigLoader: loader.LoadAsync);
|
||||
|
||||
await _storage.StoreDeployedConfigAsync(
|
||||
instanceName, MakeConfigJson(instanceName), "dep-ok", "rev-ok", true);
|
||||
@@ -510,15 +515,15 @@ public class ReconcileStartsInstanceTests : TestKit, IDisposable
|
||||
.ResolveOne(TimeSpan.FromSeconds(5));
|
||||
|
||||
/// <summary>
|
||||
/// A deployed-config loader that can be held open mid-read, so a test can commit a
|
||||
/// lifecycle write while a heal's snapshot is provably in flight rather than hoping the
|
||||
/// two race the right way round.
|
||||
/// A single-instance deployed-config loader that can be held open mid-read, so a test can
|
||||
/// commit a lifecycle write while a heal's snapshot is provably in flight rather than hoping
|
||||
/// the two race the right way round.
|
||||
///
|
||||
/// <para>
|
||||
/// Reads real storage every time and only parks AFTER the read, so the returned snapshot is
|
||||
/// exactly what production would have seen at that instant. Inert until <see cref="Arm"/>
|
||||
/// is called, which keeps the Deployment Manager's boot load (which runs through this same
|
||||
/// seam) unblocked.
|
||||
/// is called, which keeps the Deployment Manager's boot load — a separate seam
|
||||
/// (<c>_configLoader</c>, register P1-4) untouched by this gate — unblocked.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private sealed class GatedConfigLoader(SiteStorageService storage)
|
||||
@@ -534,16 +539,16 @@ public class ReconcileStartsInstanceTests : TestKit, IDisposable
|
||||
|
||||
public Task WaitUntilParkedAsync() => _parked.Task.WaitAsync(TimeSpan.FromSeconds(10));
|
||||
|
||||
public async Task<List<DeployedInstance>> LoadAsync()
|
||||
public async Task<DeployedInstance?> LoadAsync(string instanceName)
|
||||
{
|
||||
var configs = await storage.GetAllDeployedConfigsAsync();
|
||||
var config = await storage.GetDeployedConfigAsync(instanceName);
|
||||
var gate = _release;
|
||||
if (gate != null)
|
||||
{
|
||||
_parked.TrySetResult();
|
||||
await gate.Task;
|
||||
}
|
||||
return configs;
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user