1- using Microsoft . Extensions . Hosting ;
1+ using System . Runtime . InteropServices ;
2+ using Microsoft . Extensions . Hosting ;
23using Microsoft . Extensions . Logging ;
34using Microsoft . Extensions . Options ;
45using ModelContextProtocol . Server ;
@@ -21,6 +22,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
2122 {
2223 ArgumentOutOfRangeException . ThrowIfLessThan ( options . Value . IdleTimeout , TimeSpan . Zero ) ;
2324 }
25+
2426 ArgumentOutOfRangeException . ThrowIfLessThan ( options . Value . MaxIdleSessionCount , 0 ) ;
2527
2628 try
@@ -31,8 +33,11 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
3133 var idleTimeoutTicks = options . Value . IdleTimeout . Ticks ;
3234 var maxIdleSessionCount = options . Value . MaxIdleSessionCount ;
3335
34- // The default ValueTuple Comparer will check the first item then the second which preserves both order and uniqueness.
35- var idleSessions = new SortedSet < ( long Timestamp , string SessionId ) > ( ) ;
36+ // Create two lists that will be reused between runs.
37+ // This assumes that the number of idle sessions is not breached frequently.
38+ // If the idle sessions often breach the maximum, a priority queue could be considered.
39+ var idleSessionsTimestamps = new List < long > ( ) ;
40+ var idleSessionSessionIds = new List < string > ( ) ;
3641
3742 while ( ! stoppingToken . IsCancellationRequested && await timer . WaitForNextTickAsync ( stoppingToken ) )
3843 {
@@ -56,26 +61,34 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
5661 continue ;
5762 }
5863
59- idleSessions . Add ( ( session . LastActivityTicks , session . Id ) ) ;
64+ // Add the timestamp and the session
65+ idleSessionsTimestamps . Add ( session . LastActivityTicks ) ;
66+ idleSessionSessionIds . Add ( session . Id ) ;
6067
6168 // Emit critical log at most once every 5 seconds the idle count it exceeded,
6269 // since the IdleTimeout will no longer be respected.
63- if ( idleSessions . Count == maxIdleSessionCount + 1 )
70+ if ( idleSessionsTimestamps . Count == maxIdleSessionCount + 1 )
6471 {
6572 LogMaxSessionIdleCountExceeded ( maxIdleSessionCount ) ;
6673 }
6774 }
6875
69- if ( idleSessions . Count > maxIdleSessionCount )
76+ if ( idleSessionsTimestamps . Count > maxIdleSessionCount )
7077 {
71- var sessionsToPrune = idleSessions . ToArray ( ) [ ..^ maxIdleSessionCount ] ;
72- foreach ( var ( _, id ) in sessionsToPrune )
78+ var timestamps = CollectionsMarshal . AsSpan ( idleSessionsTimestamps ) ;
79+
80+ // Sort only if the maximum is breached and sort solely by the timestamp. Sort both collections.
81+ timestamps . Sort ( CollectionsMarshal . AsSpan ( idleSessionSessionIds ) ) ;
82+
83+ var sessionsToPrune = CollectionsMarshal . AsSpan ( idleSessionSessionIds ) [ ..^ maxIdleSessionCount ] ;
84+ foreach ( var id in sessionsToPrune )
7385 {
7486 RemoveAndCloseSession ( id ) ;
7587 }
7688 }
7789
78- idleSessions . Clear ( ) ;
90+ idleSessionsTimestamps . Clear ( ) ;
91+ idleSessionSessionIds . Clear ( ) ;
7992 }
8093 }
8194 catch ( OperationCanceledException ) when ( stoppingToken . IsCancellationRequested )
@@ -145,4 +158,4 @@ private async Task DisposeSessionAsync(HttpMcpSession<StreamableHttpServerTransp
145158
146159 [ LoggerMessage ( Level = LogLevel . Critical , Message = "The IdleTrackingBackgroundService has stopped unexpectedly." ) ]
147160 private partial void IdleTrackingBackgroundServiceStoppedUnexpectedly ( ) ;
148- }
161+ }
0 commit comments