Skip to content

Commit c20a5aa

Browse files
committed
do not use Task.Run from PlaceholderSpecializationMiddleware
1 parent c2825fc commit c20a5aa

File tree

3 files changed

+123
-6
lines changed

3 files changed

+123
-6
lines changed

src/WebJobs.Script.WebHost/Middleware/PlaceholderSpecializationMiddleware.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ private async Task InvokeSpecializationCheck(HttpContext httpContext)
4141
Task specializeTask;
4242
using (System.Threading.ExecutionContext.SuppressFlow())
4343
{
44-
// We need this to go async immediately, so use Task.Run.
45-
specializeTask = Task.Run(_standbyManager.SpecializeHostAsync);
44+
specializeTask = _standbyManager.SpecializeHostAsync();
4645
}
4746
await specializeTask;
4847

src/WebJobs.Script.WebHost/Standby/StandbyManager.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ public Task SpecializeHostAsync()
6969

7070
public async Task SpecializeHostCoreAsync()
7171
{
72+
// Go async immediately to ensure that any async context from
73+
// the PlaceholderSpecializationMiddleware is properly suppressed.
74+
await Task.Yield();
75+
7276
_logger.LogInformation(Resources.HostSpecializationTrace);
7377

7478
// After specialization, we need to ensure that custom timezone
Lines changed: 118 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.IO;
77
using System.Linq;
8+
using System.Net;
89
using System.Net.Http;
910
using System.Threading;
1011
using System.Threading.Tasks;
@@ -19,17 +20,20 @@
1920
using Microsoft.Azure.WebJobs.Script.WebHost;
2021
using Microsoft.Extensions.Configuration;
2122
using Microsoft.Extensions.DependencyInjection;
23+
using Microsoft.Extensions.Hosting;
2224
using Microsoft.Extensions.Logging;
2325
using Microsoft.Extensions.Options;
2426
using Microsoft.WebJobs.Script.Tests;
2527
using Xunit;
2628

27-
namespace Microsoft.Azure.WebJobs.Script.Tests.ApplicationInsights
29+
namespace Microsoft.Azure.WebJobs.Script.Tests
2830
{
29-
public class ApplicationInsightsSpecializationTests
31+
public class SpecializationE2ETests
3032
{
33+
private static SemaphoreSlim _pause = new SemaphoreSlim(1, 1);
34+
3135
[Fact]
32-
public async Task InvocationsContainDifferentOperationIds()
36+
public async Task ApplicationInsights_InvocationsContainDifferentOperationIds()
3337
{
3438
// Verify that when a request specializes the host we don't capture the context
3539
// of that request. Application Insights uses this context to correlate telemetry
@@ -159,6 +163,95 @@ await TestHelpers.Await(() =>
159163
}
160164
}
161165

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+
162255
private class InfiniteTimerStandbyManager : StandbyManager
163256
{
164257
public InfiniteTimerStandbyManager(IScriptHostManager scriptHostManager, IWebHostLanguageWorkerChannelManager languageWorkerChannelManager,
@@ -167,7 +260,28 @@ public InfiniteTimerStandbyManager(IScriptHostManager scriptHostManager, IWebHos
167260
: base(scriptHostManager, languageWorkerChannelManager, configuration, webHostEnvironment, environment, options,
168261
logger, hostNameProvider, TimeSpan.FromMilliseconds(-1))
169262
{
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+
}
171285
}
172286
}
173287
}

0 commit comments

Comments
 (0)