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:
+55
-5
@@ -51,6 +51,21 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Design;
|
||||
/// retained on the component. The file is deleted on Back-to-Upload / Reset /
|
||||
/// successful Apply / component Dispose, so an abandoned wizard does not leak
|
||||
/// staged bundle plaintext beyond circuit teardown.
|
||||
///
|
||||
/// <para>
|
||||
/// Server-side session release (register row 43): the staged temp file is only
|
||||
/// half of an abandoned wizard's footprint — the server-side
|
||||
/// <see cref="BundleSession"/> opened by <see cref="IBundleImporter.LoadAsync"/>
|
||||
/// holds the DECRYPTED bundle content and occupies one of
|
||||
/// <c>TransportOptions.MaxConcurrentImportSessions</c> slots for the full
|
||||
/// <c>BundleSessionTtlMinutes</c>. Only <see cref="IBundleImporter.ApplyAsync"/>
|
||||
/// used to release it, so eight abandoned wizards locked out every import on the
|
||||
/// active node. Every path that drops <c>_session</c> — new upload, Back-to-Upload,
|
||||
/// <see cref="ResetSessionState"/>, lockout reset, circuit teardown via
|
||||
/// <see cref="Dispose"/> — now goes through <see cref="ReleaseSession"/>, which
|
||||
/// calls <see cref="IBundleImporter.Abandon"/> (zero + remove, idempotent and
|
||||
/// safe after a successful Apply has already released the same id).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public partial class TransportImport : ComponentBase, IDisposable
|
||||
{
|
||||
@@ -149,7 +164,9 @@ public partial class TransportImport : ComponentBase, IDisposable
|
||||
{
|
||||
_errorMessage = null;
|
||||
_uploadInProgress = true;
|
||||
_session = null;
|
||||
// Release the previous upload's server-side session before we lose the
|
||||
// only reference to its id — otherwise re-selecting a file leaks a slot.
|
||||
ReleaseSession();
|
||||
DeleteBundleTempFile();
|
||||
try
|
||||
{
|
||||
@@ -207,6 +224,10 @@ public partial class TransportImport : ComponentBase, IDisposable
|
||||
_errorMessage = "No bundle staged — please re-select the file.";
|
||||
return;
|
||||
}
|
||||
// A successful LoadAsync overwrites _session; release whatever it is
|
||||
// replacing first so the id of the outgoing session is never dropped on
|
||||
// the floor (that would strand its slot for the full TTL).
|
||||
ReleaseSession();
|
||||
try
|
||||
{
|
||||
// Read the staged bundle straight off disk; the
|
||||
@@ -808,7 +829,7 @@ public partial class TransportImport : ComponentBase, IDisposable
|
||||
|
||||
private void ResetSessionState()
|
||||
{
|
||||
_session = null;
|
||||
ReleaseSession();
|
||||
DeleteBundleTempFile();
|
||||
_preview = null;
|
||||
_resolutions = null;
|
||||
@@ -854,12 +875,41 @@ public partial class TransportImport : ComponentBase, IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the staged temp file does not survive circuit
|
||||
/// teardown. Blazor invokes Dispose when the user navigates away or the
|
||||
/// circuit ends, so an abandoned wizard cleans up automatically.
|
||||
/// Releases the server-side <see cref="BundleSession"/> this wizard opened
|
||||
/// (zeroing its decrypted content) and clears the local reference. Idempotent:
|
||||
/// a null <c>_session</c> is a no-op, and <see cref="IBundleImporter.Abandon"/>
|
||||
/// tolerates an id that <see cref="IBundleImporter.ApplyAsync"/> already
|
||||
/// released. Only ever passes the id THIS circuit opened, so a concurrent
|
||||
/// operator's session is never touched.
|
||||
/// <para>
|
||||
/// Abandon failure must not block the wizard (or, from <see cref="Dispose"/>,
|
||||
/// tear down the circuit), so it is swallowed on the same best-effort footing
|
||||
/// as the staged-file delete below.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private void ReleaseSession()
|
||||
{
|
||||
var session = _session;
|
||||
_session = null;
|
||||
if (session is null) return;
|
||||
try
|
||||
{
|
||||
BundleImporter.Abandon(session.SessionId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort: the session's own TTL is the backstop.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures neither the staged temp file nor the server-side bundle session
|
||||
/// survives circuit teardown. Blazor invokes Dispose when the user navigates
|
||||
/// away or the circuit ends, so an abandoned wizard cleans up automatically.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
ReleaseSession();
|
||||
DeleteBundleTempFile();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user