5
5
using System . Collections . Generic ;
6
6
using System . IO ;
7
7
using System . Linq ;
8
+ using System . Net ;
8
9
using System . Net . Http ;
9
10
using System . Threading ;
10
11
using System . Threading . Tasks ;
19
20
using Microsoft . Azure . WebJobs . Script . WebHost ;
20
21
using Microsoft . Extensions . Configuration ;
21
22
using Microsoft . Extensions . DependencyInjection ;
23
+ using Microsoft . Extensions . Hosting ;
22
24
using Microsoft . Extensions . Logging ;
23
25
using Microsoft . Extensions . Options ;
24
26
using Microsoft . WebJobs . Script . Tests ;
25
27
using Xunit ;
26
28
27
- namespace Microsoft . Azure . WebJobs . Script . Tests . ApplicationInsights
29
+ namespace Microsoft . Azure . WebJobs . Script . Tests
28
30
{
29
- public class ApplicationInsightsSpecializationTests
31
+ public class SpecializationE2ETests
30
32
{
33
+ private static SemaphoreSlim _pause = new SemaphoreSlim ( 1 , 1 ) ;
34
+
31
35
[ Fact ]
32
- public async Task InvocationsContainDifferentOperationIds ( )
36
+ public async Task ApplicationInsights_InvocationsContainDifferentOperationIds ( )
33
37
{
34
38
// Verify that when a request specializes the host we don't capture the context
35
39
// of that request. Application Insights uses this context to correlate telemetry
@@ -159,6 +163,95 @@ await TestHelpers.Await(() =>
159
163
}
160
164
}
161
165
166
+ [ Fact ]
167
+ public async Task Specialization_ThreadUtilization ( )
168
+ {
169
+ // Start a host in standby mode.
170
+ StandbyManager . ResetChangeToken ( ) ;
171
+
172
+ string standbyPath = Path . Combine ( Path . GetTempPath ( ) , "functions" , "standby" , "wwwroot" ) ;
173
+ string specializedScriptRoot = @"TestScripts\CSharp" ;
174
+ string scriptRootConfigPath = ConfigurationPath . Combine ( ConfigurationSectionNames . WebHost , nameof ( ScriptApplicationHostOptions . ScriptPath ) ) ;
175
+
176
+ var settings = new Dictionary < string , string > ( )
177
+ {
178
+ { EnvironmentSettingNames . AzureWebsitePlaceholderMode , "1" } ,
179
+ { EnvironmentSettingNames . AzureWebsiteContainerReady , null } ,
180
+ } ;
181
+
182
+ var environment = new TestEnvironment ( settings ) ;
183
+ var loggerProvider = new TestLoggerProvider ( ) ;
184
+
185
+ var builder = Program . CreateWebHostBuilder ( )
186
+ . ConfigureLogging ( b =>
187
+ {
188
+ b . AddProvider ( loggerProvider ) ;
189
+ } )
190
+ . ConfigureAppConfiguration ( c =>
191
+ {
192
+ c . AddInMemoryCollection ( new Dictionary < string , string >
193
+ {
194
+ { scriptRootConfigPath , specializedScriptRoot }
195
+ } ) ;
196
+ } )
197
+ . ConfigureServices ( ( bc , s ) =>
198
+ {
199
+ s . AddSingleton < IEnvironment > ( environment ) ;
200
+
201
+ // Ensure that we don't have a race between the timer and the
202
+ // request for triggering specialization.
203
+ s . AddSingleton < IStandbyManager , InfiniteTimerStandbyManager > ( ) ;
204
+
205
+ s . AddSingleton < IScriptHostBuilder , PausingScriptHostBuilder > ( ) ;
206
+ } )
207
+ . AddScriptHostBuilder ( webJobsBuilder =>
208
+ {
209
+ webJobsBuilder . Services . PostConfigure < ScriptJobHostOptions > ( o =>
210
+ {
211
+ // Only load the function we care about, but not during standby
212
+ if ( o . RootScriptPath != standbyPath )
213
+ {
214
+ o . Functions = new [ ]
215
+ {
216
+ "FunctionExecutionContext"
217
+ } ;
218
+ }
219
+ } ) ;
220
+ } ) ;
221
+
222
+ using ( var testServer = new TestServer ( builder ) )
223
+ {
224
+ var client = testServer . CreateClient ( ) ;
225
+
226
+ var response = await client . GetAsync ( "api/warmup" ) ;
227
+ response . EnsureSuccessStatusCode ( ) ;
228
+
229
+ await _pause . WaitAsync ( ) ;
230
+
231
+ List < Task < HttpResponseMessage > > requestTasks = new List < Task < HttpResponseMessage > > ( ) ;
232
+
233
+ environment . SetEnvironmentVariable ( EnvironmentSettingNames . AzureWebsiteContainerReady , "1" ) ;
234
+ environment . SetEnvironmentVariable ( EnvironmentSettingNames . AzureWebsitePlaceholderMode , "0" ) ;
235
+
236
+ for ( int i = 0 ; i < 100 ; i ++ )
237
+ {
238
+ requestTasks . Add ( client . GetAsync ( "api/functionexecutioncontext" ) ) ;
239
+ }
240
+
241
+ ThreadPool . GetAvailableThreads ( out int originalWorkerThreads , out int originalcompletionThreads ) ;
242
+ Thread . Sleep ( 5000 ) ;
243
+ ThreadPool . GetAvailableThreads ( out int workerThreads , out int completionThreads ) ;
244
+
245
+ _pause . Release ( ) ;
246
+
247
+ Assert . True ( workerThreads >= originalWorkerThreads , $ "Available ThreadPool threads should not have decreased. Actual: { workerThreads } . Original: { originalWorkerThreads } .") ;
248
+
249
+ await Task . WhenAll ( requestTasks ) ;
250
+
251
+ Assert . True ( requestTasks . All ( t => t . Result . StatusCode == HttpStatusCode . OK ) ) ;
252
+ }
253
+ }
254
+
162
255
private class InfiniteTimerStandbyManager : StandbyManager
163
256
{
164
257
public InfiniteTimerStandbyManager ( IScriptHostManager scriptHostManager , IWebHostLanguageWorkerChannelManager languageWorkerChannelManager ,
@@ -167,7 +260,28 @@ public InfiniteTimerStandbyManager(IScriptHostManager scriptHostManager, IWebHos
167
260
: base ( scriptHostManager , languageWorkerChannelManager , configuration , webHostEnvironment , environment , options ,
168
261
logger , hostNameProvider , TimeSpan . FromMilliseconds ( - 1 ) )
169
262
{
170
- }
263
+ }
264
+ }
265
+
266
+ private class PausingScriptHostBuilder : IScriptHostBuilder
267
+ {
268
+ private readonly DefaultScriptHostBuilder _inner ;
269
+
270
+ public PausingScriptHostBuilder ( IOptionsMonitor < ScriptApplicationHostOptions > options , IServiceProvider root , IServiceScopeFactory scope )
271
+ {
272
+ _inner = new DefaultScriptHostBuilder ( options , root , scope ) ;
273
+ }
274
+
275
+ public IHost BuildHost ( bool skipHostStartup , bool skipHostConfigurationParsing )
276
+ {
277
+ _pause . WaitAsync ( ) . GetAwaiter ( ) . GetResult ( ) ;
278
+
279
+ IHost host = _inner . BuildHost ( skipHostStartup , skipHostConfigurationParsing ) ;
280
+
281
+ _pause . Release ( ) ;
282
+
283
+ return host ;
284
+ }
171
285
}
172
286
}
173
287
}
0 commit comments