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