Skip to content

Commit b9faed1

Browse files
committed
Fix Http trigger disposal issue (#1537)
1 parent cdcba93 commit b9faed1

File tree

10 files changed

+120
-146
lines changed

10 files changed

+120
-146
lines changed

src/WebJobs.Script.WebHost/Controllers/FunctionsController.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
using System.Web.Http.Controllers;
1212
using System.Web.Http.Dependencies;
1313
using Microsoft.Azure.WebJobs.Extensions.Http;
14-
using Microsoft.Azure.WebJobs.Script.Binding;
1514
using Microsoft.Azure.WebJobs.Script.Description;
1615
using Microsoft.Azure.WebJobs.Script.WebHost.Filters;
1716
using Microsoft.Azure.WebJobs.Script.WebHost.WebHooks;

src/WebJobs.Script.WebHost/Handlers/WebScriptHostHandler.cs

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Linq;
65
using System.Net;
76
using System.Net.Http;
87
using System.Threading;
@@ -13,19 +12,22 @@ namespace Microsoft.Azure.WebJobs.Script.WebHost.Handlers
1312
{
1413
public class WebScriptHostHandler : DelegatingHandler
1514
{
16-
private readonly TimeSpan _hostTimeoutSeconds;
17-
private readonly int _hostRunningPollIntervalMs;
15+
public const int HostTimeoutSeconds = 30;
16+
public const int HostPollingIntervalMilliseconds = 500;
17+
18+
private readonly int _hostTimeoutSeconds;
19+
private readonly int _hostRunningPollIntervalMilliseconds;
1820
private readonly HttpConfiguration _config;
1921

20-
public WebScriptHostHandler(HttpConfiguration config, int hostTimeoutSeconds = 30, int hostRunningPollIntervalMS = 500)
22+
public WebScriptHostHandler(HttpConfiguration config, int hostTimeoutSeconds = HostTimeoutSeconds, int hostPollingIntervalMilliseconds = HostPollingIntervalMilliseconds)
2123
{
2224
if (config == null)
2325
{
2426
throw new ArgumentNullException("config");
2527
}
2628

27-
_hostRunningPollIntervalMs = hostRunningPollIntervalMS;
28-
_hostTimeoutSeconds = new TimeSpan(0, 0, hostTimeoutSeconds);
29+
_hostRunningPollIntervalMilliseconds = hostPollingIntervalMilliseconds;
30+
_hostTimeoutSeconds = hostTimeoutSeconds;
2931
_config = config;
3032
}
3133

@@ -55,23 +57,7 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
5557
// If the host is not running, we'll wait a bit for it to fully
5658
// initialize. This might happen if http requests come in while the
5759
// host is starting up for the first time, or if it is restarting.
58-
TimeSpan timeWaited = TimeSpan.Zero;
59-
while (!scriptHostManager.CanInvoke() &&
60-
scriptHostManager.State != ScriptHostState.Error &&
61-
(timeWaited < _hostTimeoutSeconds))
62-
{
63-
await Task.Delay(_hostRunningPollIntervalMs);
64-
timeWaited += TimeSpan.FromMilliseconds(_hostRunningPollIntervalMs);
65-
}
66-
67-
// if the host is not ready after our wait time has expired return a 503
68-
if (!scriptHostManager.CanInvoke())
69-
{
70-
return new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)
71-
{
72-
Content = new StringContent("Function host is not running.")
73-
};
74-
}
60+
await scriptHostManager.DelayUntilHostReady(_hostTimeoutSeconds, _hostRunningPollIntervalMilliseconds);
7561
}
7662

7763
return await base.SendAsync(request, cancellationToken);

src/WebJobs.Script.WebHost/WebScriptHostManager.cs

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
using Microsoft.Azure.WebJobs.Script.Diagnostics;
2727
using Microsoft.Azure.WebJobs.Script.Eventing;
2828
using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics;
29+
using Microsoft.Azure.WebJobs.Script.WebHost.Handlers;
2930
using Microsoft.Extensions.Logging;
3031

3132
namespace Microsoft.Azure.WebJobs.Script.WebHost
@@ -42,6 +43,8 @@ public class WebScriptHostManager : ScriptHostManager
4243
private readonly ScriptHostConfiguration _config;
4344
private readonly ISwaggerDocumentManager _swaggerDocumentManager;
4445
private readonly object _syncLock = new object();
46+
private readonly int _hostTimeoutSeconds;
47+
private readonly int _hostRunningPollIntervalMilliseconds;
4548

4649
private readonly WebJobsSdkExtensionHookProvider _bindingWebHookProvider = new WebJobsSdkExtensionHookProvider();
4750

@@ -57,13 +60,17 @@ public WebScriptHostManager(ScriptHostConfiguration config,
5760
ScriptSettingsManager settingsManager,
5861
WebHostSettings webHostSettings,
5962
IScriptHostFactory scriptHostFactory = null,
60-
ISecretsRepositoryFactory secretsRepositoryFactory = null)
63+
ISecretsRepositoryFactory secretsRepositoryFactory = null,
64+
int hostTimeoutSeconds = WebScriptHostHandler.HostTimeoutSeconds,
65+
int hostPollingIntervalMilliseconds = WebScriptHostHandler.HostPollingIntervalMilliseconds)
6166
: base(config, settingsManager, scriptHostFactory, eventManager)
6267
{
6368
_config = config;
6469
_metricsLogger = new WebHostMetricsLogger();
6570
_exceptionHandler = new WebScriptHostExceptionHandler(this);
6671
_webHostSettings = webHostSettings;
72+
_hostTimeoutSeconds = hostTimeoutSeconds;
73+
_hostRunningPollIntervalMilliseconds = hostPollingIntervalMilliseconds;
6774

6875
var systemEventGenerator = config.HostConfig.GetService<IEventGenerator>() ?? new EventGenerator();
6976
var systemTraceWriter = new SystemTraceWriter(systemEventGenerator, settingsManager, TraceLevel.Verbose);
@@ -160,10 +167,12 @@ public IReadOnlyDictionary<IHttpRoute, FunctionDescriptor> HttpFunctions
160167

161168
public async Task<HttpResponseMessage> HandleRequestAsync(FunctionDescriptor function, HttpRequestMessage request, CancellationToken cancellationToken)
162169
{
170+
// ensure that the host is ready to process requests
171+
await DelayUntilHostReady(_hostTimeoutSeconds, _hostRunningPollIntervalMilliseconds);
172+
163173
// All authentication is assumed to have been done on the request
164174
// BEFORE this method is called
165-
166-
Dictionary<string, object> arguments = GetFunctionArguments(function, request);
175+
var arguments = GetFunctionArguments(function, request);
167176

168177
// Suspend the current synchronization context so we don't pass the ASP.NET
169178
// context down to the function.
@@ -173,11 +182,11 @@ public async Task<HttpResponseMessage> HandleRequestAsync(FunctionDescriptor fun
173182
// record details about the request.
174183
ILoggerFactory loggerFactory = _config.HostConfig.GetService<ILoggerFactory>();
175184
ILogger logger = loggerFactory.CreateLogger(LogCategories.Function);
176-
using (logger.BeginScope(
177-
new Dictionary<string, object>()
178-
{
179-
[ScriptConstants.LoggerHttpRequest] = request
180-
}))
185+
var scopeState = new Dictionary<string, object>()
186+
{
187+
[ScriptConstants.LoggerHttpRequest] = request
188+
};
189+
using (logger.BeginScope(scopeState))
181190
{
182191
await Instance.CallAsync(function.Name, arguments, cancellationToken);
183192
}
@@ -515,5 +524,29 @@ public override void Shutdown()
515524
Stop();
516525
HostingEnvironment.InitiateShutdown();
517526
}
527+
528+
public async Task DelayUntilHostReady(int timeoutSeconds = WebScriptHostHandler.HostTimeoutSeconds, int pollingIntervalMilliseconds = WebScriptHostHandler.HostPollingIntervalMilliseconds, bool throwOnFailure = true)
529+
{
530+
TimeSpan timeout = TimeSpan.FromSeconds(timeoutSeconds);
531+
TimeSpan delay = TimeSpan.FromMilliseconds(pollingIntervalMilliseconds);
532+
TimeSpan timeWaited = TimeSpan.Zero;
533+
534+
while (!CanInvoke() &&
535+
State != ScriptHostState.Error &&
536+
(timeWaited < timeout))
537+
{
538+
await Task.Delay(delay);
539+
timeWaited += delay;
540+
}
541+
542+
if (throwOnFailure && !CanInvoke())
543+
{
544+
var errorResponse = new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)
545+
{
546+
Content = new StringContent("Function host is not running.")
547+
};
548+
throw new HttpResponseException(errorResponse);
549+
}
550+
}
518551
}
519552
}

