1
- using Microsoft . Extensions . Hosting ;
1
+ using System . Runtime . InteropServices ;
2
+ using Microsoft . Extensions . Hosting ;
2
3
using Microsoft . Extensions . Logging ;
3
4
using Microsoft . Extensions . Options ;
4
5
using ModelContextProtocol . Server ;
@@ -21,6 +22,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
21
22
{
22
23
ArgumentOutOfRangeException . ThrowIfLessThan ( options . Value . IdleTimeout , TimeSpan . Zero ) ;
23
24
}
25
+
24
26
ArgumentOutOfRangeException . ThrowIfLessThan ( options . Value . MaxIdleSessionCount , 0 ) ;
25
27
26
28
try
@@ -31,8 +33,11 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
31
33
var idleTimeoutTicks = options . Value . IdleTimeout . Ticks ;
32
34
var maxIdleSessionCount = options . Value . MaxIdleSessionCount ;
33
35
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 > ( ) ;
36
41
37
42
while ( ! stoppingToken . IsCancellationRequested && await timer . WaitForNextTickAsync ( stoppingToken ) )
38
43
{
@@ -56,26 +61,34 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
56
61
continue ;
57
62
}
58
63
59
- idleSessions . Add ( ( session . LastActivityTicks , session . Id ) ) ;
64
+ // Add the timestamp and the session
65
+ idleSessionsTimestamps . Add ( session . LastActivityTicks ) ;
66
+ idleSessionSessionIds . Add ( session . Id ) ;
60
67
61
68
// Emit critical log at most once every 5 seconds the idle count it exceeded,
62
69
// since the IdleTimeout will no longer be respected.
63
- if ( idleSessions . Count == maxIdleSessionCount + 1 )
70
+ if ( idleSessionsTimestamps . Count == maxIdleSessionCount + 1 )
64
71
{
65
72
LogMaxSessionIdleCountExceeded ( maxIdleSessionCount ) ;
66
73
}
67
74
}
68
75
69
- if ( idleSessions . Count > maxIdleSessionCount )
76
+ if ( idleSessionsTimestamps . Count > maxIdleSessionCount )
70
77
{
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 )
73
85
{
74
86
RemoveAndCloseSession ( id ) ;
75
87
}
76
88
}
77
89
78
- idleSessions . Clear ( ) ;
90
+ idleSessionsTimestamps . Clear ( ) ;
91
+ idleSessionSessionIds . Clear ( ) ;
79
92
}
80
93
}
81
94
catch ( OperationCanceledException ) when ( stoppingToken . IsCancellationRequested )
@@ -145,4 +158,4 @@ private async Task DisposeSessionAsync(HttpMcpSession<StreamableHttpServerTransp
145
158
146
159
[ LoggerMessage ( Level = LogLevel . Critical , Message = "The IdleTrackingBackgroundService has stopped unexpectedly." ) ]
147
160
private partial void IdleTrackingBackgroundServiceStoppedUnexpectedly ( ) ;
148
- }
161
+ }
0 commit comments