fix(site-runtime): reconcile artifact deletions on apply — central deletes no longer orphan site rows
The artifact apply (DeploymentManagerActor.HandleDeployArtifacts) was upsert-only, so deleting an external system (or shared script, DB connection, data connection) centrally never removed the site's SQLite row — a deleted external system stayed callable from site scripts forever. Central always ships the COMPLETE set of each artifact class (ArtifactDeploymentService GetAll* snapshots; the wire's presence-tracking wrapper lists preserve null-vs-empty), so the site now applies upsert-then-reconcile: after storing the incoming set, SiteStorageService.DeleteRowsExceptAsync removes any stored row absent from it, per artifact table. A null list still means 'field not shipped' and touches nothing. Runtime cleanup rides along: a reconciled-away shared script is unregistered from the compiled SharedScriptLibrary (a stale delegate would stay callable until restart), and a removed data connection is evicted from the DCL hash cache and its live connection actor stopped via the previously-caller-less RemoveConnectionCommand — both on the actor thread via the extended ApplyArtifactDataConnectionsToDcl message. All four tables are RegisterReplicated, so the deletes reach the standby as ordinary CDC row tombstones. Tests: storage-level reconcile per table (incl. empty-set-deletes-all and idempotency) in ArtifactStorageTests; actor-level pins in DeploymentManagerActorTests (orphan delete, null-set no-op, library unregistration, DCL stop for the removed connection only). Docs: Component-DeploymentManager + Component-SiteRuntime record the full-set/reconcile semantics.
This commit is contained in:
@@ -1870,6 +1870,14 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
{
|
||||
try
|
||||
{
|
||||
// Each non-null artifact list is the COMPLETE system-wide set
|
||||
// (ArtifactDeploymentService ships GetAll* snapshots), so the apply is
|
||||
// upsert-then-reconcile: store every incoming artifact, then delete any
|
||||
// stored row absent from the set — that artifact was deleted centrally.
|
||||
// Upsert-only apply used to leave such rows orphaned on the site forever
|
||||
// (a centrally-deleted external system stayed callable from site scripts).
|
||||
// A null list means "field not shipped" and touches nothing.
|
||||
|
||||
// Store shared scripts and recompile
|
||||
if (command.SharedScripts != null)
|
||||
{
|
||||
@@ -1881,6 +1889,11 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
// Shared scripts recompiled on update
|
||||
_sharedScriptLibrary.CompileAndRegister(script.Name, script.Code);
|
||||
}
|
||||
|
||||
var removedScripts = await _storage.DeleteSharedScriptsExceptAsync(
|
||||
command.SharedScripts.Select(s => s.Name).ToList());
|
||||
foreach (var name in removedScripts)
|
||||
_sharedScriptLibrary.Remove(name);
|
||||
}
|
||||
|
||||
// Store external system definitions
|
||||
@@ -1891,6 +1904,9 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
await _storage.StoreExternalSystemAsync(es.Name, es.EndpointUrl,
|
||||
es.AuthType, es.AuthConfiguration, es.MethodDefinitionsJson, es.TimeoutSeconds);
|
||||
}
|
||||
|
||||
await _storage.DeleteExternalSystemsExceptAsync(
|
||||
command.ExternalSystems.Select(es => es.Name).ToList());
|
||||
}
|
||||
|
||||
// Store database connection definitions
|
||||
@@ -1901,6 +1917,9 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
await _storage.StoreDatabaseConnectionAsync(db.Name, db.ConnectionString,
|
||||
db.MaxRetries, db.RetryDelay);
|
||||
}
|
||||
|
||||
await _storage.DeleteDatabaseConnectionsExceptAsync(
|
||||
command.DatabaseConnections.Select(db => db.Name).ToList());
|
||||
}
|
||||
|
||||
// Notification lists and SMTP
|
||||
@@ -1923,6 +1942,9 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
dc.BackupConfigurationJson, dc.FailoverRetryCount);
|
||||
}
|
||||
|
||||
var removedConnections = await _storage.DeleteDataConnectionDefinitionsExceptAsync(
|
||||
command.DataConnections.Select(dc => dc.Name).ToList());
|
||||
|
||||
// After the SQLite store, dispatch an
|
||||
// internal message back to the actor thread so the DCL
|
||||
// push runs through EnsureDclConnection — keeping the
|
||||
@@ -1934,8 +1956,11 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
// self-contained after artifact deployment"). The
|
||||
// helper's hash cache skips unchanged definitions, so
|
||||
// the push is idempotent for re-deploys of the same
|
||||
// artifact bundle.
|
||||
self.Tell(new ApplyArtifactDataConnectionsToDcl(command.DataConnections));
|
||||
// artifact bundle. Reconciled removals ride the same
|
||||
// message so the live DCL connection actor is stopped
|
||||
// on the actor thread alongside the hash-cache eviction.
|
||||
self.Tell(new ApplyArtifactDataConnectionsToDcl(
|
||||
command.DataConnections, removedConnections));
|
||||
}
|
||||
|
||||
// SMTP configuration is
|
||||
@@ -2012,6 +2037,21 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
dc.BackupConfigurationJson,
|
||||
dc.FailoverRetryCount);
|
||||
}
|
||||
|
||||
// Reconciled removals: the definition row is already gone from SQLite; evict
|
||||
// the hash cache (so a later re-add with the same name recreates cleanly) and
|
||||
// stop the live DCL connection actor. A connection still referenced by a
|
||||
// deployed instance's config no longer exists centrally either — its attributes
|
||||
// go bad quality, the standard disconnected signal, until the instance is
|
||||
// redeployed against current central config.
|
||||
foreach (var name in msg.RemovedConnectionNames)
|
||||
{
|
||||
_createdConnections.Remove(name);
|
||||
_dclManager?.Tell(new DataConnectionLayer.Actors.RemoveConnectionCommand(name));
|
||||
_logger.LogWarning(
|
||||
"Artifact reconcile removed data connection '{Name}' (deleted centrally); " +
|
||||
"its DCL connection actor has been stopped", name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -2118,8 +2158,11 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
/// <see cref="HandleDeployArtifacts"/>'s off-thread persistence task back
|
||||
/// onto the actor thread, so the DCL push (and its hash-cache mutation)
|
||||
/// runs through <see cref="EnsureDclConnection"/> without crossing
|
||||
/// thread-confinement boundaries.
|
||||
/// thread-confinement boundaries. <paramref name="RemovedConnectionNames"/>
|
||||
/// carries the reconcile-deleted definitions so the hash-cache eviction and
|
||||
/// live DCL connection stop also happen actor-thread-confined.
|
||||
/// </summary>
|
||||
internal record ApplyArtifactDataConnectionsToDcl(
|
||||
IReadOnlyList<Commons.Messages.Artifacts.DataConnectionArtifact> DataConnections);
|
||||
IReadOnlyList<Commons.Messages.Artifacts.DataConnectionArtifact> DataConnections,
|
||||
IReadOnlyList<string> RemovedConnectionNames);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user