b34ec10f8b
P7-6 (behaviour fix, plan decision 1): DeploySiteAsync redeployed EVERY deployable instance unconditionally. A redeploy is not idempotent at the site — it resets static attribute overrides and clears native alarm state by design — so one site-wide push destroyed live operational state on every unchanged instance in the batch, which is normally most of them. Bulk deploy now pre-filters with IStaleInstanceProbe (the same seam the bundle importer and Deployments page use; its memo timing was fixed by the Medium wave's P7-4): an instance whose freshly computed revision hash still matches its DeployedConfigSnapshot is reported in the existing additive SkippedInstances list with reason "up-to-date" and never flattened, staged or sent. The filter is deliberately conservative — every uncertainty deploys: only an Enabled instance qualifies, a missing snapshot disqualifies, a prior record that ShouldQuerySiteBeforeRedeploy flags goes through prepare so the reconcile that HEALS it still runs, and a null/faulting probe means "indeterminate". New additive MgmtDeploySiteCommand.Force (default false = the new filtered behaviour) plus CLI `deploy site --force` restores the unconditional push for the case central cannot see (restored volume, rebuilt node). Composes with the Medium wave's P7-2: Disabled wins — force suppresses the up-to-date filter only, never the Disabled skip. P7-5 (verified, then fixed): the cascade REPRODUCES. All three bulk phases share one scoped DbContext, and a failed SaveChanges does not untrack the offending entity — EF re-attempts it on the next save, including MarkDeploymentFailedAsync's cleanup write. Measured on a real SQLite-backed ScadaBridgeDbContext: a permanent save failure on instance 1's post-success side effects failed instances 2, 3 and 4 and left all three deployment records stranded InProgress. MarkDeploymentFailedAsync now detaches the pending changes (new IDeploymentManagerRepository.DetachPendingChanges) when the cleanup save fails and retries the Failed write once on the clean tracker, so later instances save cleanly and the record reaches a terminal status. The DeployFailed audit row moved outside the status-write try (best-effort, must not be lost because the status write failed). Tests: 5 new DeploySiteAsyncTests (skip + reason + no site round-trip; --force deploys; Disabled skipped under both force values; the three not-enough-to-skip guards), 2 new IntegrationTests over a real EF context (the poisoned-context cascade, and the narrow EF tracking fact the fix rests on), CLI --force flag parse test, and a MgmtDeploySite wire-default test. Proof of failure: both P7-6 behaviour tests and both P7-5 assertions fail against the unfixed code.
68 lines
3.3 KiB
C#
68 lines
3.3 KiB
C#
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
|
|
|
public record MgmtDeployArtifactsCommand(int? SiteId = null);
|
|
|
|
/// <summary>
|
|
/// Deploys every deployable instance at one site in a single operation.
|
|
///
|
|
/// <para>
|
|
/// Distinct from <see cref="MgmtDeployArtifactsCommand"/>, which ships the
|
|
/// SYSTEM-WIDE artifact sets (shared scripts, external systems, DB/data
|
|
/// connections) to a site. This command deploys the site's INSTANCE
|
|
/// configurations — the same per-instance pipeline
|
|
/// <c>MgmtDeployInstanceCommand</c> runs, batched.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Not all-or-nothing: each instance keeps its own deployment id, revision hash,
|
|
/// operation lock and status record, and a failure on one is reported as a failed
|
|
/// row while the rest proceed. Individually retryable via the single-instance
|
|
/// deploy. Additive contract; new fields go on the end with a default.
|
|
/// </para>
|
|
/// </summary>
|
|
/// <param name="SiteId">Database id of the target site. Required — this command is never fleet-wide.</param>
|
|
/// <param name="Force">
|
|
/// Deploy every deployable instance even when its flattened config already matches
|
|
/// the revision the site is running. Default <see langword="false"/>: an unchanged
|
|
/// instance is skipped and reported in the summary's <c>SkippedInstances</c>,
|
|
/// because a redeploy resets static attribute overrides and native alarm state at
|
|
/// the site. Set it when central's recorded state cannot be trusted — a site whose
|
|
/// data volume was restored or whose node was rebuilt looks up-to-date from central
|
|
/// and needs the push anyway. Never overrides the Disabled skip.
|
|
/// </param>
|
|
public record MgmtDeploySiteCommand(int SiteId, bool Force = false);
|
|
public record QueryDeploymentsCommand(int? InstanceId = null, string? Status = null, int Page = 1, int PageSize = 50);
|
|
|
|
/// <summary>
|
|
/// Result wrapper for <see cref="QueryDeploymentsCommand"/>.
|
|
///
|
|
/// <para>
|
|
/// The handler used to return a bare row array. It cannot any more: the page size
|
|
/// is now BOUNDED (a non-positive <c>PageSize</c> no longer means "unlimited" —
|
|
/// deferred-work register row 87), and a bounded page without a total is a silent
|
|
/// truncation. <see cref="TotalCount"/> is what lets a caller tell "that is all of
|
|
/// them" from "there is more, ask for page 2". Mirrors
|
|
/// <c>AuditLogQueryResult</c> / <c>SecuredWriteListResult</c>, the other paged
|
|
/// management query results.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// <see cref="Page"/> and <see cref="PageSize"/> echo the values the server
|
|
/// actually applied, which are not necessarily the ones requested: both are
|
|
/// floored/clamped/defaulted server-side, so the echo is the only way a caller
|
|
/// learns the effective slice. Additive contract; new fields go on the end with a
|
|
/// default.
|
|
/// </para>
|
|
/// </summary>
|
|
/// <param name="Deployments">The requested page of deployment rows, newest first.</param>
|
|
/// <param name="TotalCount">Total rows matching the filter across all pages.</param>
|
|
/// <param name="Page">The 1-based page number actually served.</param>
|
|
/// <param name="PageSize">The page size actually applied.</param>
|
|
public record DeploymentQueryResult(
|
|
IReadOnlyList<Types.Deployment.DeploymentListRow> Deployments,
|
|
int TotalCount,
|
|
int Page,
|
|
int PageSize);
|
|
|
|
public record GetDeploymentDiffCommand(int InstanceId);
|