fix(transport): release abandoned BundleSessions instead of leaking a slot (row 43)
Only BundleImporter.ApplyAsync released an import session, so every path that opened one and did NOT apply held one of MaxConcurrentImportSessions (8) — and the DECRYPTED bundle content that T-007 deliberately zeroes on the apply paths — for the full BundleSessionTtlMinutes (30). Eight abandonments locked out all imports on the active node; observed live (R6: 3 back-to-back suite runs held 6 of 8 slots until central-b was restarted). New IBundleImporter.Abandon(sessionId): the same zero-then-remove release ApplyAsync performs, idempotent (double release / unknown id / expired id are no-ops) and only ever given the random-GUID id its caller opened, so it can never evict another circuit's session. Wired into every abandonment path: - Central UI wizard — ReleaseSession() from new upload, TryLoadAsync (before it overwrites _session), Back-to-Upload / ResetSessionState, the 3-strike lockout reset, and Dispose (navigation away / circuit teardown). - CLI transport preview — a preview never applies, so nothing released it at all; now released in a finally. - CLI transport import — covers the pre-apply aborts (blocker rows, unresolved site/connection mappings). Class doc's "an abandoned wizard does not leak" claim is true again (it only ever covered the staged plaintext temp file). Component-Transport.md + docs/components/Transport.md now record the explicit-release contract and that BundleSessionEvictionService is a TTL backstop, not an abandonment path. Tests: 4 bUnit tests over a REAL BundleSessionStore (dispose / back-to-upload / apply-then-dispose idempotency / concurrent-circuit isolation) — 3 of the 4 fail against the pre-fix page; 4 importer tests (release + zeroing, idempotency, cross-session isolation, and the slot-cap regression).
This commit is contained in:
@@ -1715,6 +1715,32 @@ public sealed class BundleImporter : IBundleImporter
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// Register row 43: every abandonment path (Central UI wizard reset /
|
||||
/// back-to-upload / circuit teardown, CLI preview, CLI import that aborts
|
||||
/// before Apply) funnels here so a session that will never be applied stops
|
||||
/// holding one of the <c>MaxConcurrentImportSessions</c> slots — and stops
|
||||
/// holding its decrypted plaintext — for the rest of its TTL. Applies the same
|
||||
/// zero-then-remove ordering as the T-007 release inside
|
||||
/// <see cref="ApplyAsync"/>: zeroing first means a caller still holding the
|
||||
/// session reference (the Razor page keeps one for the Result step) observes
|
||||
/// the cleared buffer too.
|
||||
/// </remarks>
|
||||
public void Abandon(Guid sessionId)
|
||||
{
|
||||
// Get() returns null for an unknown or already-expired id, which makes the
|
||||
// double-release after ApplyAsync (which already removed it) a no-op rather
|
||||
// than a throw. Remove is called unconditionally so an expired-but-still-
|
||||
// resident entry is dropped as well.
|
||||
var session = _sessionStore.Get(sessionId);
|
||||
if (session is not null)
|
||||
{
|
||||
ZeroDecryptedContent(session);
|
||||
}
|
||||
_sessionStore.Remove(sessionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the deployed instances stale-ed by this import.
|
||||
/// Overwriting a template changes the flattened-config hash of every instance
|
||||
|
||||
Reference in New Issue
Block a user