perf(deploy): flatten-session caching, bulk DeploySiteAsync, paged management queries

This commit is contained in:
Joseph Doherty
2026-08-14 21:14:22 -04:00
parent ee193cd2bb
commit 48b3c40a7f
59 changed files with 3051 additions and 231 deletions
@@ -137,6 +137,57 @@ When deploying artifacts (shared scripts, external system definitions, etc.) to
- Deployment is performed at the **individual instance level**.
- The UI may provide convenience operations (e.g., "deploy all out-of-date instances at Site A"), but these decompose into individual instance deployments.
### Bulk site deployment (`DeploySiteAsync`, WP2.5)
`DeploySiteAsync(siteId, user)` deploys every instance at one site in a single
operation, surfaced as the `MgmtDeploySite` management command and the CLI
`deploy site --site-id`. It is the "deploy all at Site A" convenience above, made
first-class — it still decomposes into individual instance deployments, and each
instance keeps its own deployment id, revision hash, operation lock, and
optimistically-concurrent status record.
It runs the ordinary deployment pipeline in three phases, of which only the middle
one is parallel:
1. **Prepare (serial).** Validate transition, take the operation lock, flatten +
validate, run query-before-redeploy reconciliation, stage the
`PendingDeployment`, insert the `InProgress` record. Every step here touches the
scoped, non-thread-safe `DbContext`, so the phase is strictly serial. All
instances share ONE `FlattenSession`, so a template chain common to N instances
is walked once and the session-global queries (shared scripts, schema library,
the site's data connections) run once for the batch.
2. **Send (bounded parallel).** The `RefreshDeploymentCommand` round-trips run
concurrently up to `SiteDeploymentMaxParallelism` (default 4), each under a
`SiteDeploymentTimeoutPerInstance` deadline (default 120 s). This phase touches
no repository — that is exactly why it is the only phase allowed to run in
parallel. Shape mirrors `ArtifactDeploymentService.DeployCoreAsync`.
3. **Finalize (serial).** Commit terminal statuses, apply post-success side
effects, write audit rows, release each operation lock.
**Not all-or-nothing.** An instance that fails for any reason (wrong state, failed
validation, lock already held by another operation, site round-trip timed out) is
reported as a failed row while the rest proceed, and is individually retryable via
the ordinary single-instance deploy. This matches the artifact-deployment policy:
successful targets are never rolled back because another target failed.
`DeployInstanceAsync` is composed from the same three phase helpers with a batch of
one, so the two entry points cannot drift on deployment identity, idempotency, lock
coverage, or optimistic concurrency.
### Terminal deployment-record retention
Deployment records are insert-only — one row per deploy attempt — so without a
window the table grows for the life of the system and every unfiltered deployment
query degrades with age rather than with page size.
`TerminalDeploymentRecordRetention` (default **365 days**, deliberately generous
because deployment history is operator forensics) bounds it, swept opportunistically
and rate-limited on the deployment-list read path.
Only **terminal** rows (`Success` / `Failed`) are eligible. An `InProgress` row is
never purged regardless of age: it is precisely the row the query-before-redeploy
reconciliation reads to decide whether a prior deploy actually landed at the site,
and expiring it by age would silently disable that idempotency guard.
## Diff View
Before deploying, the Deployment Manager can request a diff from the Template Engine showing: