Skip to content

Commit a7e941a

Browse files
authored
Add support for customHandler section in host.json (#6064) (#6193)
1 parent efb460d commit a7e941a

24 files changed

+553
-123
lines changed

src/WebJobs.Script/Config/ConfigurationSectionNames.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public static class ConfigurationSectionNames
1616
public const string ManagedDependency = "managedDependency";
1717
public const string Extensions = "extensions";
1818
public const string HttpWorker = "httpWorker";
19+
public const string CustomHandler = "customHandler";
1920
public const string Http = Extensions + ":http";
2021
public const string Hsts = Http + ":hsts";
2122
public const string CustomHttpHeaders = Http + ":customHeaders";

src/WebJobs.Script/Config/HostJsonFileConfigurationSource.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public class HostJsonFileConfigurationProvider : ConfigurationProvider
5151
{
5252
"version", "functionTimeout", "functions", "http", "watchDirectories", "queues", "serviceBus",
5353
"eventHub", "singleton", "logging", "aggregator", "healthMonitor", "extensionBundle", "managedDependencies",
54-
"httpWorker"
54+
"customHandler", "httpWorker"
5555
};
5656

5757
private readonly HostJsonFileConfigurationSource _configurationSource;

src/WebJobs.Script/Diagnostics/MetricEventNames.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public static class MetricEventNames
5656
public const string FunctionInvokeFailed = "function.invoke.failed";
5757

5858
// Http worker events
59-
public const string HttpWorker = "hostjsonfileconfigurationsource.httpworker";
59+
public const string CustomHandlerConfiguration = "hostjsonfileconfigurationsource.customhandler";
6060
public const string DelayUntilWorkerIsInitialized = "httpworkerchannel.delayuntilworkerisinitialized";
6161

6262
// Out of proc process events

src/WebJobs.Script/Workers/Http/Configuration/HttpWorkerOptions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ namespace Microsoft.Azure.WebJobs.Script.Workers.Http
55
{
66
public class HttpWorkerOptions
77
{
8+
public string Type { get; set; } = "http";
9+
810
public HttpWorkerDescription Description { get; set; }
911

1012
public WorkerProcessArguments Arguments { get; set; }
1113

1214
public int Port { get; set; }
15+
16+
public bool EnableForwardingHttpRequest { get; set; }
1317
}
1418
}

src/WebJobs.Script/Workers/Http/Configuration/HttpWorkerOptionsSetup.cs

Lines changed: 85 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4+
using System.Collections.Generic;
5+
using System.IO;
46
using System.Net;
57
using System.Net.Sockets;
68
using Microsoft.Azure.WebJobs.Script.Configuration;
79
using Microsoft.Azure.WebJobs.Script.Diagnostics;
810
using Microsoft.Extensions.Configuration;
911
using Microsoft.Extensions.Logging;
1012
using Microsoft.Extensions.Options;
13+
using Newtonsoft.Json;
14+
using Newtonsoft.Json.Linq;
1115

1216
namespace Microsoft.Azure.WebJobs.Script.Workers.Http
1317
{
@@ -17,6 +21,8 @@ internal class HttpWorkerOptionsSetup : IConfigureOptions<HttpWorkerOptions>
1721
private ILogger _logger;
1822
private IMetricsLogger _metricsLogger;
1923
private ScriptJobHostOptions _scriptJobHostOptions;
24+
private string argumentsSectionName = $"{WorkerConstants.WorkerDescription}:arguments";
25+
private string workerArgumentsSectionName = $"{WorkerConstants.WorkerDescription}:workerArguments";
2026

2127
public HttpWorkerOptionsSetup(IOptions<ScriptJobHostOptions> scriptJobHostOptions, IConfiguration configuration, ILoggerFactory loggerFactory, IMetricsLogger metricsLogger)
2228
{
@@ -30,27 +36,92 @@ public void Configure(HttpWorkerOptions options)
3036
{
3137
IConfigurationSection jobHostSection = _configuration.GetSection(ConfigurationSectionNames.JobHost);
3238
var httpWorkerSection = jobHostSection.GetSection(ConfigurationSectionNames.HttpWorker);
39+
var customHandlerSection = jobHostSection.GetSection(ConfigurationSectionNames.CustomHandler);
40+
41+
if (httpWorkerSection.Exists() && customHandlerSection.Exists())
42+
{
43+
_logger.LogWarning($"Both {ConfigurationSectionNames.HttpWorker} and {ConfigurationSectionNames.CustomHandler} sections are spefified in {ScriptConstants.HostMetadataFileName} file. {ConfigurationSectionNames.CustomHandler} takes precedence.");
44+
}
45+
46+
if (customHandlerSection.Exists())
47+
{
48+
_metricsLogger.LogEvent(MetricEventNames.CustomHandlerConfiguration);
49+
ConfigureWorkerDescription(options, customHandlerSection);
50+
return;
51+
}
52+
3353
if (httpWorkerSection.Exists())
3454
{
35-
_metricsLogger.LogEvent(MetricEventNames.HttpWorker);
36-
httpWorkerSection.Bind(options);
37-
HttpWorkerDescription httpWorkerDescription = options.Description;
55+
// TODO: Add aka.ms/link to new docs
56+
_logger.LogWarning($"Section {ConfigurationSectionNames.HttpWorker} will be deprecated. Please use {ConfigurationSectionNames.CustomHandler} section.");
57+
ConfigureWorkerDescription(options, httpWorkerSection);
58+
// Explicity set this empty to differentiate between customHandler and httpWorker options.
59+
options.Type = string.Empty;
60+
}
61+
}
62+
63+
private void ConfigureWorkerDescription(HttpWorkerOptions options, IConfigurationSection workerSection)
64+
{
65+
workerSection.Bind(options);
66+
HttpWorkerDescription httpWorkerDescription = options.Description;
67+
68+
if (httpWorkerDescription == null)
69+
{
70+
throw new HostConfigurationException($"Missing worker Description.");
71+
}
72+
73+
var argumentsList = GetArgumentList(workerSection, argumentsSectionName);
74+
if (argumentsList != null)
75+
{
76+
httpWorkerDescription.Arguments = argumentsList;
77+
}
78+
79+
var workerArgumentList = GetArgumentList(workerSection, workerArgumentsSectionName);
80+
if (workerArgumentList != null)
81+
{
82+
httpWorkerDescription.WorkerArguments = workerArgumentList;
83+
}
84+
85+
httpWorkerDescription.ApplyDefaultsAndValidate(_scriptJobHostOptions.RootScriptPath, _logger);
3886

39-
if (httpWorkerDescription == null)
87+
// Set default working directory to function app root.
88+
if (string.IsNullOrEmpty(httpWorkerDescription.WorkingDirectory))
89+
{
90+
httpWorkerDescription.WorkingDirectory = _scriptJobHostOptions.RootScriptPath;
91+
}
92+
else
93+
{
94+
// Compute working directory relative to fucntion app root.
95+
if (!Path.IsPathRooted(httpWorkerDescription.WorkingDirectory))
4096
{
41-
throw new HostConfigurationException($"Missing WorkerDescription for HttpWorker");
97+
httpWorkerDescription.WorkingDirectory = Path.Combine(_scriptJobHostOptions.RootScriptPath, httpWorkerDescription.WorkingDirectory);
4298
}
43-
httpWorkerDescription.ApplyDefaultsAndValidate(_scriptJobHostOptions.RootScriptPath, _logger);
44-
options.Arguments = new WorkerProcessArguments()
45-
{
46-
ExecutablePath = options.Description.DefaultExecutablePath,
47-
WorkerPath = options.Description.DefaultWorkerPath
48-
};
99+
}
100+
101+
options.Arguments = new WorkerProcessArguments()
102+
{
103+
ExecutablePath = options.Description.DefaultExecutablePath,
104+
WorkerPath = options.Description.DefaultWorkerPath
105+
};
106+
107+
options.Arguments.ExecutableArguments.AddRange(options.Description.Arguments);
108+
options.Port = GetUnusedTcpPort();
109+
}
49110

50-
options.Arguments.ExecutableArguments.AddRange(options.Description.Arguments);
51-
options.Port = GetUnusedTcpPort();
52-
_logger.LogDebug("Configured httpWorker with {DefaultExecutablePath}: {exepath} with arguments {args}", nameof(options.Description.DefaultExecutablePath), options.Description.DefaultExecutablePath, options.Arguments);
111+
private static List<string> GetArgumentList(IConfigurationSection httpWorkerSection, string argumentSectionName)
112+
{
113+
var argumentsSection = httpWorkerSection.GetSection(argumentSectionName);
114+
if (argumentsSection.Exists() && argumentsSection?.Value != null)
115+
{
116+
try
117+
{
118+
return JsonConvert.DeserializeObject<List<string>>(argumentsSection.Value);
119+
}
120+
catch
121+
{
122+
}
53123
}
124+
return null;
54125
}
55126

56127
internal static int GetUnusedTcpPort()

src/WebJobs.Script/Workers/Http/DefaultHttpWorkerService.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,12 @@ public Task InvokeAsync(ScriptInvocationContext scriptInvocationContext)
4040
{
4141
if (scriptInvocationContext.FunctionMetadata.IsHttpInAndOutFunction())
4242
{
43-
return ProcessHttpInAndOutInvocationRequest(scriptInvocationContext);
43+
// type is empty for httpWorker section. EnableForwardingHttpRequest is opt-in for custom handler section.
44+
if (string.IsNullOrEmpty(_httpWorkerOptions.Type) || _httpWorkerOptions.EnableForwardingHttpRequest)
45+
{
46+
return ProcessHttpInAndOutInvocationRequest(scriptInvocationContext);
47+
}
48+
return ProcessDefaultInvocationRequest(scriptInvocationContext);
4449
}
4550
return ProcessDefaultInvocationRequest(scriptInvocationContext);
4651
}
@@ -108,11 +113,11 @@ internal async Task ProcessDefaultInvocationRequest(ScriptInvocationContext scri
108113
{
109114
if (httpScriptInvocationResult.Outputs == null || !httpScriptInvocationResult.Outputs.Any())
110115
{
111-
_logger.LogDebug("Outputs not set on http response for invocationId:{invocationId}", scriptInvocationContext.ExecutionContext.InvocationId);
116+
_logger.LogWarning("Outputs not set on http response for invocationId:{invocationId}", scriptInvocationContext.ExecutionContext.InvocationId);
112117
}
113118
if (httpScriptInvocationResult.ReturnValue == null)
114119
{
115-
_logger.LogDebug("ReturnValue not set on http response for invocationId:{invocationId}", scriptInvocationContext.ExecutionContext.InvocationId);
120+
_logger.LogWarning("ReturnValue not set on http response for invocationId:{invocationId}", scriptInvocationContext.ExecutionContext.InvocationId);
116121
}
117122

118123
ProcessLogsFromHttpResponse(scriptInvocationContext, httpScriptInvocationResult);
@@ -165,7 +170,13 @@ private HttpRequestMessage CreateAndGetHttpRequestMessage(string functionName, s
165170

166171
private void AddRequestHeadersAndSetRequestUri(HttpRequestMessage httpRequestMessage, string functionName, string invocationId)
167172
{
168-
httpRequestMessage.RequestUri = new Uri(new UriBuilder(WorkerConstants.HttpScheme, WorkerConstants.HostName, _httpWorkerOptions.Port, functionName).ToString());
173+
string pathValue = functionName;
174+
// _httpWorkerOptions.Type is populated only in customHandler section
175+
if (httpRequestMessage.RequestUri != null && !string.IsNullOrEmpty(_httpWorkerOptions.Type))
176+
{
177+
pathValue = httpRequestMessage.RequestUri.AbsolutePath;
178+
}
179+
httpRequestMessage.RequestUri = new Uri(new UriBuilder(WorkerConstants.HttpScheme, WorkerConstants.HostName, _httpWorkerOptions.Port, pathValue).ToString());
169180
httpRequestMessage.Headers.Add(HttpWorkerConstants.InvocationIdHeaderName, invocationId);
170181
httpRequestMessage.Headers.Add(HttpWorkerConstants.HostVersionHeaderName, ScriptHost.Version);
171182
httpRequestMessage.Headers.UserAgent.ParseAdd($"{HttpWorkerConstants.UserAgentHeaderValue}/{ScriptHost.Version}");

src/WebJobs.Script/Workers/Http/HttpScriptInvocationResultExtensions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Dynamic;
76
using System.Linq;
8-
using Microsoft.Azure.WebJobs.Script.Binding;
97
using Microsoft.Azure.WebJobs.Script.Description;
108
using Newtonsoft.Json;
11-
using Newtonsoft.Json.Linq;
129

1310
namespace Microsoft.Azure.WebJobs.Script.Workers.Http
1411
{
@@ -33,7 +30,10 @@ public static ScriptInvocationResult ToScriptInvocationResult(this HttpScriptInv
3330
if (httpScriptInvocationResult.ReturnValue != null)
3431
{
3532
BindingMetadata returnParameterBindingMetadata = GetBindingMetadata(ScriptConstants.SystemReturnParameterBindingName, scriptInvocationContext);
36-
scriptInvocationResult.Return = GetBindingValue(returnParameterBindingMetadata.DataType, httpScriptInvocationResult.ReturnValue);
33+
if (returnParameterBindingMetadata != null)
34+
{
35+
scriptInvocationResult.Return = GetBindingValue(returnParameterBindingMetadata.DataType, httpScriptInvocationResult.ReturnValue);
36+
}
3737
}
3838
return scriptInvocationResult;
3939
}

src/WebJobs.Script/Workers/Http/HttpWorkerConstants.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,8 @@ public static class HttpWorkerConstants
1313
// Child Process Env vars
1414
public const string PortEnvVarName = "FUNCTIONS_HTTPWORKER_PORT";
1515
public const string WorkerIdEnvVarName = "FUNCTIONS_HTTPWORKER_ID";
16+
public const string FunctionAppRootVarName = "FUNCTIONS_APP_ROOT_PATH";
17+
public const string CustomHandlerPortEnvVarName = "FUNCTIONS_CUSTOMHANDLER_PORT";
18+
public const string CustomHandlerWorkerIdEnvVarName = "FUNCTIONS_CUSTOMHANDLER_WORKER_ID";
1619
}
1720
}

src/WebJobs.Script/Workers/Http/HttpWorkerContext.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
5-
64
namespace Microsoft.Azure.WebJobs.Script.Workers.Http
75
{
86
// Arguments to start a worker process

src/WebJobs.Script/Workers/Http/HttpWorkerDescription.cs

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,48 @@
55
using System.Collections.Generic;
66
using System.ComponentModel.DataAnnotations;
77
using System.IO;
8+
using System.Linq;
89
using Microsoft.Extensions.Logging;
910

1011
namespace Microsoft.Azure.WebJobs.Script.Workers.Http
1112
{
1213
public class HttpWorkerDescription : WorkerDescription
1314
{
14-
public override void ApplyDefaultsAndValidate(string workerDirectory, ILogger logger)
15+
/// <summary>
16+
/// Gets or sets WorkingDirectory for the process: DefaultExecutablePath
17+
/// </summary>
18+
public string WorkingDirectory { get; set; }
19+
20+
public override void ApplyDefaultsAndValidate(string inputWorkerDirectory, ILogger logger)
1521
{
16-
if (workerDirectory == null)
22+
if (inputWorkerDirectory == null)
1723
{
18-
throw new ArgumentNullException(nameof(workerDirectory));
24+
throw new ArgumentNullException(nameof(inputWorkerDirectory));
1925
}
2026
Arguments = Arguments ?? new List<string>();
27+
WorkerArguments = WorkerArguments ?? new List<string>();
28+
29+
if (string.IsNullOrEmpty(WorkerDirectory))
30+
{
31+
WorkerDirectory = inputWorkerDirectory;
32+
}
33+
else
34+
{
35+
if (!Path.IsPathRooted(WorkerDirectory))
36+
{
37+
WorkerDirectory = Path.Combine(inputWorkerDirectory, WorkerDirectory);
38+
}
39+
}
2140

22-
WorkerDirectory = WorkerDirectory ?? workerDirectory;
41+
ExpandEnvironmentVariables();
2342

24-
// If DefaultWorkerPath is not set then compute full path for DefaultExecutablePath from scriptRootDir
25-
if (string.IsNullOrEmpty(DefaultWorkerPath) && !string.IsNullOrEmpty(DefaultExecutablePath) && !Path.IsPathRooted(DefaultExecutablePath))
43+
// If DefaultWorkerPath is not set then compute full path for DefaultExecutablePath from WorkingDirectory.
44+
// Empty DefaultWorkerPath or empty Arguments indicates DefaultExecutablePath is either a runtime on the system path or a file relative to WorkingDirectory.
45+
// No need to find full path for DefaultWorkerPath as WorkerDirectory will be set when launching the worker process.
46+
// DefaultWorkerPath can be specified as part of the arguments list
47+
if (string.IsNullOrEmpty(DefaultWorkerPath) && !string.IsNullOrEmpty(DefaultExecutablePath) && !Path.IsPathRooted(DefaultExecutablePath) && !Arguments.Any())
2648
{
27-
DefaultExecutablePath = Path.Combine(workerDirectory, DefaultExecutablePath);
49+
DefaultExecutablePath = Path.Combine(WorkerDirectory, DefaultExecutablePath);
2850
}
2951

3052
// If DefaultWorkerPath is set and find full path from scriptRootDir

0 commit comments

Comments
 (0)