mirror of
https://github.com/sshnet/SSH.NET.git
synced 2026-10-16 21:12:58 +08:00
Make MaxPendingReads configurable on SftpClient to prevent connection freezes on resource-constrained platforms
Add MaxPendingReads property to ISftpClient and SftpClient (default 100 for backward compatibility). Pass the value through SftpFileStream.Open/OpenAsync instead of using a hardcoded constant. Users on Android or other constrained platforms can now reduce this value (e.g., to 10) to prevent freezes when downloading larger files. Agent-Logs-Url: https://github.com/sshnet/SSH.NET/sessions/b4b9b5e6-e214-41a2-9efd-0c2a63f65426 Co-authored-by: WojciechNagorski <17333903+WojciechNagorski@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
0a0e5e0ad9
commit
56f9e2a744
@@ -49,6 +49,27 @@ namespace Renci.SshNet
|
||||
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
|
||||
uint BufferSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of pending read requests allowed in read-ahead mode.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The maximum number of pending read requests. The default value is 100.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This controls how many SSH_FXP_READ requests can be in-flight simultaneously
|
||||
/// when sequentially reading a file. Higher values allow the library to pipeline
|
||||
/// more requests, improving throughput on high-latency connections.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// On resource-constrained platforms (e.g., mobile devices), reducing this value
|
||||
/// can prevent connection stalls when downloading larger files.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><paramref name="value"/> is less than 1.</exception>
|
||||
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
|
||||
int MaxPendingReads { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the operation timeout.
|
||||
/// </summary>
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Renci.SshNet.Sftp
|
||||
/// </summary>
|
||||
public sealed partial class SftpFileStream : Stream
|
||||
{
|
||||
private const int MaxPendingReads = 100;
|
||||
private readonly int _maxPendingReads;
|
||||
|
||||
private readonly ISftpSession _session;
|
||||
private readonly FileAccess _access;
|
||||
@@ -140,6 +140,7 @@ namespace Renci.SshNet.Sftp
|
||||
int writeBufferSize,
|
||||
byte[] handle,
|
||||
long position,
|
||||
int maxPendingReads,
|
||||
SftpFileReader? initialReader)
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(30);
|
||||
@@ -148,6 +149,7 @@ namespace Renci.SshNet.Sftp
|
||||
_session = session;
|
||||
_access = access;
|
||||
_canSeek = canSeek;
|
||||
_maxPendingReads = maxPendingReads;
|
||||
|
||||
Handle = handle;
|
||||
_readBufferSize = readBufferSize;
|
||||
@@ -163,9 +165,10 @@ namespace Renci.SshNet.Sftp
|
||||
FileMode mode,
|
||||
FileAccess access,
|
||||
int bufferSize,
|
||||
bool isDownloadFile = false)
|
||||
bool isDownloadFile = false,
|
||||
int maxPendingReads = 100)
|
||||
{
|
||||
return Open(session, path, mode, access, bufferSize, isDownloadFile, isAsync: false, CancellationToken.None).GetAwaiter().GetResult();
|
||||
return Open(session, path, mode, access, bufferSize, maxPendingReads, isDownloadFile, isAsync: false, CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
internal static Task<SftpFileStream> OpenAsync(
|
||||
@@ -175,9 +178,10 @@ namespace Renci.SshNet.Sftp
|
||||
FileAccess access,
|
||||
int bufferSize,
|
||||
CancellationToken cancellationToken,
|
||||
bool isDownloadFile = false)
|
||||
bool isDownloadFile = false,
|
||||
int maxPendingReads = 100)
|
||||
{
|
||||
return Open(session, path, mode, access, bufferSize, isDownloadFile, isAsync: true, cancellationToken);
|
||||
return Open(session, path, mode, access, bufferSize, maxPendingReads, isDownloadFile, isAsync: true, cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task<SftpFileStream> Open(
|
||||
@@ -186,6 +190,7 @@ namespace Renci.SshNet.Sftp
|
||||
FileMode mode,
|
||||
FileAccess access,
|
||||
int bufferSize,
|
||||
int maxPendingReads,
|
||||
bool isDownloadFile,
|
||||
bool isAsync,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -309,15 +314,15 @@ namespace Renci.SshNet.Sftp
|
||||
// so we can let there be several in-flight requests from the get go.
|
||||
// This optimisation is mostly only beneficial to smaller files on higher latency connections.
|
||||
// The +2 is +1 for rounding up to cover the whole file, and +1 for the final request to receive EOF.
|
||||
var initialPendingReads = (int)Math.Max(1, Math.Min(MaxPendingReads, 2 + (attributes.Size / readBufferSize)));
|
||||
var initialPendingReads = (int)Math.Max(1, Math.Min(maxPendingReads, 2 + (attributes.Size / readBufferSize)));
|
||||
|
||||
initialReader = new(handle, session, readBufferSize, position, MaxPendingReads, (ulong)attributes.Size, initialPendingReads);
|
||||
initialReader = new(handle, session, readBufferSize, position, maxPendingReads, (ulong)attributes.Size, initialPendingReads);
|
||||
}
|
||||
else if ((access & FileAccess.Read) == FileAccess.Read)
|
||||
{
|
||||
// The reader can use the size information to reduce in-flight requests near the expected EOF,
|
||||
// so pass it in here.
|
||||
initialReader = new(handle, session, readBufferSize, position, MaxPendingReads, (ulong)attributes.Size);
|
||||
initialReader = new(handle, session, readBufferSize, position, maxPendingReads, (ulong)attributes.Size);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -327,7 +332,7 @@ namespace Renci.SshNet.Sftp
|
||||
canSeek = false;
|
||||
}
|
||||
|
||||
return new SftpFileStream(session, path, access, canSeek, readBufferSize, writeBufferSize, handle, position, initialReader);
|
||||
return new SftpFileStream(session, path, access, canSeek, readBufferSize, writeBufferSize, handle, position, maxPendingReads, initialReader);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -421,7 +426,7 @@ namespace Renci.SshNet.Sftp
|
||||
if (_sftpFileReader is null)
|
||||
{
|
||||
Flush();
|
||||
_sftpFileReader = new(Handle, _session, _readBufferSize, _position, MaxPendingReads);
|
||||
_sftpFileReader = new(Handle, _session, _readBufferSize, _position, _maxPendingReads);
|
||||
}
|
||||
|
||||
_readBuffer = _sftpFileReader.ReadAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
@@ -475,7 +480,7 @@ namespace Renci.SshNet.Sftp
|
||||
{
|
||||
await FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_sftpFileReader = new(Handle, _session, _readBufferSize, _position, MaxPendingReads);
|
||||
_sftpFileReader = new(Handle, _session, _readBufferSize, _position, _maxPendingReads);
|
||||
}
|
||||
|
||||
_readBuffer = await _sftpFileReader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -42,6 +42,11 @@ namespace Renci.SshNet
|
||||
/// </summary>
|
||||
private uint _bufferSize;
|
||||
|
||||
/// <summary>
|
||||
/// Holds the maximum number of pending reads.
|
||||
/// </summary>
|
||||
private int _maxPendingReads;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the operation timeout.
|
||||
/// </summary>
|
||||
@@ -112,6 +117,45 @@ namespace Renci.SshNet
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of pending read requests allowed in read-ahead mode.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The maximum number of pending read requests. The default value is 100.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This controls how many SSH_FXP_READ requests can be in-flight simultaneously
|
||||
/// when sequentially reading a file. Higher values allow the library to pipeline
|
||||
/// more requests, improving throughput on high-latency connections.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// On resource-constrained platforms (e.g., mobile devices), reducing this value
|
||||
/// can prevent connection stalls when downloading larger files.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><paramref name="value"/> is less than 1.</exception>
|
||||
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
|
||||
public int MaxPendingReads
|
||||
{
|
||||
get
|
||||
{
|
||||
CheckDisposed();
|
||||
return _maxPendingReads;
|
||||
}
|
||||
set
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
if (value < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(value), "Cannot be less than one.");
|
||||
}
|
||||
|
||||
_maxPendingReads = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this client is connected to the server and
|
||||
/// the SFTP session is open.
|
||||
@@ -279,6 +323,7 @@ namespace Renci.SshNet
|
||||
{
|
||||
_operationTimeout = Timeout.Infinite;
|
||||
_bufferSize = 1024 * 32;
|
||||
_maxPendingReads = 100;
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
@@ -1544,7 +1589,7 @@ namespace Renci.SshNet
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
return SftpFileStream.Open(_sftpSession, path, FileMode.Create, FileAccess.ReadWrite, bufferSize);
|
||||
return SftpFileStream.Open(_sftpSession, path, FileMode.Create, FileAccess.ReadWrite, bufferSize, maxPendingReads: _maxPendingReads);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -1682,7 +1727,7 @@ namespace Renci.SshNet
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
return SftpFileStream.Open(_sftpSession, path, mode, access, (int)_bufferSize);
|
||||
return SftpFileStream.Open(_sftpSession, path, mode, access, (int)_bufferSize, maxPendingReads: _maxPendingReads);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1703,7 +1748,7 @@ namespace Renci.SshNet
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
return SftpFileStream.OpenAsync(_sftpSession, path, mode, access, (int)_bufferSize, cancellationToken);
|
||||
return SftpFileStream.OpenAsync(_sftpSession, path, mode, access, (int)_bufferSize, cancellationToken, maxPendingReads: _maxPendingReads);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -2357,7 +2402,8 @@ namespace Renci.SshNet
|
||||
FileAccess.Read,
|
||||
(int)_bufferSize,
|
||||
cancellationToken,
|
||||
isDownloadFile: true).ConfigureAwait(false);
|
||||
isDownloadFile: true,
|
||||
maxPendingReads: _maxPendingReads).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2369,7 +2415,8 @@ namespace Renci.SshNet
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
(int)_bufferSize,
|
||||
isDownloadFile: true);
|
||||
isDownloadFile: true,
|
||||
maxPendingReads: _maxPendingReads);
|
||||
}
|
||||
|
||||
// The below is effectively sftpStream.CopyTo{Async}(output) with consideration
|
||||
|
||||
Reference in New Issue
Block a user