Skip to content

Commit 612336e

Browse files
committed
Implementing Http throttle support (#1261)
1 parent 3b9547c commit 612336e

23 files changed

+800
-80
lines changed

sample/Bot-CSharp/run.csx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
public class BotMessage
1+
using System.Net;
2+
3+
public class BotMessage
24
{
35
public string Source { get; set; }
46
public string Message { get; set; }

sample/host.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
"from": "Azure Functions <[email protected]>"
55
},
66
"http": {
7-
"routePrefix": "api"
7+
"routePrefix": "api",
8+
"maxDegreeOfParallelism": 5,
9+
"maxQueueLength": 30
810
},
911
"tracing": {
1012
"fileLoggingMode": "always"

schemas/json/host.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,21 @@
4444
"description": "Defines the default route prefix that applies to all routes. Use an empty string to remove the prefix.",
4545
"type": "string",
4646
"default": "api"
47+
},
48+
"maxDegreeOfParallelism": {
49+
"description": "Defines the the maximum number of http functions that will execute in parallel.",
50+
"type": "integer",
51+
"default": -1
52+
},
53+
"maxQueueLength": {
54+
"description": "Defines the maximum number of pending requests that will be queued for processing.",
55+
"type": "integer",
56+
"default": -1
57+
},
58+
"dynamicThrottlesEnabled": {
59+
"description": "Indicates whether dynamic host counter checks should be enabled.",
60+
"type": "boolean",
61+
"default": false
4762
}
4863
}
4964
},
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
{
2-
"key": null
2+
"keys": [
3+
{
4+
"name": "default",
5+
"value": "9884kkdlkkdf83ksld8589kflss90sll5kjjsyfjskqv",
6+
"encrypted": false
7+
}
8+
]
39
}

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

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Threading.Tasks;
1111
using System.Web.Http;
1212
using System.Web.Http.Controllers;
13+
using System.Web.Http.Dependencies;
1314
using Microsoft.Azure.WebJobs.Script.Description;
1415
using Microsoft.Azure.WebJobs.Script.WebHost.Filters;
1516
using Microsoft.Azure.WebJobs.Script.WebHost.WebHooks;
@@ -32,36 +33,52 @@ public FunctionsController(WebScriptHostManager scriptHostManager, WebHookReceiv
3233

3334
public override async Task<HttpResponseMessage> ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)
3435
{
35-
HttpRequestMessage request = controllerContext.Request;
36-
37-
// First see if the request maps to an HTTP function
38-
FunctionDescriptor function = _scriptHostManager.GetHttpFunctionOrNull(request);
36+
var request = controllerContext.Request;
37+
var function = _scriptHostManager.GetHttpFunctionOrNull(request);
3938
if (function == null)
4039
{
40+
// request does not map to an HTTP function
4141
return new HttpResponseMessage(HttpStatusCode.NotFound);
4242
}
43+
request.SetProperty(ScriptConstants.AzureFunctionsHttpFunctionKey, function);
4344

44-
// Determine the authorization level of the request
45-
ISecretManager secretManager = controllerContext.Configuration.DependencyResolver.GetService<ISecretManager>();
46-
var settings = controllerContext.Configuration.DependencyResolver.GetService<WebHostSettings>();
47-
var authorizationLevel = settings.IsAuthDisabled
48-
? AuthorizationLevel.Admin
49-
: await AuthorizationLevelAttribute.GetAuthorizationLevelAsync(request, secretManager, functionName: function.Name);
50-
request.SetAuthorizationLevel(authorizationLevel);
51-
45+
var authorizationLevel = await DetermineAuthorizationLevelAsync(request, function, controllerContext.Configuration.DependencyResolver);
5246
if (function.Metadata.IsExcluded ||
53-
(function.Metadata.IsDisabled && authorizationLevel != AuthorizationLevel.Admin))
47+
(function.Metadata.IsDisabled && authorizationLevel != AuthorizationLevel.Admin))
5448
{
5549
// disabled functions are not publicly addressable w/o Admin level auth,
5650
// and excluded functions are also ignored here (though the check above will
5751
// already exclude them)
5852
return new HttpResponseMessage(HttpStatusCode.NotFound);
5953
}
6054