src/WebJobs.Script/Host/ScriptHostManager.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
using Microsoft.Azure.WebJobs.Script.Config;
1616
using Microsoft.Azure.WebJobs.Script.Diagnostics;
1717
using Microsoft.Azure.WebJobs.Script.Eventing;
18-
using Microsoft.Azure.WebJobs.Script.Eventing.File;
19-
using Microsoft.Azure.WebJobs.Script.IO;
2018
using Microsoft.Extensions.Logging;
2119

2220
namespace Microsoft.Azure.WebJobs.Script

test/WebJobs.Script.Tests.Integration/Host/WebScriptHostManagerTests.cs

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
using System.Diagnostics;
77
using System.IO;
88
using System.Linq;
9+
using System.Net;
910
using System.Net.Http;
1011
using System.Text.RegularExpressions;
12+
using System.Threading;
1113
using System.Threading.Tasks;
1214
using System.Web.Http;
1315
using System.Web.Http.Routing;
@@ -28,12 +30,40 @@ public class WebScriptHostManagerTests : IClassFixture<WebScriptHostManagerTests
2830
{
2931
private readonly ScriptSettingsManager _settingsManager;
3032
private readonly TempDirectory _secretsDirectory = new TempDirectory();
33+
private readonly WebScriptHostManager _hostManager;
34+
private readonly Mock<IScriptHostFactory> _mockScriptHostFactory;
35+
private ScriptHostConfiguration _config;
3136
private WebScriptHostManagerTests.Fixture _fixture;
3237

3338
public WebScriptHostManagerTests(WebScriptHostManagerTests.Fixture fixture)
3439
{
3540
_fixture = fixture;
3641
_settingsManager = ScriptSettingsManager.Instance;
42+
43+
string functionTestDir = Path.Combine(_fixture.TestFunctionRoot, Guid.NewGuid().ToString());
44+
Directory.CreateDirectory(functionTestDir);
45+
string logDir = Path.Combine(_fixture.TestLogsRoot, Guid.NewGuid().ToString());
46+
string secretsDir = Path.Combine(_fixture.TestSecretsRoot, Guid.NewGuid().ToString());
47+
48+
_config = new ScriptHostConfiguration
49+
{
50+
RootLogPath = logDir,
51+
RootScriptPath = functionTestDir,
52+
FileLoggingMode = FileLoggingMode.Always,
53+
};
54+
55+
var secretsRepository = new FileSystemSecretsRepository(_secretsDirectory.Path);
56+
SecretManager secretManager = new SecretManager(_settingsManager, secretsRepository, NullTraceWriter.Instance, null);
57+
WebHostSettings webHostSettings = new WebHostSettings
58+
{
59+
SecretsPath = _secretsDirectory.Path
60+
};
61+
62+
var mockEventManager = new Mock<IScriptEventManager>();
63+
_mockScriptHostFactory = new Mock<IScriptHostFactory>();
64+
65+
_hostManager = new WebScriptHostManager(_config, new TestSecretManagerFactory(secretManager), mockEventManager.Object,
66+
_settingsManager, webHostSettings, _mockScriptHostFactory.Object, new DefaultSecretsRepositoryFactory(), 2, 500);
3767
}
3868

3969
[Fact]
@@ -142,48 +172,41 @@ public async Task EmptyHost_StartsSuccessfully()
142172
[Fact]
143173
public async Task MultipleHostRestarts()
144174
{
145-
string functionTestDir = Path.Combine(_fixture.TestFunctionRoot, Guid.NewGuid().ToString());
146-
Directory.CreateDirectory(functionTestDir);
147-
string logDir = Path.Combine(_fixture.TestLogsRoot, Guid.NewGuid().ToString());
148-
string secretsDir = Path.Combine(_fixture.TestSecretsRoot, Guid.NewGuid().ToString());
149-
150-
ScriptHostConfiguration config = new ScriptHostConfiguration
151-
{
152-
RootLogPath = logDir,
153-
RootScriptPath = functionTestDir,
154-
FileLoggingMode = FileLoggingMode.Always,
155-
};
156-
157-
ISecretsRepository repository = new FileSystemSecretsRepository(_secretsDirectory.Path);
158-
SecretManager secretManager = new SecretManager(_settingsManager, repository, NullTraceWriter.Instance, null);
159-
WebHostSettings webHostSettings = new WebHostSettings();
160-
webHostSettings.SecretsPath = _secretsDirectory.Path;
161-
162-
var mockEventManager = new Mock<IScriptEventManager>();
163-
var factoryMock = new Mock<IScriptHostFactory>();
164175
int count = 0;
165-
factoryMock.Setup(p => p.Create(It.IsAny<IScriptHostEnvironment>(), It.IsAny<IScriptEventManager>(), _settingsManager, config)).Callback(() =>
176+
_mockScriptHostFactory.Setup(p => p.Create(It.IsAny<IScriptHostEnvironment>(), It.IsAny<IScriptEventManager>(), _settingsManager, _config)).Callback(() =>
166177
{
167178
count++;
168179
}).Throws(new Exception("Kaboom!"));
169180

170-
ScriptHostManager hostManager = new WebScriptHostManager(config, new TestSecretManagerFactory(secretManager), mockEventManager.Object,
171-
_settingsManager, webHostSettings, factoryMock.Object);
172-
173-
Task runTask = Task.Run(() => hostManager.RunAndBlock());
181+
Task runTask = Task.Run(() => _hostManager.RunAndBlock());
174182

175183
await TestHelpers.Await(() =>
176184
{
177185
return count > 3;
178186
});
179187

180-
hostManager.Stop();
181-
Assert.Equal(ScriptHostState.Default, hostManager.State);
188+
_hostManager.Stop();
189+
Assert.Equal(ScriptHostState.Default, _hostManager.State);
182190

183191
// regression test: previously on multiple restarts we were recomposing
184192
// the writer on each restart, resulting in a nested chain of writers
185193
// increasing on each restart
186-
Assert.Equal(typeof(SystemTraceWriter), config.TraceWriter.GetType());
194+
Assert.Equal(typeof(SystemTraceWriter), _config.TraceWriter.GetType());
195+
}
196+
197+
[Fact]
198+
public async Task HandleRequestAsync_HostNotReady_Throws()
199+
{
200+
Assert.False(_hostManager.CanInvoke());
201+
202+
var ex = await Assert.ThrowsAsync<HttpResponseException>(async () =>
203+
{
204+
await _hostManager.HandleRequestAsync(null, new HttpRequestMessage(), CancellationToken.None);
205+
});
206+
207+
Assert.Equal(HttpStatusCode.ServiceUnavailable, ex.Response.StatusCode);
208+
var message = await ex.Response.Content.ReadAsStringAsync();
209+
Assert.Equal("Function host is not running.", message);
187210
}
188211

189212
[Fact]

test/WebJobs.Script.Tests.Integration/SamplesEndToEndRestartTests.cs

Lines changed: 0 additions & 79 deletions
This file was deleted.

test/WebJobs.Script.Tests.Integration/SamplesEndToEndTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ namespace Microsoft.Azure.WebJobs.Script.Tests
3434
{
3535
public class SamplesEndToEndTests : IClassFixture<SamplesEndToEndTests.TestFixture>
3636
{
37-
private const string MasterKey = "t8laajal0a1ajkgzoqlfv5gxr4ebhqozebw4qzdy";
37+
internal const string MasterKey = "t8laajal0a1ajkgzoqlfv5gxr4ebhqozebw4qzdy";
3838

3939
private readonly ScriptSettingsManager _settingsManager;
4040
private TestFixture _fixture;

test/WebJobs.Script.Tests.Integration/WebJobs.Script.Tests.Integration.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,6 @@
431431
<DesignTime>True</DesignTime>
432432
<DependentUpon>Resources.resx</DependentUpon>
433433
</Compile>
434-
<Compile Include="SamplesEndToEndRestartTests.cs" />
435434
<Compile Include="SamplesEndToEndTests.cs" />
436435
<Compile Include="Host\ScriptHostManagerTests.cs" />
437436
<Compile Include="Host\StandbyModeTests.cs" />

0 commit comments

Comments
 (0)