1
- using System . Runtime . InteropServices ;
2
- using Microsoft . Extensions . Hosting ;
1
+ using Microsoft . Extensions . Hosting ;
3
2
using Microsoft . Extensions . Logging ;
4
3
using Microsoft . Extensions . Options ;
5
- using ModelContextProtocol . Server ;
6
4
7
5
namespace ModelContextProtocol . AspNetCore ;
8
6
9
7
internal sealed partial class IdleTrackingBackgroundService (
10
- StreamableHttpHandler handler ,
8
+ StatefulSessionManager sessions ,
11
9
IOptions < HttpServerTransportOptions > options ,
12
10
IHostApplicationLifetime appLifetime ,
13
11
ILogger < IdleTrackingBackgroundService > logger ) : BackgroundService
14
12
{
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 .
16
14
private readonly ILogger _logger = logger ;
17
15
18
16
protected override async Task ExecuteAsync ( CancellationToken stoppingToken )
@@ -30,65 +28,9 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
30
28
var timeProvider = options . Value . TimeProvider ;
31
29
using var timer = new PeriodicTimer ( TimeSpan . FromSeconds ( 5 ) , timeProvider ) ;
32
30
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
-
42
31
while ( ! stoppingToken . IsCancellationRequested && await timer . WaitForNextTickAsync ( stoppingToken ) )
43
32
{
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 ) ;
92
34
}
93
35
}
94
36
catch ( OperationCanceledException ) when ( stoppingToken . IsCancellationRequested )
@@ -98,17 +40,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
98
40
{
99
41
try
100
42
{
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 ( ) ;
112
44
}
113
45
finally
114
46
{
@@ -123,39 +55,6 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
123
55
}
124
56
}
125
57
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
-
159
58
[ LoggerMessage ( Level = LogLevel . Critical , Message = "The IdleTrackingBackgroundService has stopped unexpectedly." ) ]
160
59
private partial void IdleTrackingBackgroundServiceStoppedUnexpectedly ( ) ;
161
60
}
0 commit comments