Skip to content

Commit 8c6de57

Browse files
committed
suppress ExecutionContext during host specialization request
1 parent 3699fc1 commit 8c6de57

File tree

7 files changed

+210
-3
lines changed

7 files changed

+210
-3
lines changed

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,16 @@ private async Task InvokeSpecializationCheck(HttpContext httpContext)
3535
{
3636
if (!_webHostEnvironment.InStandbyMode && _environment.IsContainerReady())
3737
{
38-
await _standbyManager.SpecializeHostAsync();
38+
// We don't want AsyncLocal context (like Activity.Current) to flow
39+
// here as it will contain request details. Suppressing this context
40+
// prevents the request context from being captured by the host.
41+
Task specializeTask;
42+
using (System.Threading.ExecutionContext.SuppressFlow())
43+
{
44+
// We need this to go async immediately, so use Task.Run.
45+
specializeTask = Task.Run(_standbyManager.SpecializeHostAsync);
46+
}
47+
await specializeTask;
3948

4049
if (Interlocked.CompareExchange(ref _specialized, 1, 0) == 0)
4150
{

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public class StandbyManager : IStandbyManager, IDisposable
3131
private readonly ILogger _logger;
3232
private readonly HostNameProvider _hostNameProvider;
3333
private readonly IDisposable _changeTokenCallbackSubscription;
34-
private readonly TimeSpan _specializationTimerInterval = TimeSpan.FromMilliseconds(500);
34+
private readonly TimeSpan _specializationTimerInterval;
3535

3636
private Timer _specializationTimer;
3737
private static CancellationTokenSource _standbyCancellationTokenSource = new CancellationTokenSource();
@@ -40,6 +40,12 @@ public class StandbyManager : IStandbyManager, IDisposable
4040

4141
public StandbyManager(IScriptHostManager scriptHostManager, ILanguageWorkerChannelManager languageWorkerChannelManager, IConfiguration configuration, IScriptWebHostEnvironment webHostEnvironment,
4242
IEnvironment environment, IOptionsMonitor<ScriptApplicationHostOptions> options, ILogger<StandbyManager> logger, HostNameProvider hostNameProvider)
43+
: this(scriptHostManager, languageWorkerChannelManager, configuration, webHostEnvironment, environment, options, logger, hostNameProvider, TimeSpan.FromMilliseconds(500))
44+
{
45+
}
46+
47+
public StandbyManager(IScriptHostManager scriptHostManager, ILanguageWorkerChannelManager languageWorkerChannelManager, IConfiguration configuration, IScriptWebHostEnvironment webHostEnvironment,
48+
IEnvironment environment, IOptionsMonitor<ScriptApplicationHostOptions> options, ILogger<StandbyManager> logger, HostNameProvider hostNameProvider, TimeSpan specializationTimerInterval)
4349
{
4450
_scriptHostManager = scriptHostManager ?? throw new ArgumentNullException(nameof(scriptHostManager));
4551
_options = options ?? throw new ArgumentNullException(nameof(options));
@@ -51,6 +57,7 @@ public StandbyManager(IScriptHostManager scriptHostManager, ILanguageWorkerChann
5157
_languageWorkerChannelManager = languageWorkerChannelManager ?? throw new ArgumentNullException(nameof(languageWorkerChannelManager));
5258
_hostNameProvider = hostNameProvider ?? throw new ArgumentNullException(nameof(hostNameProvider));
5359
_changeTokenCallbackSubscription = ChangeToken.RegisterChangeCallback(_ => _logger.LogDebug($"{nameof(StandbyManager)}.{nameof(ChangeToken)} callback has fired."), null);
60+
_specializationTimerInterval = specializationTimerInterval;
5461
}
5562

5663
public static IChangeToken ChangeToken => _standbyChangeToken;

src/WebJobs.Script.WebHost/WebHostServiceCollectionExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public static void AddWebJobsScriptHost(this IServiceCollection services, IConfi
7070

7171
services.AddSingleton<IScriptHostManager>(s => s.GetRequiredService<WebJobsScriptHostService>());
7272
services.AddSingleton<IScriptWebHostEnvironment, ScriptWebHostEnvironment>();
73-
services.AddSingleton<IStandbyManager, StandbyManager>();
73+
services.TryAddSingleton<IStandbyManager, StandbyManager>();
7474
services.TryAddSingleton<IScriptHostBuilder, DefaultScriptHostBuilder>();
7575
services.AddSingleton<IMetricsLogger, WebHostMetricsLogger>();
7676

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Net.Http;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using Microsoft.ApplicationInsights.Channel;
12+
using Microsoft.ApplicationInsights.DataContracts;
13+
using Microsoft.AspNetCore.Hosting;
14+
using Microsoft.AspNetCore.TestHost;
15+
using Microsoft.Azure.WebJobs.Logging;
16+
using Microsoft.Azure.WebJobs.Logging.ApplicationInsights;
17+
using Microsoft.Azure.WebJobs.Script.Configuration;
18+
using Microsoft.Azure.WebJobs.Script.Rpc;
19+
using Microsoft.Azure.WebJobs.Script.WebHost;
20+
using Microsoft.Extensions.Configuration;
21+
using Microsoft.Extensions.DependencyInjection;
22+
using Microsoft.Extensions.Logging;
23+
using Microsoft.Extensions.Options;
24+
using Microsoft.WebJobs.Script.Tests;
25+
using Xunit;
26+
27+
namespace Microsoft.Azure.WebJobs.Script.Tests.ApplicationInsights
28+
{
29+
public class ApplicationInsightsSpecializationTests
30+
{
31+
[Fact]
32+
public async Task InvocationsContainDifferentOperationIds()
33+
{
34+
// Verify that when a request specializes the host we don't capture the context
35+
// of that request. Application Insights uses this context to correlate telemetry
36+
// so it had a confusing effect. Previously all TimerTrigger traces would have the
37+
// operation id of this request and all host logs would as well.
38+
39+
// Start a host in standby mode.
40+
StandbyManager.ResetChangeToken();
41+
42+
string standbyPath = Path.Combine(Path.GetTempPath(), "functions", "standby", "wwwroot");
43+
string specializedScriptRoot = @"TestScripts\CSharp";
44+
string scriptRootConfigPath = ConfigurationPath.Combine(ConfigurationSectionNames.WebHost, nameof(ScriptApplicationHostOptions.ScriptPath));
45+
46+
var settings = new Dictionary<string, string>()
47+
{
48+
{ EnvironmentSettingNames.AzureWebsitePlaceholderMode, "1" },
49+
{ EnvironmentSettingNames.AzureWebsiteContainerReady, null },
50+
};
51+
52+
var environment = new TestEnvironment(settings);
53+
var loggerProvider = new TestLoggerProvider();
54+
var channel = new TestTelemetryChannel();
55+
56+
var builder = Program.CreateWebHostBuilder()
57+
.ConfigureLogging(b =>
58+
{
59+
b.AddProvider(loggerProvider);
60+
})
61+
.ConfigureAppConfiguration(c =>
62+
{
63+
c.AddInMemoryCollection(new Dictionary<string, string>
64+
{
65+
{ scriptRootConfigPath, specializedScriptRoot }
66+
});
67+
})
68+
.ConfigureServices((bc, s) =>
69+
{
70+
s.AddSingleton<IEnvironment>(environment);
71+
72+
// Ensure that we don't have a race between the timer and the
73+
// request for triggering specialization.
74+
s.AddSingleton<IStandbyManager, InfiniteTimerStandbyManager>();
75+
})
76+
.AddScriptHostBuilder(webJobsBuilder =>
77+
{
78+
webJobsBuilder.Services.AddSingleton<ITelemetryChannel>(_ => channel);
79+
80+
webJobsBuilder.Services.Configure<FunctionResultAggregatorOptions>(o =>
81+
{
82+
o.IsEnabled = false;
83+
});
84+
85+
webJobsBuilder.Services.PostConfigure<ApplicationInsightsLoggerOptions>(o =>
86+
{
87+
o.SamplingSettings = null;
88+
});
89+
90+
webJobsBuilder.Services.PostConfigure<ScriptJobHostOptions>(o =>
91+
{
92+
// Only load the function we care about, but not during standby
93+
if (o.RootScriptPath != standbyPath)
94+
{
95+
o.Functions = new[]
96+
{
97+
"OneSecondTimer",
98+
"FunctionExecutionContext"
99+
};
100+
}
101+
});
102+
})
103+
.ConfigureScriptHostAppConfiguration(c =>
104+
{
105+
c.AddInMemoryCollection(new Dictionary<string, string>
106+
{
107+
[EnvironmentSettingNames.AppInsightsInstrumentationKey] = "some_key"
108+
});
109+
});
110+
111+
using (var testServer = new TestServer(builder))
112+
{
113+
var client = testServer.CreateClient();
114+
115+
HttpResponseMessage response = await client.GetAsync("api/warmup");
116+
Assert.True(response.IsSuccessStatusCode, loggerProvider.GetLog());
117+
118+
// Now that standby mode is warmed up, set the specialization properties...
119+
environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteContainerReady, "1");
120+
environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsitePlaceholderMode, "0");
121+
122+
// ...and issue a request which will force specialization.
123+
response = await client.GetAsync("api/functionexecutioncontext");
124+
Assert.True(response.IsSuccessStatusCode, loggerProvider.GetLog());
125+
126+
// Wait until we have a few logs from the timer trigger.
127+
IEnumerable<TraceTelemetry> timerLogs = null;
128+
await TestHelpers.Await(() =>
129+
{
130+
timerLogs = channel.Telemetries
131+
.OfType<TraceTelemetry>()
132+
.Where(p => p.Message == "OneSecondTimer fired!");
133+
134+
return timerLogs.Count() >= 3;
135+
});
136+
137+
var startupRequest = channel.Telemetries
138+
.OfType<RequestTelemetry>()
139+
.Where(p => p.Name == "FunctionExecutionContext")
140+
.Single();
141+
142+
// Make sure that auto-Http tracking worked with this request.
143+
Assert.Equal("200", startupRequest.ResponseCode);
144+
145+
// The host logs should not be associated with this request.
146+
var logsWithRequestId = channel.Telemetries
147+
.OfType<TraceTelemetry>()
148+
.Select(p => p.Context.Operation.Id)
149+
.Where(p => p == startupRequest.Context.Operation.Id);
150+
151+
// Just expect the "Executing" and "Executed" logs from the actual request.
152+
Assert.Equal(2, logsWithRequestId.Count());
153+
154+
// And each of the timer invocations should have a different operation id, and none
155+
// should match the request id.
156+
var distinctOpIds = timerLogs.Select(p => p.Context.Operation.Id).Distinct();
157+
Assert.Equal(timerLogs.Count(), distinctOpIds.Count());
158+
Assert.Empty(timerLogs.Where(p => p.Context.Operation.Id == startupRequest.Context.Operation.Id));
159+
}
160+
}
161+
162+
private class InfiniteTimerStandbyManager : StandbyManager
163+
{
164+
public InfiniteTimerStandbyManager(IScriptHostManager scriptHostManager, ILanguageWorkerChannelManager languageWorkerChannelManager,
165+
IConfiguration configuration, IScriptWebHostEnvironment webHostEnvironment, IEnvironment environment,
166+
IOptionsMonitor<ScriptApplicationHostOptions> options, ILogger<StandbyManager> logger, HostNameProvider hostNameProvider)
167+
: base(scriptHostManager, languageWorkerChannelManager, configuration, webHostEnvironment, environment, options,
168+
logger, hostNameProvider, TimeSpan.FromMilliseconds(-1))
169+
{
170+
}
171+
}
172+
}
173+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"bindings": [
3+
{
4+
"type": "timerTrigger",
5+
"name": "timerInfo",
6+
"schedule": "*/1 * * * * *",
7+
"direction": "in"
8+
}
9+
]
10+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
public static void Run(TimerInfo timerInfo, ILogger log)
2+
{
3+
log.LogInformation("OneSecondTimer fired!");
4+
}

test/WebJobs.Script.Tests.Shared/TestWebHostBuilderExtensions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.Azure.WebJobs;
66
using Microsoft.Azure.WebJobs.Script;
77
using Microsoft.Azure.WebJobs.Script.Tests;
8+
using Microsoft.Extensions.Configuration;
89
using Microsoft.Extensions.DependencyInjection;
910

1011
namespace Microsoft.AspNetCore.Hosting
@@ -13,5 +14,8 @@ public static class TestWebHostBuilderExtensions
1314
{
1415
public static IWebHostBuilder AddScriptHostBuilder(this IWebHostBuilder webHostBuilder, Action<IWebJobsBuilder> builder) =>
1516
webHostBuilder.ConfigureServices(s => s.AddSingleton<IConfigureBuilder<IWebJobsBuilder>>(_ => new DelegatedConfigureBuilder<IWebJobsBuilder>(builder)));
17+
18+
public static IWebHostBuilder ConfigureScriptHostAppConfiguration(this IWebHostBuilder webHostBuilder, Action<IConfigurationBuilder> builder) =>
19+
webHostBuilder.ConfigureServices(s => s.AddSingleton<IConfigureBuilder<IConfigurationBuilder>>(_ => new DelegatedConfigureBuilder<IConfigurationBuilder>(builder)));
1620
}
1721
}

0 commit comments

Comments
 (0)