Fix large CMS upload timeout with idle heartbeat and add per-file progress meter
Replace the 100s default HttpClient timeout (set Timeout=Infinite) with an idle/heartbeat deadline driven by a ProgressStreamContent wrapper that reports bytes-on-the-wire. Each tick resets the idle window and advances a MudProgressLinear per upload row. Idle window is configurable via Upload:IdleTimeoutSeconds (default 90s).
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
using System.Net;
|
||||
|
||||
namespace DeepDrftManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="HttpContent"/> that streams a source stream to the wire while reporting cumulative
|
||||
/// bytes written after each chunk. This is the single source of truth for both the upload progress
|
||||
/// meter and the idle/heartbeat timeout: every reported tick advances the UI <em>and</em> resets the
|
||||
/// idle deadline, so one mechanism feeds both concerns.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Wrap the audio payload (not the whole multipart container) so <see cref="TryComputeLength"/>
|
||||
/// returns the file length and the reported byte counts map directly onto "bytes of this file".
|
||||
/// </remarks>
|
||||
public sealed class ProgressStreamContent : HttpContent
|
||||
{
|
||||
// 80 KB: large enough to keep the socket fed on a healthy link, small enough that a stalled
|
||||
// connection trips the idle window without a multi-MB write swallowing the whole heartbeat budget.
|
||||
private const int CopyBufferSize = 81_920;
|
||||
|
||||
private readonly Stream _source;
|
||||
private readonly long _length;
|
||||
private readonly Action<long> _onBytesWritten;
|
||||
|
||||
/// <param name="source">The payload stream. Read once, sequentially — not seekable-rewound.</param>
|
||||
/// <param name="length">Total bytes the source will yield; sets Content-Length and the meter denominator.</param>
|
||||
/// <param name="onBytesWritten">Invoked after each chunk with the cumulative bytes written so far.</param>
|
||||
public ProgressStreamContent(Stream source, long length, Action<long> onBytesWritten)
|
||||
{
|
||||
_source = source;
|
||||
_length = length;
|
||||
_onBytesWritten = onBytesWritten;
|
||||
}
|
||||
|
||||
// Token-aware overload (.NET 5+): HttpClient calls this on the send path and passes the request's
|
||||
// CancellationToken, so the idle-heartbeat CTS aborts an in-flight read/write promptly — not just
|
||||
// between chunks. The parameterless base override delegates here with CancellationToken.None.
|
||||
protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken)
|
||||
=> CopyAsync(stream, cancellationToken);
|
||||
|
||||
protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context)
|
||||
=> CopyAsync(stream, CancellationToken.None);
|
||||
|
||||
private async Task CopyAsync(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
var buffer = new byte[CopyBufferSize];
|
||||
long written = 0;
|
||||
int read;
|
||||
while ((read = await _source.ReadAsync(buffer, cancellationToken)) > 0)
|
||||
{
|
||||
await stream.WriteAsync(buffer.AsMemory(0, read), cancellationToken);
|
||||
written += read;
|
||||
// Report after the bytes are on the wire — a tick means real forward progress, which is
|
||||
// exactly the signal the idle heartbeat must reset on.
|
||||
_onBytesWritten(written);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool TryComputeLength(out long length)
|
||||
{
|
||||
length = _length;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user