feat(controlplane): deployment artifact retention on the admin singleton

Every deploy stored the whole gzip'd fleet snapshot in Deployment.ArtifactBlob forever (R03:1.2).
DeploymentRetentionActor (admin singleton, proxy-less) sweeps every Deployments:Retention:
IntervalMinutes (60): terminal rows (Sealed / PartiallyFailed / TimedOut) older than the newest
KeepSealed (50) and not referenced by a NodeDeploymentState started/applied within ReferencedDays
(30) get their blob EMPTIED — never deleted, never NULLed: the column stays NOT NULL and every
reader already treats zero bytes as "no artifact" (#485), so no schema migration and no reader
change. In-flight rows are never touched; MaxRowsPerSweep (200) bounds one sweep.

Refs #733

Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 10:27:59 -04:00
parent f72af76e99
commit 1bf5ef916c
5 changed files with 428 additions and 1 deletions
@@ -26,7 +26,11 @@ public sealed class Deployment
public DateTime CreatedAtUtc { get; init; } = DateTime.UtcNow;
/// <summary>Gets or sets the serialized artifact blob containing the configuration.</summary>
public byte[] ArtifactBlob { get; init; } = Array.Empty<byte>();
/// <remarks>
/// Settable (not init-only) so the retention sweep can EMPTY it on an old terminal row. An empty blob
/// is "no artifact" to every reader (#485), so the audit row survives with its bytes retired.
/// </remarks>
public byte[] ArtifactBlob { get; set; } = Array.Empty<byte>();
/// <summary>Gets or sets the row version for optimistic concurrency control.</summary>
public byte[] RowVersion { get; set; } = Array.Empty<byte>();