Skip to content
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

Add support for customHandler section in host.json #6064

Merged
merged 4 commits into from
Jun 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/WebJobs.Script/Config/ConfigurationSectionNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public static class ConfigurationSectionNames
public const string ManagedDependency = "managedDependency";
public const string Extensions = "extensions";
public const string HttpWorker = "httpWorker";
public const string CustomHandler = "customHandler";
public const string Http = Extensions + ":http";
public const string Hsts = Http + ":hsts";
public const string CustomHttpHeaders = Http + ":customHeaders";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public class HostJsonFileConfigurationProvider : ConfigurationProvider
{
"version", "functionTimeout", "functions", "http", "watchDirectories", "queues", "serviceBus",
"eventHub", "singleton", "logging", "aggregator", "healthMonitor", "extensionBundle", "managedDependencies",
"httpWorker"
"customHandler", "httpWorker"
};

private readonly HostJsonFileConfigurationSource _configurationSource;
Expand Down
2 changes: 1 addition & 1 deletion src/WebJobs.Script/Diagnostics/MetricEventNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public static class MetricEventNames
public const string FunctionInvokeFailed = "function.invoke.failed";

// Http worker events
public const string HttpWorker = "hostjsonfileconfigurationsource.httpworker";
public const string CustomHandlerConfiguration = "hostjsonfileconfigurationsource.customhandler";
public const string DelayUntilWorkerIsInitialized = "httpworkerchannel.delayuntilworkerisinitialized";

