fix(install): cut the swap's stopped window to the swapped-in folder (#905)
v2-ci / unit-tests (push) Has been cancelled

Root cause: the protected-DACL coverage scan, not the ACL writes. Both -Reapply and
-Verify walked every denied folder (all of E:\ApiInstall on vd03, nested roots again)
with Get-ChildItem -Recurse + Get-Acl per item + a pipeline per item for -Skip, about
3.4 ms an item on PS 5.1, all while the service was stopped. An unchanged deny is read,
not rewritten; the re-applied install-folder deny propagates over ~1,400 files in 0.5 s.

- Find-OtOpcUaDenyCoverageGap: .NET enumeration, -Skip folders pruned, DACL-only
  GetAccessControl(Access) (about 0.15 ms an item); reparse points not followed.
- Get-OtOpcUaCoverageWalkRoots: each denied folder walked once (outermost only).
- Install-ScriptWorkerIsolation.ps1 -Scope All|InstallRoot|Outside (with -Reapply,
  -Verify, -PlanOnly). InstallRoot = the folder ACEs a swap drops; Outside = the rest,
  including the firewall rule (its query costs 3-5 s; its exe path does not change).
  The two partition the plan. Prints the scan time.
- Invoke-OtOpcUaInstallSwap.ps1: -Reapply/-Verify -Scope Outside before the stop (a
  failure stops before anything moves), -Scope InstallRoot inside the window; preflight
  checks the candidate carries the worker exe; prints the stopped window and its parts.
  -NoStart, -NoCarry and the pre-#901 refusal unchanged.

windev, vd03-like tree (17,851 items): stopped window 153.1 s -> 6.0 s (PS 5.1),
101.6 s -> 6.2 s (PS 7); the Outside part takes ~23 s with the old build serving.
Dry-run harness 101 -> 123 passed; Windows test 72 passed on PS 5.1 and PS 7.

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
Joseph Doherty
2026-11-13 03:51:41 -05:00
parent f817c77e02
commit 0c19f26c81
8 changed files with 475 additions and 58 deletions
@@ -331,3 +331,13 @@ rollback.
| vd03 | `fea7de0a` about **10 s** (stop 2 s); `6c4d2990` about **15 s** (stop 6 s), stop → `/health/ready` | **about 20–40 s.** The data robocopy and the in-script `-Reapply`/`-Verify` now run while the service is stopped. The step-2 record write happens with the service up. |
| QA pair | `fea7de0a` window **about 2 min** pair outage (16:48:13 → 16:50:14), of which about 70 s was the isolation install + uninstall drill. The first node took 39 s to ready because it waits for its peer; the second took 8 s. | **about 1–1.5 min** pair outage: stops about 1–2 s each, backup + swap + `-Reapply`/`-Verify` per node, then about 40 s + 8 s to ready. One-node restarts (the OTel record) took up to 32 s to stop a Secondary with its peer up. Stopping both together avoids that graceful-leave wait. |
| QA failover gate | 30 s graceful leave, then the new Primary at 250 on its first sample | same |
**After lmxopcua#905 (for the next pack that uses `Invoke-OtOpcUaInstallSwap.ps1`).** The 101 s (vd03) and 50–60 s per
QA node were the protected-DACL coverage scan (`Get-Acl` per item over every denied folder, in both `-Reapply` and
`-Verify`, while stopped), not the ACL writes. The swap now runs `-Reapply`/`-Verify -Scope Outside` (the full scan
included) **before** the stop and only `-Scope InstallRoot` inside the window. windev, a vd03-like tree of 17,851 items
under the denied folder: stopped window **153.1 s → 6.0 s** (Windows PowerShell 5.1) and **101.6 s → 6.2 s** (PowerShell 7);
the pre-stop part takes about 23 s with the old build still serving. Estimate for the next vd03 swap: stop (2–6 s) +
the `data\` backup + about 6 s + start-up to `/health/ready` (about 8–10 s), i.e. **about 20–30 s**. QA: about 10 s of
swap per node inside the pair outage instead of 50–60 s, and the Secondary-first order (deviation 3) is no longer needed
to hide it. The script prints `stopped window (…): N s [stop, move, carry, isolation]`; record that line.
@@ -130,6 +130,8 @@ old Primary first.
1. vd03 downtime about 101 s against an estimate of 20–40 s, because the coverage walk runs inside `-Reapply`/`-Verify`
while the service is stopped. QA swaps took about 50–60 s per node for the same reason.
→ lmxopcua#905: fixed (scan about 20× faster per item, walked once, and only the swapped-in folder inside the
window; windev 17.9k items: stopped window 153 s → 6 s on PS 5.1, 102 s → 6 s on PS 7).
2. #903: the edit-and-revert cannot return the chip to "In sync", because `RowVersion` is part of the revision hash (see
above). Draft content is restored; the chip shows drift until the next deploy.
3. QA order: the Secondary was stopped and swapped while the Primary still served, then the Primary. Both started