fix(m9/T24a): scope move-guard native-alarm scan to source-site templates (Ordinal); purpose-built include; add guard-4 + repo tests

This commit is contained in:
Joseph Doherty
2026-06-18 11:38:31 -04:00
parent fbe4ddaf58
commit dbe51e5f25
5 changed files with 290 additions and 15 deletions
@@ -90,6 +90,18 @@ public interface ISiteRepository
/// <returns>A task that resolves to the distinct referencing <see cref="Instance"/> entities (empty when none).</returns>
Task<IReadOnlyList<Instance>> GetInstancesReferencingDataConnectionAsync(int dataConnectionId, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves a site's instances with their
/// <see cref="Instance.NativeAlarmSourceOverrides"/> eagerly loaded. Purpose-built
/// for the move-data-connection guard, which must scan source-site instance
/// overrides for name-based connection references without paying the eager-load
/// cost on the hot-path <see cref="GetInstancesBySiteIdAsync"/>.
/// </summary>
/// <param name="siteId">The site primary key to filter by.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the site's <see cref="Instance"/> entities with overrides loaded (empty when none).</returns>
Task<IReadOnlyList<Instance>> GetInstancesWithNativeAlarmOverridesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
/// <summary>Saves all pending changes to the database.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the number of state entries written to the database.</returns>
@@ -132,9 +132,15 @@ public class SiteRepository : ISiteRepository
{
return await _dbContext.Instances
.Where(i => i.SiteId == siteId)
.Include(i => i.ConnectionBindings)
.ToListAsync(cancellationToken);
}
/// <inheritdoc />
public async Task<IReadOnlyList<Instance>> GetInstancesWithNativeAlarmOverridesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default)
{
return await _dbContext.Instances
.Where(i => i.SiteId == siteId)
.Include(i => i.NativeAlarmSourceOverrides)
.AsSplitQuery()
.ToListAsync(cancellationToken);
}
@@ -1426,17 +1426,40 @@ public class ManagementActor : ReceiveActor
// instances may override that name
// (InstanceNativeAlarmSourceOverride.ConnectionNameOverride). Moving a
// connection changes which physical connection that name resolves to on
// the source site, so any such reference to the moving name is a blocker.
// Detect and report only — never auto-rewrite.
// the SOURCE site, so any such reference *on the source site* to the
// moving name is a blocker. Detect and report only — never auto-rewrite.
//
// Scoping: templates are site-agnostic and ConnectionName resolves
// against the DEPLOYING site's connection pool at flatten time, so the
// template scan is restricted to templates actually instantiated on the
// SOURCE site. A template referencing the same connection NAME but only
// instantiated on another site is NOT affected by this move (that site
// keeps its own connection of the same name), so a global template scan
// would be a false positive that over-blocks legitimate moves whenever
// connection names are reused across sites (the common case).
//
// Comparison is StringComparison.Ordinal to match the
// flattening/deployment pipeline (FlatteningService / FlatteningPipeline
// resolve connection names case-sensitively) — block exactly the
// references the runtime would actually fail to resolve.
var referenceBlockers = new List<string>();
// Source-site instances with their native-alarm-source overrides eagerly
// loaded (purpose-built load so the hot-path GetInstancesBySiteIdAsync
// stays lean). Reused below for both the override scan and to derive the
// set of templates to scan.
var sourceInstances = await repo.GetInstancesWithNativeAlarmOverridesBySiteIdAsync(sourceSiteId);
// Only templates instantiated on the SOURCE site can be broken by the move.
var templateRepo = sp.GetRequiredService<ITemplateEngineRepository>();
var templates = await templateRepo.GetAllTemplatesAsync();
foreach (var template in templates)
var sourceTemplateIds = sourceInstances.Select(i => i.TemplateId).Distinct();
foreach (var templateId in sourceTemplateIds)
{
var template = await templateRepo.GetTemplateByIdAsync(templateId);
if (template is null) continue;
foreach (var source in template.NativeAlarmSources)
{
if (string.Equals(source.ConnectionName, conn.Name, StringComparison.OrdinalIgnoreCase))
if (string.Equals(source.ConnectionName, conn.Name, StringComparison.Ordinal))
{
referenceBlockers.Add(
$"template '{template.Name}' native-alarm-source '{source.Name}'");
@@ -1446,12 +1469,11 @@ public class ManagementActor : ReceiveActor
// Instance-level overrides on the SOURCE site that name this connection
// would orphan once the connection leaves the site.
var sourceInstances = await repo.GetInstancesBySiteIdAsync(sourceSiteId);
foreach (var instance in sourceInstances)
{
foreach (var ovr in instance.NativeAlarmSourceOverrides)
{
if (string.Equals(ovr.ConnectionNameOverride, conn.Name, StringComparison.OrdinalIgnoreCase))
if (string.Equals(ovr.ConnectionNameOverride, conn.Name, StringComparison.Ordinal))
{
referenceBlockers.Add(
$"instance '{instance.UniqueName}' (ID {instance.Id}) native-alarm-source override '{ovr.SourceCanonicalName}'");