fix(deploymentmanager): bulk site deploy skips up-to-date instances and un-poisons the shared DbContext (arch-review Low P7-5 + P7-6)
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.
This commit is contained in:
@@ -55,14 +55,25 @@ public static class DeployCommands
|
||||
private static Command BuildSite(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
||||
{
|
||||
var siteIdOption = new Option<int>("--site-id") { Description = "Target site ID", Required = true };
|
||||
// A flag, not a value option: absent means the default (skip unchanged
|
||||
// instances), present means redeploy them. Disabled instances are skipped
|
||||
// either way — force is not an override for that.
|
||||
var forceOption = new Option<bool>("--force")
|
||||
{
|
||||
Description =
|
||||
"Redeploy instances whose config is unchanged (default: skip them — a redeploy " +
|
||||
"resets static overrides and native alarm state at the site)",
|
||||
};
|
||||
var cmd = new Command("site") { Description = "Deploy every deployable instance at a site" };
|
||||
cmd.Add(siteIdOption);
|
||||
cmd.Add(forceOption);
|
||||
cmd.SetAction(async (ParseResult result) =>
|
||||
{
|
||||
var siteId = result.GetValue(siteIdOption);
|
||||
var force = result.GetValue(forceOption);
|
||||
return await CommandHelpers.ExecuteCommandAsync(
|
||||
result, urlOption, formatOption, usernameOption, passwordOption,
|
||||
new MgmtDeploySiteCommand(siteId),
|
||||
new MgmtDeploySiteCommand(siteId, force),
|
||||
// A bulk deploy is N instance round-trips; the default 30 s client
|
||||
// timeout would abandon the request while the server is still
|
||||
// applying. Matches the server-side long-running Ask window.
|
||||
|
||||
@@ -974,19 +974,35 @@ Site round-trips run with bounded concurrency
|
||||
`SiteDeploymentTimeoutPerInstance` (default 120 s), so one wedged instance cannot stall
|
||||
the batch. The client waits up to 5 minutes.
|
||||
|
||||
Two classes of instance are **skipped** — not flattened, not sent, not changed in any
|
||||
way — and reported in `skippedInstances` with a reason instead of counting as a success
|
||||
or a failure:
|
||||
|
||||
- **Disabled** instances. Deploy is a legal transition out of `Disabled` and re-enables
|
||||
the instance, so a site-wide push would silently restart equipment an operator
|
||||
deliberately stopped. Deploy them individually to re-enable.
|
||||
- **Up-to-date** instances — those whose freshly flattened config still matches the
|
||||
revision the site is running. A redeploy is destructive at the site (it resets static
|
||||
attribute overrides and clears native alarm state), so pushing one to an unchanged
|
||||
instance loses live state for no benefit. `--force` deploys them anyway; use it when
|
||||
central's recorded state cannot be trusted, e.g. after a site's data volume was
|
||||
restored or a node rebuilt. `--force` does **not** override the Disabled skip.
|
||||
|
||||
`--site-id` is **required** — unlike `deploy artifacts`, omitting it does not mean
|
||||
fleet-wide.
|
||||
|
||||
```sh
|
||||
scadabridge --url <url> deploy site --site-id <int>
|
||||
scadabridge --url <url> deploy site --site-id <int> [--force]
|
||||
```
|
||||
|
||||
| Option | Required | Description |
|
||||
|--------|----------|-------------|
|
||||
| `--site-id` | yes | Target site ID. A site-scoped (non-Administrator) Deployer must supply an in-scope site. |
|
||||
| `--force` | no | Redeploy instances whose config is unchanged (default: skip them). Disabled instances are still skipped. |
|
||||
|
||||
Output is a per-instance result matrix (`instanceId`, `uniqueName`, `deploymentId`,
|
||||
`success`, `errorMessage`) plus `successCount` / `failureCount`.
|
||||
`success`, `errorMessage`) plus `successCount` / `failureCount`, and `skippedInstances`
|
||||
(`instanceId`, `uniqueName`, `reason`).
|
||||
|
||||
#### `deploy artifacts`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user