-
Notifications
You must be signed in to change notification settings - Fork 466
[Enhanced Http] Support for http proxying scenarios #9193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
714da52
baaae4b
4257c20
7f4b66a
3052efc
0ff22dd
93c0b81
a0e4048
807b46c
9c7f967
8871f18
142257f
2b53757
b2dbedb
2e094ee
43bd487
06cb2ad
24cfbe7
2360393
bfe5633
697880f
1a1d5fe
c9988cd
f22dfca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,7 @@ | |
using Microsoft.Azure.WebJobs.Script.Workers.SharedMemoryDataTransfer; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
using Yarp.ReverseProxy.Forwarder; | ||
|
||
using static Microsoft.Azure.WebJobs.Script.Grpc.Messages.RpcLog.Types; | ||
using FunctionMetadata = Microsoft.Azure.WebJobs.Script.Description.FunctionMetadata; | ||
|
@@ -81,7 +82,8 @@ internal class GrpcWorkerChannel : IRpcWorkerChannel, IDisposable | |
private bool _isSharedMemoryDataTransferEnabled; | ||
private bool? _cancelCapabilityEnabled; | ||
private bool _isWorkerApplicationInsightsLoggingEnabled; | ||
|
||
private IHttpProxyService _httpProxyService; | ||
private Uri _httpProxyEndpoint; | ||
private System.Timers.Timer _timer; | ||
|
||
internal GrpcWorkerChannel( | ||
|
@@ -96,7 +98,8 @@ internal GrpcWorkerChannel( | |
IOptionsMonitor<ScriptApplicationHostOptions> applicationHostOptions, | ||
ISharedMemoryManager sharedMemoryManager, | ||
IOptions<WorkerConcurrencyOptions> workerConcurrencyOptions, | ||
IOptions<FunctionsHostingConfigOptions> hostingConfigOptions) | ||
IOptions<FunctionsHostingConfigOptions> hostingConfigOptions, | ||
IHttpProxyService httpProxyService) | ||
{ | ||
_workerId = workerId; | ||
_eventManager = eventManager; | ||
|
@@ -112,6 +115,7 @@ internal GrpcWorkerChannel( | |
_processInbound = state => ProcessItem((InboundGrpcEvent)state); | ||
_hostingConfigOptions = hostingConfigOptions; | ||
|
||
_httpProxyService = httpProxyService; | ||
_workerCapabilities = new GrpcCapabilities(_workerChannelLogger); | ||
|
||
if (!_eventManager.TryGetGrpcChannels(workerId, out var inbound, out var outbound)) | ||
|
@@ -133,6 +137,8 @@ internal GrpcWorkerChannel( | |
_state = RpcWorkerChannelState.Default; | ||
} | ||
|
||
private bool IsHttpProxyingWorker => _httpProxyEndpoint is not null; | ||
|
||
public string Id => _workerId; | ||
|
||
public IDictionary<string, BufferBlock<ScriptInvocationContext>> FunctionInputBuffers => _functionInputBuffers; | ||
|
@@ -420,6 +426,20 @@ internal void WorkerInitResponse(GrpcEvent initEvent) | |
_isWorkerApplicationInsightsLoggingEnabled = true; | ||
} | ||
|
||
// If http proxying is enabled, we need to get the proxying endpoint of this worker | ||
var httpUri = _workerCapabilities.GetCapabilityState(RpcWorkerConstants.HttpUri); | ||
satvu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (!string.IsNullOrEmpty(httpUri) && FeatureFlags.IsEnabled(ScriptConstants.FeatureFlagEnableHttpProxying)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
{ | ||
try | ||
{ | ||
_httpProxyEndpoint = new Uri(httpUri); | ||
} | ||
catch (Exception ex) | ||
{ | ||
HandleWorkerInitError(ex); | ||
} | ||
} | ||
|
||
_workerInitTask.TrySetResult(true); | ||
} | ||
|
||
|
@@ -738,6 +758,13 @@ await SendStreamingMessageAsync(new StreamingMessage | |
{ | ||
context.CancellationToken.Register(() => SendInvocationCancel(invocationRequest.InvocationId)); | ||
} | ||
|
||
if (IsHttpProxyingWorker && FeatureFlags.IsEnabled(ScriptConstants.FeatureFlagEnableHttpProxying) && context.FunctionMetadata.IsHttpTriggerFunction()) | ||
{ | ||
var aspNetTask = _httpProxyService.ForwardAsync(context, _httpProxyEndpoint).AsTask(); | ||
|
||
context.Properties.Add(ScriptConstants.HttpProxyTask, aspNetTask); | ||
} | ||
} | ||
catch (Exception invokeEx) | ||
{ | ||
|
@@ -902,6 +929,20 @@ internal async Task InvokeResponse(InvocationResponse invokeResponse) | |
{ | ||
if (invokeResponse.Result.IsInvocationSuccess(context.ResultSource, capabilityEnabled)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The design for these cases hasn't been discussed but for now if |
||
{ | ||
if (FeatureFlags.IsEnabled(ScriptConstants.FeatureFlagEnableHttpProxying) && IsHttpProxyingWorker) | ||
{ | ||
if (context.Properties.TryGetValue(ScriptConstants.HttpProxyTask, out Task<ForwarderError> httpProxyTask)) | ||
{ | ||
ForwarderError httpProxyTaskResult = await httpProxyTask; | ||
|
||
if (httpProxyTaskResult is not ForwarderError.None) | ||
{ | ||
// TODO: Understand scenarios where function invocation succeeds but there is an error proxying | ||
// need to investigate different ForwarderErrors and consider how they will be relayed through other services and to users | ||
} | ||
} | ||
} | ||
|
||
_metricsLogger.LogEvent(string.Format(MetricEventNames.WorkerInvokeSucceeded, Id)); | ||
|
||
try | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,9 @@ public static IServiceCollection AddScriptGrpc(this IServiceCollection services) | |
|
||
services.AddSingleton<IRpcServer, AspNetCoreGrpcServer>(); | ||
|
||
services.AddHttpForwarder(); | ||
services.AddSingleton<IHttpProxyService, DefaultHttpProxyService>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately, we can't get this behind the feature flag (can't access |
||
|
||
return services; | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT License. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Net.Http; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.Azure.WebJobs.Script.Description; | ||
using Microsoft.Azure.WebJobs.Script.Workers.Rpc; | ||
using Yarp.ReverseProxy.Forwarder; | ||
|
||
namespace Microsoft.Azure.WebJobs.Script.Grpc | ||
{ | ||
internal class DefaultHttpProxyService : IHttpProxyService, IDisposable | ||
{ | ||
private readonly SocketsHttpHandler _handler; | ||
private readonly IHttpForwarder _httpForwarder; | ||
private readonly HttpMessageInvoker _messageInvoker; | ||
private readonly ForwarderRequestConfig _forwarderRequestConfig; | ||
|
||
public DefaultHttpProxyService(IHttpForwarder httpForwarder) | ||
{ | ||
_httpForwarder = httpForwarder ?? throw new ArgumentNullException(nameof(httpForwarder)); | ||
|
||
_handler = new SocketsHttpHandler(); | ||
_messageInvoker = new HttpMessageInvoker(_handler); | ||
_forwarderRequestConfig = new ForwarderRequestConfig(); | ||
} | ||
|
||
public void Dispose() | ||
{ | ||
_handler?.Dispose(); | ||
_messageInvoker?.Dispose(); | ||
} | ||
|
||
public ValueTask<ForwarderError> ForwardAsync(ScriptInvocationContext context, Uri httpUri) | ||
{ | ||
ArgumentNullException.ThrowIfNull(context); | ||
|
||
if (context.Inputs is null || context.Inputs?.Count() == 0) | ||
{ | ||
throw new InvalidOperationException($"The function {context.FunctionMetadata.Name} can not be evaluated since it has no inputs."); | ||
} | ||
|
||
HttpRequest httpRequest = context.Inputs.FirstOrDefault(i => i.Val is HttpRequest).Val as HttpRequest; | ||
|
||
if (httpRequest is null) | ||
{ | ||
throw new InvalidOperationException($"Cannot proxy the HttpTrigger function {context.FunctionMetadata.Name} without an input of type {nameof(HttpRequest)}."); | ||
} | ||
|
||
HttpContext httpContext = httpRequest.HttpContext; | ||
|
||
httpContext.Items.Add(ScriptConstants.HttpProxyingEnabled, bool.TrueString); | ||
|
||
// add invocation id as correlation id | ||
httpRequest.Headers.TryAdd(ScriptConstants.HttpProxyCorrelationHeader, context.ExecutionContext.InvocationId.ToString()); | ||
satvu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
var aspNetTask = _httpForwarder.SendAsync(httpContext, httpUri.ToString(), _messageInvoker, _forwarderRequestConfig); | ||
satvu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return aspNetTask; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT License. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Threading.Tasks; | ||
using Microsoft.Azure.WebJobs.Script.Description; | ||
using Yarp.ReverseProxy.Forwarder; | ||
|
||
namespace Microsoft.Azure.WebJobs.Script.Grpc | ||
{ | ||
public interface IHttpProxyService | ||
{ | ||
ValueTask<ForwarderError> ForwardAsync(ScriptInvocationContext context, Uri httpUri); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.