@@ -22,6 +22,7 @@ public abstract class HttpRequestor : IDisposable
2222
2323 private static long requestCount = 0 ;
2424 private static SemaphoreSlim availableConnections ;
25+ private static int connectionLimitConfigured = 0 ;
2526
2627 private readonly ProductInfoHeaderValue userAgentHeader ;
2728
@@ -37,8 +38,12 @@ static HttpRequestor()
3738 using ( var machineConfigLock = GetMachineConfigLock ( ) )
3839 {
3940 ServicePointManager . SecurityProtocol = ServicePointManager . SecurityProtocol | SecurityProtocolType . Tls12 ;
40- ServicePointManager . DefaultConnectionLimit = Environment . ProcessorCount ;
41- availableConnections = new SemaphoreSlim ( ServicePointManager . DefaultConnectionLimit ) ;
41+
42+ // HTTP downloads are I/O-bound, not CPU-bound, so we default to
43+ // 2x ProcessorCount. Can be overridden via gvfs.max-http-connections.
44+ int connectionLimit = 2 * Environment . ProcessorCount ;
45+ ServicePointManager . DefaultConnectionLimit = connectionLimit ;
46+ availableConnections = new SemaphoreSlim ( connectionLimit ) ;
4247 }
4348 }
4449
@@ -50,6 +55,13 @@ protected HttpRequestor(ITracer tracer, RetryConfig retryConfig, Enlistment enli
5055
5156 this . Tracer = tracer ;
5257
58+ // On first instantiation, check git config for a custom connection limit.
59+ // This runs before any requests are made (during mount initialization).
60+ if ( Interlocked . CompareExchange ( ref connectionLimitConfigured , 1 , 0 ) == 0 )
61+ {
62+ TryApplyConnectionLimitFromConfig ( tracer , enlistment ) ;
63+ }
64+
5365 HttpClientHandler httpClientHandler = new HttpClientHandler ( ) { UseDefaultCredentials = true } ;
5466
5567 this . authentication . ConfigureHttpClientHandlerSslIfNeeded ( this . Tracer , httpClientHandler , enlistment . CreateGitProcess ( ) ) ;
@@ -362,6 +374,58 @@ private static bool TryGetResponseMessageFromHttpRequestException(HttpRequestExc
362374
363375 }
364376
377+ private static void TryApplyConnectionLimitFromConfig ( ITracer tracer , Enlistment enlistment )
378+ {
379+ try
380+ {
381+ GitProcess . ConfigResult result = enlistment . CreateGitProcess ( ) . GetFromConfig ( GVFSConstants . GitConfig . MaxHttpConnectionsConfig ) ;
382+ string error ;
383+ int configuredLimit ;
384+ if ( ! result . TryParseAsInt ( 0 , 1 , out configuredLimit , out error ) )
385+ {
386+ EventMetadata metadata = new EventMetadata ( ) ;
387+ metadata . Add ( "error" , error ) ;
388+ tracer . RelatedWarning ( metadata , "HttpRequestor: Invalid gvfs.max-http-connections config value, using default" ) ;
389+ return ;
390+ }
391+
392+ if ( configuredLimit > 0 )
393+ {
394+ int currentLimit = ServicePointManager . DefaultConnectionLimit ;
395+ ServicePointManager . DefaultConnectionLimit = configuredLimit ;
396+
397+ // Adjust the existing semaphore rather than replacing it, so any
398+ // in-flight waiters release permits to the correct instance.
399+ int delta = configuredLimit - currentLimit ;
400+ if ( delta > 0 )
401+ {
402+ for ( int i = 0 ; i < delta ; i ++ )
403+ {
404+ availableConnections . Release ( ) ;
405+ }
406+ }
407+ else if ( delta < 0 )
408+ {
409+ for ( int i = 0 ; i < - delta ; i ++ )
410+ {
411+ availableConnections . Wait ( ) ;
412+ }
413+ }
414+
415+ EventMetadata metadata = new EventMetadata ( ) ;
416+ metadata . Add ( "configuredLimit" , configuredLimit ) ;
417+ metadata . Add ( "previousLimit" , currentLimit ) ;
418+ tracer . RelatedEvent ( EventLevel . Informational , "HttpRequestor_ConnectionLimitConfigured" , metadata ) ;
419+ }
420+ }
421+ catch ( Exception e )
422+ {
423+ EventMetadata metadata = new EventMetadata ( ) ;
424+ metadata . Add ( "Exception" , e . ToString ( ) ) ;
425+ tracer . RelatedWarning ( metadata , "HttpRequestor: Failed to read gvfs.max-http-connections config, using default" ) ;
426+ }
427+ }
428+
365429 private static FileStream GetMachineConfigLock ( )
366430 {
367431 var machineConfigLocation = RuntimeEnvironment . SystemConfigurationFile ;
0 commit comments