// Out of proc process events
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ namespace Microsoft.Azure.WebJobs.Script.Workers.Http
{
public class HttpWorkerOptions
{
public string Type { get; set; } = "http";
pragnagopa marked this conversation as resolved.
Show resolved Hide resolved

public HttpWorkerDescription Description { get; set; }

public WorkerProcessArguments Arguments { get; set; }

public int Port { get; set; }

public bool EnableForwardingHttpRequest { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using Microsoft.Azure.WebJobs.Script.Configuration;
using Microsoft.Azure.WebJobs.Script.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

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

public HttpWorkerOptionsSetup(IOptions<ScriptJobHostOptions> scriptJobHostOptions, IConfiguration configuration, ILoggerFactory loggerFactory, IMetricsLogger metricsLogger)
{
Expand All @@ -30,27 +36,92 @@ public void Configure(HttpWorkerOptions options)
{
IConfigurationSection jobHostSection = _configuration.GetSection(ConfigurationSectionNames.JobHost);
var httpWorkerSection = jobHostSection.GetSection(ConfigurationSectionNames.HttpWorker);
var customHandlerSection = jobHostSection.GetSection(ConfigurationSectionNames.CustomHandler);

if (httpWorkerSection.Exists() && customHandlerSection.Exists())
{
_logger.LogWarning($"Both {ConfigurationSectionNames.HttpWorker} and {ConfigurationSectionNames.CustomHandler} sections are spefified in {ScriptConstants.HostMetadataFileName} file. {ConfigurationSectionNames.CustomHandler} takes precedence.");
}

if (customHandlerSection.Exists())
{
_metricsLogger.LogEvent(MetricEventNames.CustomHandlerConfiguration);
ConfigureWorkerDescription(options, customHandlerSection);
return;
}

if (httpWorkerSection.Exists())
{
_metricsLogger.LogEvent(MetricEventNames.HttpWorker);
httpWorkerSection.Bind(options);
HttpWorkerDescription httpWorkerDescription = options.Description;
// TODO: Add aka.ms/link to new docs
_logger.LogWarning($"Section {ConfigurationSectionNames.HttpWorker} will be deprecated. Please use {ConfigurationSectionNames.CustomHandler} section.");
ConfigureWorkerDescription(options, httpWorkerSection);
// Explicity set this empty to differentiate between customHandler and httpWorker options.
options.Type = string.Empty;
}
}

private void ConfigureWorkerDescription(HttpWorkerOptions options, IConfigurationSection workerSection)
{
workerSection.Bind(options);
HttpWorkerDescription httpWorkerDescription = options.Description;

if (httpWorkerDescription == null)
{
throw new HostConfigurationException($"Missing worker Description.");
}

var argumentsList = GetArgumentList(workerSection, argumentsSectionName);
if (argumentsList != null)
{
httpWorkerDescription.Arguments = argumentsList;
}

var workerArgumentList = GetArgumentList(workerSection, workerArgumentsSectionName);
if (workerArgumentList != null)
{
httpWorkerDescription.WorkerArguments = workerArgumentList;
}

httpWorkerDescription.ApplyDefaultsAndValidate(_scriptJobHostOptions.RootScriptPath, _logger);

if (httpWorkerDescription == null)
// Set default working directory to function app root.
if (string.IsNullOrEmpty(httpWorkerDescription.WorkingDirectory))
{
httpWorkerDescription.WorkingDirectory = _scriptJobHostOptions.RootScriptPath;
}
else
{
// Compute working directory relative to fucntion app root.
if (!Path.IsPathRooted(httpWorkerDescription.WorkingDirectory))
{
throw new HostConfigurationException($"Missing WorkerDescription for HttpWorker");
httpWorkerDescription.WorkingDirectory = Path.Combine(_scriptJobHostOptions.RootScriptPath, httpWorkerDescription.WorkingDirectory);
}
httpWorkerDescription.ApplyDefaultsAndValidate(_scriptJobHostOptions.RootScriptPath, _logger);
options.Arguments = new WorkerProcessArguments()
{
ExecutablePath = options.Description.DefaultExecutablePath,
WorkerPath = options.Description.DefaultWorkerPath
};
}

options.Arguments = new WorkerProcessArguments()
{
ExecutablePath = options.Description.DefaultExecutablePath,
WorkerPath = options.Description.DefaultWorkerPath
};

options.Arguments.ExecutableArguments.AddRange(options.Description.Arguments);
options.Port = GetUnusedTcpPort();
}

options.Arguments.ExecutableArguments.AddRange(options.Description.Arguments);
options.Port = GetUnusedTcpPort();
_logger.LogDebug("Configured httpWorker with {DefaultExecutablePath}: {exepath} with arguments {args}", nameof(options.Description.DefaultExecutablePath), options.Description.DefaultExecutablePath, options.Arguments);
private static List<string> GetArgumentList(IConfigurationSection httpWorkerSection, string argumentSectionName)
{
var argumentsSection = httpWorkerSection.GetSection(argumentSectionName);
if (argumentsSection.Exists() && argumentsSection?.Value != null)
{
try
{
return JsonConvert.DeserializeObject<List<string>>(argumentsSection.Value);
}
catch
{
}
}
return null;
}

internal static int GetUnusedTcpPort()
Expand Down
19 changes: 15 additions & 4 deletions src/WebJobs.Script/Workers/Http/DefaultHttpWorkerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ public Task InvokeAsync(ScriptInvocationContext scriptInvocationContext)
{
if (scriptInvocationContext.FunctionMetadata.IsHttpInAndOutFunction())
{
return ProcessHttpInAndOutInvocationRequest(scriptInvocationContext);
// type is empty for httpWorker section. EnableForwardingHttpRequest is opt-in for custom handler section.
if (string.IsNullOrEmpty(_httpWorkerOptions.Type) || _httpWorkerOptions.EnableForwardingHttpRequest)
{
return ProcessHttpInAndOutInvocationRequest(scriptInvocationContext);
}
return ProcessDefaultInvocationRequest(scriptInvocationContext);
}
return ProcessDefaultInvocationRequest(scriptInvocationContext);
}
Expand Down Expand Up @@ -108,11 +113,11 @@ internal async Task ProcessDefaultInvocationRequest(ScriptInvocationContext scri
{
if (httpScriptInvocationResult.Outputs == null || !httpScriptInvocationResult.Outputs.Any())
{
_logger.LogDebug("Outputs not set on http response for invocationId:{invocationId}", scriptInvocationContext.ExecutionContext.InvocationId);
_logger.LogWarning("Outputs not set on http response for invocationId:{invocationId}", scriptInvocationContext.ExecutionContext.InvocationId);
}
if (httpScriptInvocationResult.ReturnValue == null)
{
_logger.LogDebug("ReturnValue not set on http response for invocationId:{invocationId}", scriptInvocationContext.ExecutionContext.InvocationId);
_logger.LogWarning("ReturnValue not set on http response for invocationId:{invocationId}", scriptInvocationContext.ExecutionContext.InvocationId);
}

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

private void AddRequestHeadersAndSetRequestUri(HttpRequestMessage httpRequestMessage, string functionName, string invocationId)
{
httpRequestMessage.RequestUri = new Uri(new UriBuilder(WorkerConstants.HttpScheme, WorkerConstants.HostName, _httpWorkerOptions.Port, functionName).ToString());
string pathValue = functionName;
// _httpWorkerOptions.Type is populated only in customHandler section
if (httpRequestMessage.RequestUri != null && !string.IsNullOrEmpty(_httpWorkerOptions.Type))
{
pathValue = httpRequestMessage.RequestUri.AbsolutePath;
pragnagopa marked this conversation as resolved.
Show resolved Hide resolved
}
httpRequestMessage.RequestUri = new Uri(new UriBuilder(WorkerConstants.HttpScheme, WorkerConstants.HostName, _httpWorkerOptions.Port, pathValue).ToString());
httpRequestMessage.Headers.Add(HttpWorkerConstants.InvocationIdHeaderName, invocationId);
httpRequestMessage.Headers.Add(HttpWorkerConstants.HostVersionHeaderName, ScriptHost.Version);
httpRequestMessage.Headers.UserAgent.ParseAdd($"{HttpWorkerConstants.UserAgentHeaderValue}/{ScriptHost.Version}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@

using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using Microsoft.Azure.WebJobs.Script.Binding;
using Microsoft.Azure.WebJobs.Script.Description;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Microsoft.Azure.WebJobs.Script.Workers.Http
{
Expand All @@ -33,7 +30,10 @@ public static ScriptInvocationResult ToScriptInvocationResult(this HttpScriptInv
if (httpScriptInvocationResult.ReturnValue != null)
{
BindingMetadata returnParameterBindingMetadata = GetBindingMetadata(ScriptConstants.SystemReturnParameterBindingName, scriptInvocationContext);
scriptInvocationResult.Return = GetBindingValue(returnParameterBindingMetadata.DataType, httpScriptInvocationResult.ReturnValue);
if (returnParameterBindingMetadata != null)
{
scriptInvocationResult.Return = GetBindingValue(returnParameterBindingMetadata.DataType, httpScriptInvocationResult.ReturnValue);
}
}
return scriptInvocationResult;
}
Expand Down
3 changes: 3 additions & 0 deletions src/WebJobs.Script/Workers/Http/HttpWorkerConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,8 @@ public static class HttpWorkerConstants
// Child Process Env vars
public const string PortEnvVarName = "FUNCTIONS_HTTPWORKER_PORT";
public const string WorkerIdEnvVarName = "FUNCTIONS_HTTPWORKER_ID";
public const string FunctionAppRootVarName = "FUNCTIONS_APP_ROOT_PATH";
public const string CustomHandlerPortEnvVarName = "FUNCTIONS_CUSTOMHANDLER_PORT";
public const string CustomHandlerWorkerIdEnvVarName = "FUNCTIONS_CUSTOMHANDLER_WORKER_ID";
pragnagopa marked this conversation as resolved.
Show resolved Hide resolved
}
}
2 changes: 0 additions & 2 deletions src/WebJobs.Script/Workers/Http/HttpWorkerContext.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Microsoft.Azure.WebJobs.Script.Workers.Rpc;

namespace Microsoft.Azure.WebJobs.Script.Workers.Http
{
// Arguments to start a worker process
Expand Down
36 changes: 29 additions & 7 deletions src/WebJobs.Script/Workers/Http/HttpWorkerDescription.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,48 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using Microsoft.Extensions.Logging;

namespace Microsoft.Azure.WebJobs.Script.Workers.Http
{
public class HttpWorkerDescription : WorkerDescription
{
public override void ApplyDefaultsAndValidate(string workerDirectory, ILogger logger)
/// <summary>
/// Gets or sets WorkingDirectory for the process: DefaultExecutablePath
/// </summary>
public string WorkingDirectory { get; set; }

public override void ApplyDefaultsAndValidate(string inputWorkerDirectory, ILogger logger)
{
if (workerDirectory == null)
if (inputWorkerDirectory == null)
{
throw new ArgumentNullException(nameof(workerDirectory));
throw new ArgumentNullException(nameof(inputWorkerDirectory));
}
Arguments = Arguments ?? new List<string>();
WorkerArguments = WorkerArguments ?? new List<string>();
pragnagopa marked this conversation as resolved.
Show resolved Hide resolved

if (string.IsNullOrEmpty(WorkerDirectory))
{
WorkerDirectory = inputWorkerDirectory;
}
else
{
if (!Path.IsPathRooted(WorkerDirectory))
{
WorkerDirectory = Path.Combine(inputWorkerDirectory, WorkerDirectory);
}
}

WorkerDirectory = WorkerDirectory ?? workerDirectory;
ExpandEnvironmentVariables();

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

// If DefaultWorkerPath is set and find full path from scriptRootDir
Expand Down
6 changes: 5 additions & 1 deletion src/WebJobs.Script/Workers/Http/HttpWorkerProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Azure.WebJobs.Script.Diagnostics;
using Microsoft.Azure.WebJobs.Script.Eventing;
Expand Down Expand Up @@ -50,11 +51,14 @@ internal override Process CreateWorkerProcess()
RequestId = Guid.NewGuid().ToString(),
WorkerId = _workerId,
Arguments = _workerProcessArguments,
WorkingDirectory = _scriptRootPath,
WorkingDirectory = _httpWorkerOptions.Description.WorkingDirectory,
pragnagopa marked this conversation as resolved.
Show resolved Hide resolved
Port = _httpWorkerOptions.Port
};
workerContext.EnvironmentVariables.Add(HttpWorkerConstants.PortEnvVarName, _httpWorkerOptions.Port.ToString());
workerContext.EnvironmentVariables.Add(HttpWorkerConstants.WorkerIdEnvVarName, _workerId);
workerContext.EnvironmentVariables.Add(HttpWorkerConstants.CustomHandlerPortEnvVarName, _httpWorkerOptions.Port.ToString());
workerContext.EnvironmentVariables.Add(HttpWorkerConstants.CustomHandlerWorkerIdEnvVarName, _workerId);
workerContext.EnvironmentVariables.Add(HttpWorkerConstants.FunctionAppRootVarName, _scriptRootPath);
Process workerProcess = _processFactory.CreateWorkerProcess(workerContext);
if (_environment.IsLinuxConsumption())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
Expand Down Expand Up @@ -34,19 +35,23 @@ public virtual Process CreateWorkerProcess(WorkerContext context)
WorkingDirectory = context.WorkingDirectory,
Arguments = GetArguments(context),
};

var processEnvVariables = context.EnvironmentVariables;
if (processEnvVariables != null && processEnvVariables.Any())
{
foreach (var evnVar in processEnvVariables)
foreach (var envVar in processEnvVariables)
{
startInfo.EnvironmentVariables[evnVar.Key] = evnVar.Value;
startInfo.EnvironmentVariables[envVar.Key] = envVar.Value;
startInfo.Arguments = startInfo.Arguments.Replace($"%{envVar.Key}%", envVar.Value);
}
}
return new Process { StartInfo = startInfo };
}

private StringBuilder MergeArguments(StringBuilder builder, string arg) => builder.AppendFormat(" {0}", arg);
private StringBuilder MergeArguments(StringBuilder builder, string arg)
{
string expandedArg = Environment.ExpandEnvironmentVariables(arg);
return builder.AppendFormat(" {0}", expandedArg);
}

public string GetArguments(WorkerContext context)
{
Expand Down
Loading