Skip to content

Commit 5652e92

Browse files
authored
Fixing http throttle enablement (#6548) (#6867)
1 parent 038c748 commit 5652e92

File tree

8 files changed

+115
-15
lines changed

8 files changed

+115
-15
lines changed

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,17 +154,18 @@ public async Task<IActionResult> GetScaleStatus([FromBody] ScaleStatusContext co
154154

155155
// TEMP: Once https://github.com/Azure/azure-functions-host/issues/5161 is fixed, we should take
156156
// FunctionsScaleManager as a parameter.
157-
var scaleManager = (scriptHostManager as IServiceProvider)?.GetService<FunctionsScaleManager>();
158-
if (scaleManager == null)
157+
if (Utility.TryGetHostService(scriptHostManager, out FunctionsScaleManager scaleManager))
158+
{
159+
var scaleStatus = await scaleManager.GetScaleStatusAsync(context);
160+
return new ObjectResult(scaleStatus);
161+
}
162+
else
159163
{
160164
// This case should never happen. Because this action is marked RequiresRunningHost,
161165
// it's only invoked when the host is running, and if it's running, we'll have access
162166
// to the FunctionsScaleManager.
163167
return StatusCode(StatusCodes.Status503ServiceUnavailable);
164168
}
165-
var scaleStatus = await scaleManager.GetScaleStatusAsync(context);
166-
167-
return new ObjectResult(scaleStatus);
168169
}
169170

170171
[HttpPost]

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.Azure.WebJobs.Extensions.Http;
99
using Microsoft.Azure.WebJobs.Script.Diagnostics;
1010
using Microsoft.Azure.WebJobs.Script.Scale;
11+
using Microsoft.Extensions.DependencyInjection;
1112
using Microsoft.Extensions.Logging;
1213
using Microsoft.Extensions.Options;
1314

@@ -28,9 +29,15 @@ public HttpThrottleMiddleware(RequestDelegate next, ILoggerFactory loggerFactory
2829
_logger = loggerFactory?.CreateLogger("Host." + ScriptConstants.TraceSourceHttpThrottleMiddleware);
2930
}
3031

31-
public static bool ShouldEnable(HttpOptions options)
32+
public static bool ShouldEnable(IServiceProvider serviceProvider)
3233
{
33-
return HttpRequestQueue.IsEnabled(options) || options.DynamicThrottlesEnabled;
34+
var scriptHostManager = serviceProvider?.GetService<IScriptHostManager>();
35+
if (scriptHostManager != null && Utility.TryGetHostService(scriptHostManager, out IOptions<HttpOptions> options))
36+
{
37+
return HttpRequestQueue.IsEnabled(options.Value) || options.Value.DynamicThrottlesEnabled;
38+
}
39+
40+
return false;
3441
}
3542

3643
public async Task Invoke(HttpContext httpContext, IOptions<HttpOptions> httpOptions, HttpRequestQueue requestQueue, HostPerformanceManager performanceManager, IMetricsLogger metricsLogger)

src/WebJobs.Script.WebHost/WebJobsApplicationBuilderExtension.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public static IApplicationBuilder UseWebJobsScriptHost(this IApplicationBuilder
2525
IEnvironment environment = builder.ApplicationServices.GetService<IEnvironment>() ?? SystemEnvironment.Instance;
2626
IOptionsMonitor<StandbyOptions> standbyOptions = builder.ApplicationServices.GetService<IOptionsMonitor<StandbyOptions>>();
2727
IOptionsMonitor<HttpBodyControlOptions> httpBodyControlOptions = builder.ApplicationServices.GetService<IOptionsMonitor<HttpBodyControlOptions>>();
28-
IOptionsMonitor<HttpOptions> httpOptions = builder.ApplicationServices.GetService<IOptionsMonitor<HttpOptions>>();
28+
IServiceProvider serviceProvider = builder.ApplicationServices;
2929

3030
builder.UseMiddleware<SystemTraceMiddleware>();
3131
builder.UseMiddleware<HostnameFixupMiddleware>();
@@ -71,10 +71,11 @@ public static IApplicationBuilder UseWebJobsScriptHost(this IApplicationBuilder
7171
{
7272
config.UseMiddleware<HomepageMiddleware>();
7373
});
74-
builder.UseWhen(context => HttpThrottleMiddleware.ShouldEnable(httpOptions.CurrentValue) && !context.Request.IsAdminRequest(), config =>
74+
builder.UseWhen(context => !context.Request.IsAdminRequest() && HttpThrottleMiddleware.ShouldEnable(serviceProvider), config =>
7575
{
7676
config.UseMiddleware<HttpThrottleMiddleware>();
7777
});
78+
7879
builder.UseMiddleware<JobHostPipelineMiddleware>();
7980
builder.UseMiddleware<FunctionInvocationMiddleware>();
8081

src/WebJobs.Script/Scale/HostPerformanceManager.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,7 @@ internal ApplicationPerformanceCounters GetPerformanceCounters(ILogger logger =
234234
private IFunctionInvocationDispatcher GetDispatcherAsync()
235235
{
236236
var hostManager = _serviceProvider.GetService<IScriptHostManager>();
237-
var dispatcherFactory = (hostManager as IServiceProvider)?.GetService<IFunctionInvocationDispatcherFactory>();
238-
if (dispatcherFactory != null)
237+
if (Utility.TryGetHostService(hostManager, out IFunctionInvocationDispatcherFactory dispatcherFactory))
239238
{
240239
var dispatcher = dispatcherFactory.GetFunctionDispatcher();
241240
if (dispatcher.State == FunctionInvocationDispatcherState.Initialized)

src/WebJobs.Script/Utility.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616
using System.Threading;
1717
using System.Threading.Tasks;
1818
using Microsoft.AspNetCore.Http;
19-
using Microsoft.Azure.WebJobs.Host;
2019
using Microsoft.Azure.WebJobs.Logging;
2120
using Microsoft.Azure.WebJobs.Script.Description;
2221
using Microsoft.Azure.WebJobs.Script.Diagnostics.Extensions;
2322
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
23+
using Microsoft.Extensions.DependencyInjection;
2424
using Microsoft.Extensions.Logging;
2525
using Newtonsoft.Json;
2626
using Newtonsoft.Json.Converters;
@@ -44,6 +44,22 @@ public static class Utility
4444

4545
public static int ColdStartDelayMS { get; set; } = 5000;
4646

47+
internal static bool TryGetHostService<TService>(IScriptHostManager scriptHostManager, out TService service) where TService : class
48+
{
49+
service = null;
50+
51+
try
52+
{
53+
service = (scriptHostManager as IServiceProvider)?.GetService<TService>();
54+
}
55+
catch
56+
{
57+
// can get exceptions if the host is being disposed
58+
}
59+
60+
return service != null;
61+
}
62+
4763
/// <summary>
4864
/// Walk from the method up to the containing type, looking for an instance
4965
/// of the specified attribute type, returning it if found.

test/WebJobs.Script.Tests.Integration/Host/StandbyManager/StandbyManagerE2ETestBase.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using System.Web.Http;
1313
using Microsoft.AspNetCore.Hosting;
1414
using Microsoft.AspNetCore.TestHost;
15+
using Microsoft.Azure.WebJobs.Extensions.Http;
1516
using Microsoft.Azure.WebJobs.Host.Executors;
1617
using Microsoft.Azure.WebJobs.Script.Diagnostics;
1718
using Microsoft.Azure.WebJobs.Script.WebHost;
@@ -98,6 +99,15 @@ protected async Task<IWebHostBuilder> CreateWebHostBuilderAsync(string testDirNa
9899
.ConfigureScriptHostLogging(b =>
99100
{
100101
b.AddProvider(_loggerProvider);
102+
})
103+
.ConfigureScriptHostServices(s =>
104+
{
105+
s.PostConfigure<HttpOptions>(o =>
106+
{
107+
// disabling dynamic throttles since they can cause sporadic failures for
108+
// tests based on CPU limits being hit resulting in 429 responses
109+
o.DynamicThrottlesEnabled = false;
110+
});
101111
});
102112

103113
return webHostBuilder;

test/WebJobs.Script.Tests/Middleware/HttpThrottleMiddlewareTests.cs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,31 @@ public HttpThrottleMiddlewareTests()
6363
_requestQueue = new HttpRequestQueue(new OptionsWrapper<HttpOptions>(_httpOptions));
6464
}
6565

66+
[Fact]
67+
public void ShouldEnable_ReturnsExpectedValue()
68+
{
69+
IOptions<HttpOptions> optionsWrapper = null;
70+
var scriptHostManagerMock = new Mock<IScriptHostManager>(MockBehavior.Strict);
71+
var hostServiceProviderMock = scriptHostManagerMock.As<IServiceProvider>();
72+
hostServiceProviderMock.Setup(p => p.GetService(typeof(IOptions<HttpOptions>))).Returns(() => optionsWrapper);
73+
var rootServiceProvider = new Mock<IServiceProvider>(MockBehavior.Strict);
74+
rootServiceProvider.Setup(p => p.GetService(typeof(IScriptHostManager))).Returns(scriptHostManagerMock.Object);
75+
76+
Assert.False(HttpThrottleMiddleware.ShouldEnable(null));
77+
Assert.False(HttpThrottleMiddleware.ShouldEnable(rootServiceProvider.Object));
78+
79+
var httpOptions = new HttpOptions();
80+
optionsWrapper = new OptionsWrapper<HttpOptions>(httpOptions);
81+
Assert.False(HttpThrottleMiddleware.ShouldEnable(rootServiceProvider.Object));
82+
83+
httpOptions.MaxConcurrentRequests = 5;
84+
Assert.True(HttpThrottleMiddleware.ShouldEnable(rootServiceProvider.Object));
85+
86+
httpOptions.MaxConcurrentRequests = -1;
87+
httpOptions.DynamicThrottlesEnabled = true;
88+
Assert.True(HttpThrottleMiddleware.ShouldEnable(rootServiceProvider.Object));
89+
}
90+
6691
[Fact]
6792
public async Task Invoke_PropagatesExceptions()
6893
{
@@ -108,10 +133,11 @@ public async Task Invoke_NoThrottle_DispatchesDirectly()
108133
Assert.Equal(HttpStatusCode.Accepted, (HttpStatusCode)httpContext.Response.StatusCode);
109134
}
110135

111-
[Fact]
112-
public async Task Invoke_MaxParallelism_RequestsAreThrottled()
136+
[Theory]
137+
[InlineData(3)]
138+
[InlineData(1)]
139+
public async Task Invoke_MaxParallelism_RequestsAreThrottled(int maxParallelism)
113140
{
114-
int maxParallelism = 3;
115141
_httpOptions = new HttpOptions
116142
{
117143
MaxConcurrentRequests = maxParallelism

test/WebJobs.Script.Tests/UtilityTests.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ public struct TestStruct
3333
public string Location { get; set; }
3434
}
3535

36+
public class TestClass : ITestInterface
37+
{
38+
}
39+
3640
public class TestPoco
3741
{
3842
public string Name { get; set; }
@@ -55,6 +59,42 @@ public class UtilityTests
5559
{
5660
private TestLogger _testLogger = new TestLogger("test");
5761

62+
[Fact]
63+
public void TryGetHostService_ReturnsExpectedResult()
64+
{
65+
ITestInterface test = new TestClass();
66+
var scriptHostManagerMock = new Mock<IScriptHostManager>(MockBehavior.Strict);
67+
var serviceProviderMock = scriptHostManagerMock.As<IServiceProvider>();
68+
serviceProviderMock.Setup(p => p.GetService(typeof(ITestInterface))).Returns(() => test);
69+
70+
Assert.True(Utility.TryGetHostService(scriptHostManagerMock.Object, out ITestInterface result));
71+
Assert.Same(test, result);
72+
73+
test = null;
74+
Assert.False(Utility.TryGetHostService(scriptHostManagerMock.Object, out result));
75+
Assert.Null(result);
76+
}
77+
78+
[Fact]
79+
public void TryGetHostService_ObjectDisposed_ReturnsFalse()
80+
{
81+
var scriptHostManagerMock = new Mock<IScriptHostManager>(MockBehavior.Strict);
82+
var serviceProviderMock = scriptHostManagerMock.As<IServiceProvider>();
83+
serviceProviderMock.Setup(p => p.GetService(typeof(ITestInterface))).Throws(new ObjectDisposedException("Test"));
84+
85+
Assert.False(Utility.TryGetHostService(scriptHostManagerMock.Object, out ITestInterface result));
86+
Assert.Null(result);
87+
}
88+
89+
[Fact]
90+
public void TryGetHostService_NotServiceProvider_ReturnsFalse()
91+
{
92+
var scriptHostManagerMock = new Mock<IScriptHostManager>(MockBehavior.Strict);
93+
94+
Assert.False(Utility.TryGetHostService(scriptHostManagerMock.Object, out ITestInterface result));
95+
Assert.Null(result);
96+
}
97+
5898
[Fact]
5999
public async Task InvokeWithRetriesAsync_Throws_WhenRetryCountExceeded()
60100
{

0 commit comments

Comments
 (0)