1- using System . Runtime . InteropServices ;
2- using Microsoft . Extensions . Hosting ;
1+ using Microsoft . Extensions . Hosting ;
32using Microsoft . Extensions . Logging ;
43using Microsoft . Extensions . Options ;
5- using ModelContextProtocol . Server ;
64
75namespace ModelContextProtocol . AspNetCore ;
86
97internal sealed partial class IdleTrackingBackgroundService (
10- StreamableHttpHandler handler ,
8+ StatefulSessionManager sessions ,
119 IOptions < HttpServerTransportOptions > options ,
1210 IHostApplicationLifetime appLifetime ,
1311 ILogger < IdleTrackingBackgroundService > logger ) : BackgroundService
1412{
15- // The compiler will complain about the parameter being unused otherwise despite the source generator .
13+ // Workaround for https://github.com/dotnet/runtime/issues/91121. This is fixed in .NET 9 and later .
1614 private readonly ILogger _logger = logger ;
1715
1816 protected override async Task ExecuteAsync ( CancellationToken stoppingToken )
@@ -30,65 +28,9 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
3028 var timeProvider = options . Value . TimeProvider ;
3129 using var timer = new PeriodicTimer ( TimeSpan . FromSeconds ( 5 ) , timeProvider ) ;
3230
33- var idleTimeoutTicks = options . Value . IdleTimeout . Ticks ;
34- var maxIdleSessionCount = options . Value . MaxIdleSessionCount ;
35-
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 > ( ) ;
41-
4231 while ( ! stoppingToken . IsCancellationRequested && await timer . WaitForNextTickAsync ( stoppingToken ) )
4332 {
44- var idleActivityCutoff = idleTimeoutTicks switch
45- {
46- < 0 => long . MinValue ,
47- var ticks => timeProvider . GetTimestamp ( ) - ticks ,
48- } ;
49-
50- foreach ( var ( _, session ) in handler . Sessions )
51- {
52- if ( session . IsActive || session . SessionClosed . IsCancellationRequested )
53- {
54- // There's a request currently active or the session is already being closed.
55- continue ;
56- }
57-
58- if ( session . LastActivityTicks < idleActivityCutoff )
59- {
60- RemoveAndCloseSession ( session . Id ) ;
61- continue ;
62- }
63-
64- // Add the timestamp and the session
65- idleSessionsTimestamps . Add ( session . LastActivityTicks ) ;
66- idleSessionSessionIds . Add ( session . Id ) ;
67-
68- // Emit critical log at most once every 5 seconds the idle count it exceeded,
69- // since the IdleTimeout will no longer be respected.
70- if ( idleSessionsTimestamps . Count == maxIdleSessionCount + 1 )
71- {
72- LogMaxSessionIdleCountExceeded ( maxIdleSessionCount ) ;
73- }
74- }
75-
76- if ( idleSessionsTimestamps . Count > maxIdleSessionCount )
77- {
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 )
85- {
86- RemoveAndCloseSession ( id ) ;
87- }
88- }
89-
90- idleSessionsTimestamps . Clear ( ) ;
91- idleSessionSessionIds . Clear ( ) ;
33+ await sessions . PruneIdleSessionsAsync ( stoppingToken ) ;
9234 }
9335 }
9436 catch ( OperationCanceledException ) when ( stoppingToken . IsCancellationRequested )
@@ -98,17 +40,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
9840 {
9941 try
10042 {
101- List < Task > disposeSessionTasks = [ ] ;
102-
103- foreach ( var ( sessionKey , _) in handler . Sessions )
104- {
105- if ( handler . Sessions . TryRemove ( sessionKey , out var session ) )
106- {
107- disposeSessionTasks . Add ( DisposeSessionAsync ( session ) ) ;
108- }
109- }
110-
111- await Task . WhenAll ( disposeSessionTasks ) ;
43+ await sessions . DisposeAllSessionsAsync ( ) ;
11244 }
11345 finally
11446 {
@@ -123,39 +55,6 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
12355 }
12456 }
12557
126- private void RemoveAndCloseSession ( string sessionId )
127- {
128- if ( ! handler . Sessions . TryRemove ( sessionId , out var session ) )
129- {
130- return ;
131- }
132-
133- LogSessionIdle ( session . Id ) ;
134- // Don't slow down the idle tracking loop. DisposeSessionAsync logs. We only await during graceful shutdown.
135- _ = DisposeSessionAsync ( session ) ;
136- }
137-
138- private async Task DisposeSessionAsync ( HttpMcpSession < StreamableHttpServerTransport > session )
139- {
140- try
141- {
142- await session . DisposeAsync ( ) ;
143- }
144- catch ( Exception ex )
145- {
146- LogSessionDisposeError ( session . Id , ex ) ;
147- }
148- }
149-
150- [ LoggerMessage ( Level = LogLevel . Information , Message = "Closing idle session {sessionId}." ) ]
151- private partial void LogSessionIdle ( string sessionId ) ;
152-
153- [ LoggerMessage ( Level = LogLevel . Error , Message = "Error disposing session {sessionId}." ) ]
154- private partial void LogSessionDisposeError ( string sessionId , Exception ex ) ;
155-
156- [ LoggerMessage ( Level = LogLevel . Critical , Message = "Exceeded maximum of {maxIdleSessionCount} idle sessions. Now closing sessions active more recently than configured IdleTimeout." ) ]
157- private partial void LogMaxSessionIdleCountExceeded ( int maxIdleSessionCount ) ;
158-
15958 [ LoggerMessage ( Level = LogLevel . Critical , Message = "The IdleTrackingBackgroundService has stopped unexpectedly." ) ]
16059 private partial void IdleTrackingBackgroundServiceStoppedUnexpectedly ( ) ;
16160}
0 commit comments