61-
// Dispatch the request
55+
Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> processRequestHandler = async (req, ct) =>
56+
{
57+
return await ProcessRequestAsync(req, function, ct);
58+
};
59+
return await _scriptHostManager.HttpRequestManager.ProcessRequestAsync(request, processRequestHandler, cancellationToken);
60+
}
61+
62+
public static async Task<AuthorizationLevel> DetermineAuthorizationLevelAsync(HttpRequestMessage request, FunctionDescriptor function, IDependencyResolver resolver)
63+
{
64+
var secretManager = resolver.GetService<ISecretManager>();
65+
var settings = resolver.GetService<WebHostSettings>();
66+
67+
var authorizationLevel = settings.IsAuthDisabled
68+
? AuthorizationLevel.Admin
69+
: await AuthorizationLevelAttribute.GetAuthorizationLevelAsync(request, secretManager, functionName: function.Name);
70+
request.SetAuthorizationLevel(authorizationLevel);
71+
72+
return authorizationLevel;
73+
}
74+
75+
private async Task<HttpResponseMessage> ProcessRequestAsync(HttpRequestMessage request, FunctionDescriptor function, CancellationToken cancellationToken)
76+
{
6277
HttpTriggerBindingMetadata httpFunctionMetadata = (HttpTriggerBindingMetadata)function.Metadata.InputBindings.FirstOrDefault(p => string.Compare("HttpTrigger", p.Type, StringComparison.OrdinalIgnoreCase) == 0);
6378
bool isWebHook = !string.IsNullOrEmpty(httpFunctionMetadata.WebHookType);
79+
var authorizationLevel = request.GetAuthorizationLevel();
6480
HttpResponseMessage response = null;
81+
6582
if (isWebHook)
6683
{
6784
if (authorizationLevel == AuthorizationLevel.Admin)

src/WebJobs.Script.WebHost/WebJobs.Script.WebHost.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,7 @@
475475
<Compile Include="WebHooks\WebHookReceiverManager.cs" />
476476
<Compile Include="WebScriptHostExceptionHandler.cs" />
477477
<Compile Include="WebScriptHostManager.cs" />
478+
<Compile Include="WebScriptHostRequestManager.cs" />
478479
</ItemGroup>
479480
<ItemGroup>
480481
<Content Include="packages.config">
@@ -492,6 +493,7 @@
492493
<Content Include="App_Data\secrets\httptrigger-csharp-customroute.json" />
493494
<Content Include="App_Data\secrets\httptrigger-customroute-get.json" />
494495
<Content Include="App_Data\secrets\httptrigger-customroute-post.json" />
496+
<Content Include="App_Data\secrets\httptrigger-csharp.json" />
495497
<None Include="App_Data\secrets\HttpTrigger.json" />
496498
<Content Include="App_Data\secrets\webhook-azure-csharp.json" />
497499
<None Include="App_Data\secrets\WebHook-Generic-CSharp.json" />

src/WebJobs.Script.WebHost/WebScriptHostManager.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using Microsoft.Azure.WebJobs.Host.Loggers;
2020
using Microsoft.Azure.WebJobs.Host.Timers;
2121
using Microsoft.Azure.WebJobs.Script.Binding;
22+
using Microsoft.Azure.WebJobs.Script.Binding.Http;
2223
using Microsoft.Azure.WebJobs.Script.Config;
2324
using Microsoft.Azure.WebJobs.Script.Description;
2425
using Microsoft.Azure.WebJobs.Script.Diagnostics;
@@ -42,6 +43,7 @@ public class WebScriptHostManager : ScriptHostManager
4243
private bool _hostStarted = false;
4344
private IDictionary<IHttpRoute, FunctionDescriptor> _httpFunctions;
4445
private HttpRouteCollection _httpRoutes;
46+
private HttpRequestManager _httpRequestManager;
4547

4648
public WebScriptHostManager(ScriptHostConfiguration config, ISecretManagerFactory secretManagerFactory, ScriptSettingsManager settingsManager, WebHostSettings webHostSettings, IScriptHostFactory scriptHostFactory = null, ISecretsRepositoryFactory secretsRepositoryFactory = null)
4749
: base(config, settingsManager, scriptHostFactory)
@@ -64,7 +66,7 @@ public WebScriptHostManager(ScriptHostConfiguration config, ISecretManagerFactor
6466

6567
config.IsSelfHost = webHostSettings.IsSelfHost;
6668

67-
_performanceManager = new HostPerformanceManager(settingsManager);
69+
_performanceManager = new HostPerformanceManager(settingsManager, config.TraceWriter);
6870
_swaggerDocumentManager = new SwaggerDocumentManager(config);
6971

7072
var secretsRepository = secretsRepositoryFactory.Create(settingsManager, webHostSettings, config);
@@ -87,6 +89,8 @@ public WebScriptHostManager(ScriptHostConfiguration config, ISecretManagerFactor
8789

8890
public ISwaggerDocumentManager SwaggerDocumentManager => _swaggerDocumentManager;
8991

92+
public HttpRequestManager HttpRequestManager => _httpRequestManager;
93+
9094
public virtual bool Initialized
9195
{
9296
get
@@ -414,6 +418,10 @@ protected override void OnHostCreated()
414418
// all http function routes
415419
InitializeHttpFunctions(Instance.Functions);
416420

421+
// since the request manager is created based on configurable
422+
// settings, it has to be recreated when host config changes
423+
_httpRequestManager = new WebScriptHostRequestManager(Instance.ScriptConfig.HttpConfiguration, PerformanceManager, _metricsLogger, _config.TraceWriter);
424+
417425
base.OnHostCreated();
418426
}
419427

@@ -429,7 +437,7 @@ internal void InitializeHttpFunctions(IEnumerable<FunctionDescriptor> functions)
429437
{
430438
// we must initialize the route factory here AFTER full configuration
431439
// has been resolved so we apply any route prefix customizations
432-
HttpRouteFactory httpRouteFactory = new HttpRouteFactory(_config.HttpRoutePrefix);
440+
var httpRouteFactory = new HttpRouteFactory(_config.HttpConfiguration.RoutePrefix);
433441

434442
_httpFunctions = new Dictionary<IHttpRoute, FunctionDescriptor>();
435443
_httpRoutes = new HttpRouteCollection();
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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.ObjectModel;
6+
using System.Net.Http;
7+
using Microsoft.Azure.WebJobs.Host;
8+
using Microsoft.Azure.WebJobs.Script.Binding.Http;
9+
using Microsoft.Azure.WebJobs.Script.Description;
10+
using Microsoft.Azure.WebJobs.Script.Diagnostics;
11+
12+
namespace Microsoft.Azure.WebJobs.Script.WebHost
13+
{
14+
public class WebScriptHostRequestManager : HttpRequestManager
15+
{
16+
private readonly HostPerformanceManager _performanceManager;
17+
private readonly IMetricsLogger _metricsLogger;
18+
private readonly int _performanceCheckPeriodSeconds;
19+
private DateTime _lastPerformanceCheck;
20+
private bool _rejectAllRequests;
21+
22+
public WebScriptHostRequestManager(HttpConfiguration config, HostPerformanceManager performanceManager, IMetricsLogger metricsLogger, TraceWriter traceWriter, int performanceCheckPeriodSeconds = 15) : base(config, traceWriter)
23+
{
24+
_performanceManager = performanceManager;
25+
_metricsLogger = metricsLogger;
26+
_performanceCheckPeriodSeconds = performanceCheckPeriodSeconds;
27+
}
28+
29+
protected override bool RejectAllRequests()
30+
{
31+
if (base.RejectAllRequests())
32+
{
33+
return true;
34+
}
35+
36+
if (Config.DynamicThrottlesEnabled &&
37+
((DateTime.UtcNow - _lastPerformanceCheck) > TimeSpan.FromSeconds(_performanceCheckPeriodSeconds)))
38+
{
39+
// only check host status periodically
40+
Collection<string> exceededCounters = new Collection<string>();
41+
_rejectAllRequests = _performanceManager.IsUnderHighLoad(exceededCounters);
42+
_lastPerformanceCheck = DateTime.UtcNow;
43+
if (_rejectAllRequests)
44+
{
45+
TraceWriter.Info($"Thresholds for the following counters have been exceeded: {string.Join(", ", exceededCounters)}");
46+
}
47+
}
48+
49+
return _rejectAllRequests;
50+
}
51+
52+
protected override HttpResponseMessage RejectRequest(HttpRequestMessage request)
53+
{
54+
var function = request.GetPropertyOrDefault<FunctionDescriptor>(ScriptConstants.AzureFunctionsHttpFunctionKey);
55+
_metricsLogger.LogEvent(MetricEventNames.FunctionInvokeThrottled, function.Name);
56+
57+
return base.RejectRequest(request);
58+
}
59+
}
60+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.Threading.Tasks.Dataflow;
5+
6+
namespace Microsoft.Azure.WebJobs.Script.Binding.Http
7+
{
8+
public class HttpConfiguration
9+
{
10+
public HttpConfiguration()
11+
{
12+
MaxQueueLength = DataflowBlockOptions.Unbounded;
13+
MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded;
14+
RoutePrefix = ScriptConstants.DefaultHttpRoutePrefix;
15+
}
16+
17+
/// <summary>
18+
/// Gets or sets the default route prefix that will be applied to
19+
/// function routes.
20+
/// </summary>
21+
public string RoutePrefix { get; set; }
22+
23+
/// <summary>
24+
/// Gets or sets the maximum number of pending requests that
25+
/// will be queued for processing. If this limit is exceeded,
26+
/// new requests will be rejected with a 429 status code.
27+
/// </summary>
28+
public int MaxQueueLength { get; set; }
29+
30+
/// <summary>
31+
/// Gets or sets the maximum number of http functions that will execute
32+
/// in parallel.
33+
/// </summary>
34+
public int MaxDegreeOfParallelism { get; set; }
35+
36+
/// <summary>
37+
/// Gets or sets a value indicating whether dynamic host counter
38+
/// checks should be enabled.
39+
/// </summary>
40+
public bool DynamicThrottlesEnabled { get; set; }
41+
}
42+
}

0 commit comments

Comments
 (0)