fix(vault): atomic streamed write via temp→rename, suppress OCE log noise

AddEntryStreamingAsync now writes to a temp file in the same vault directory,
renames it into place (POSIX rename(2) — atomic on Linux), and updates the
index only after the rename succeeds. A client disconnect or I/O fault during
the write leaves the original backing file intact and the index unchanged; the
temp file is cleaned up best-effort on failure. Fixes the data-corruption
regression on the replace path where a cancelled write could truncate the live
backing file after the index update and FileMode.Create already ran.

Also filters OperationCanceledException from error-level logging in
RegisterResourceStreamingAsync — a normal client disconnect is not an error.

Two tests added to AudioStoreStreamingTests covering cancel and fault on the
replace path.
This commit is contained in:
daniel-c-harvey
2026-06-25 15:46:58 -04:00
parent 79bbbd4956
commit beec36a382
3 changed files with 136 additions and 17 deletions
@@ -206,9 +206,13 @@ public class FileDatabase : DirectoryIndexDirectory, IDisposable
}
catch (Exception ex)
{
// Swallow and return false, matching RegisterResourceAsync. Logged (unlike the buffered
// path) because a streamed write failure can leave a partial backing file worth noticing.
_logger.LogError(ex, "RegisterResourceStreamingAsync failed for vault {VaultId} entry {EntryId}", vaultId, entryId);
// Swallow and return false, matching RegisterResourceAsync. Log at error for real failures
// only — a normal client cancel (OperationCanceledException) is not an error condition and
// would spam the error log on every client disconnect during a large upload or replace.
if (ex is not OperationCanceledException)
{
_logger.LogError(ex, "RegisterResourceStreamingAsync failed for vault {VaultId} entry {EntryId}", vaultId, entryId);
}
}
return false;