@@ -14,6 +14,7 @@ public class AdaptiveUploadController : IAdaptiveUploadController
1414 private const int MAX_CHUNK_SIZE_BYTES = 16 * 1024 * 1024 ; // 16 MB
1515 private const int MIN_PARALLELISM = 2 ;
1616 private const int MAX_PARALLELISM = 4 ;
17+ private const int CLIENT_TIMEOUTS_BEFORE_DOWNSCALE = 2 ;
1718
1819 private const double MULTIPLIER_2_X = 2.0 ;
1920 private const double MULTIPLIER_1_75_X = 1.75 ;
@@ -36,6 +37,7 @@ public class AdaptiveUploadController : IAdaptiveUploadController
3637 private readonly Queue < long > _recentBytes ;
3738 private int _successesInWindow ;
3839 private int _windowSize ;
40+ private int _consecutiveClientTimeouts ;
3941 private readonly ILogger < AdaptiveUploadController > _logger ;
4042 private readonly object _syncRoot = new ( ) ;
4143
@@ -86,11 +88,20 @@ public void RecordUploadResult(UploadResult uploadResult)
8688 {
8789 lock ( _syncRoot )
8890 {
89- if ( IsClientSideFailure ( uploadResult . FailureKind ) )
91+ if ( uploadResult . FailureKind == UploadFailureKind . ClientCancellation )
9092 {
93+ _consecutiveClientTimeouts = 0 ;
9194 return ;
9295 }
9396
97+ if ( uploadResult . FailureKind == UploadFailureKind . ClientTimeout )
98+ {
99+ HandleClientTimeout ( uploadResult . FileId ) ;
100+ return ;
101+ }
102+
103+ _consecutiveClientTimeouts = 0 ;
104+
94105 EnqueueSample ( uploadResult . Elapsed , uploadResult . IsSuccess , uploadResult . ActualBytes ) ;
95106
96107 if ( HandleBandwidthReset ( uploadResult . IsSuccess , uploadResult . StatusCode ) )
@@ -151,9 +162,26 @@ private void EnqueueSample(TimeSpan elapsed, bool isSuccess, long actualBytes)
151162 }
152163 }
153164
154- private static bool IsClientSideFailure ( UploadFailureKind failureKind )
165+ private void HandleClientTimeout ( string ? fileId )
155166 {
156- return failureKind is UploadFailureKind . ClientCancellation or UploadFailureKind . ClientTimeout ;
167+ _consecutiveClientTimeouts += 1 ;
168+ if ( _consecutiveClientTimeouts < CLIENT_TIMEOUTS_BEFORE_DOWNSCALE )
169+ {
170+ _logger . LogDebug (
171+ "Adaptive: file {FileId} client timeout {TimeoutCount}/{Threshold}. Waiting before downscale" ,
172+ fileId ?? "-" ,
173+ _consecutiveClientTimeouts ,
174+ CLIENT_TIMEOUTS_BEFORE_DOWNSCALE ) ;
175+
176+ return ;
177+ }
178+
179+ _logger . LogInformation (
180+ "Adaptive: file {FileId} client timeout threshold reached ({TimeoutCount}). Downscaling upload settings" ,
181+ fileId ?? "-" ,
182+ _consecutiveClientTimeouts ) ;
183+ _consecutiveClientTimeouts = 0 ;
184+ Downscale ( fileId , "client timeouts" ) ;
157185 }
158186
159187 private bool HandleBandwidthReset ( bool isSuccess , int ? statusCode )
@@ -192,40 +220,45 @@ private bool TryHandleDownscale(TimeSpan maxElapsed, string? fileId)
192220 {
193221 if ( maxElapsed > _downscaleThreshold )
194222 {
195- if ( _currentParallelism > MIN_PARALLELISM )
196- {
197- _logger . LogInformation (
198- "Adaptive: file {FileId} Downscale. Reducing parallelism {Prev} -> {Next}. Resetting window (window before {WindowBefore})" ,
199- fileId ?? "-" ,
200- _currentParallelism , _currentParallelism - 1 ,
201- _windowSize ) ;
202- _currentParallelism -= 1 ;
203- _windowSize = _currentParallelism ;
204- ResetWindow ( ) ;
205-
206- return true ;
207- }
208-
209- var reduced = ( int ) Math . Max ( MIN_CHUNK_SIZE_BYTES , _currentChunkSizeBytes * 0.75 ) ;
210- if ( reduced != _currentChunkSizeBytes )
211- {
212- _currentChunkSizeBytes = reduced ;
213- _logger . LogInformation (
214- "Adaptive: file {FileId} Downscale. maxElapsed={MaxElapsedMs} ms > {ThresholdMs} ms. New chunkSize={ChunkKb} KB" ,
215- fileId ?? "-" ,
216- maxElapsed . TotalMilliseconds ,
217- _downscaleThreshold . TotalMilliseconds ,
218- Math . Round ( _currentChunkSizeBytes / 1024d ) ) ;
219- }
220-
221- ResetWindow ( ) ;
222-
223+ Downscale ( fileId , $ "maxElapsed={ maxElapsed . TotalMilliseconds } ms > { _downscaleThreshold . TotalMilliseconds } ms") ;
223224 return true ;
224225 }
225226
226227 return false ;
227228 }
228229
230+ private void Downscale ( string ? fileId , string reason )
231+ {
232+ if ( _currentParallelism > MIN_PARALLELISM )
233+ {
234+ _logger . LogInformation (
235+ "Adaptive: file {FileId} Downscale ({Reason}). Reducing parallelism {Prev} -> {Next}. Resetting window (window before {WindowBefore})" ,
236+ fileId ?? "-" ,
237+ reason ,
238+ _currentParallelism ,
239+ _currentParallelism - 1 ,
240+ _windowSize ) ;
241+ _currentParallelism -= 1 ;
242+ _windowSize = _currentParallelism ;
243+ ResetWindow ( ) ;
244+
245+ return ;
246+ }
247+
248+ var reduced = ( int ) Math . Max ( MIN_CHUNK_SIZE_BYTES , _currentChunkSizeBytes * 0.75 ) ;
249+ if ( reduced != _currentChunkSizeBytes )
250+ {
251+ _currentChunkSizeBytes = reduced ;
252+ _logger . LogInformation (
253+ "Adaptive: file {FileId} Downscale ({Reason}). New chunkSize={ChunkKb} KB" ,
254+ fileId ?? "-" ,
255+ reason ,
256+ Math . Round ( _currentChunkSizeBytes / 1024d ) ) ;
257+ }
258+
259+ ResetWindow ( ) ;
260+ }
261+
229262 private void TryHandleUpscale ( string ? fileId )
230263 {
231264 var recentDurations = _recentDurations . ToArray ( ) ;
@@ -362,6 +395,7 @@ private void ResetState()
362395 _currentChunkSizeBytes = Math . Clamp ( INITIAL_CHUNK_SIZE_BYTES , MIN_CHUNK_SIZE_BYTES , MAX_CHUNK_SIZE_BYTES ) ;
363396 _currentParallelism = MIN_PARALLELISM ;
364397 _windowSize = _currentParallelism ;
398+ _consecutiveClientTimeouts = 0 ;
365399 }
366400
367401 ResetWindow ( ) ;
0 commit comments