Skip to content

Commit

Permalink
Allow direct Load webjobs dll.
Browse files Browse the repository at this point in the history
This is similar to precompiled, but is enabled if Function.json includes a "configurationSource" : "attributes".
All functions within an assembly need the same  setting.

Resolves #1508

Add test, which runs locally but disabled since it fails in appveyor.
  • Loading branch information
MikeStall committed Jul 28, 2017
1 parent 4aa3ad6 commit 05b4195
Show file tree
Hide file tree
Showing 9 changed files with 352 additions and 26 deletions.
5 changes: 5 additions & 0 deletions src/WebJobs.Script/Description/FunctionGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ public static Type Generate(string functionAssemblyName, string typeName, Collec

foreach (FunctionDescriptor function in functions)
{
if (function.Metadata.IsDirect)
{
continue;
}

MethodBuilder methodBuilder = tb.DefineMethod(function.Name, MethodAttributes.Public | MethodAttributes.Static);
Type[] types = function.Parameters.Select(p => p.Type).ToArray();
methodBuilder.SetParameters(types);
Expand Down
5 changes: 5 additions & 0 deletions src/WebJobs.Script/Description/FunctionMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ public FunctionMetadata()
/// </summary>
public bool IsExcluded { get; set; }

/// <summary>
/// Gets or sets a value indicating whether that this function is a direct invoke.
/// </summary>
public bool IsDirect { get; set; }

public Collection<BindingMetadata> Bindings { get; }

public IEnumerable<BindingMetadata> InputBindings
Expand Down
4 changes: 3 additions & 1 deletion src/WebJobs.Script/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Ms", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Description.FunctionLogger.#LogFunctionResult(System.Boolean,System.String,System.Int64)")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Binding.HttpBinding.#SetResponse(System.Net.Http.HttpRequestMessage,System.Object)")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Description.FunctionLogger.#CreateUserTraceWriter(Microsoft.Azure.WebJobs.Host.TraceWriter)")]

[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1648:inheritdoc must be used with inheriting class", Justification = "<Pending>", Scope = "member", Target = "~M:Microsoft.Azure.WebJobs.Script.Binding.MobileAppsScriptBindingProvider.#ctor(Microsoft.Azure.WebJobs.JobHostConfiguration,Newtonsoft.Json.Linq.JObject,Microsoft.Azure.WebJobs.Host.TraceWriter)")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Extensibility.ScriptBindingContext.#Type")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sku", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptConstants.#DynamicSkuConnectionLimit")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadFrom", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptHost.#AddDirectTypes(System.Collections.Generic.List`1<System.Type>,System.Collections.ObjectModel.Collection`1<Microsoft.Azure.WebJobs.Script.Description.FunctionDescriptor>)")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadFrom", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptHost.#LoadCustomExtensions(System.String)")]
145 changes: 120 additions & 25 deletions src/WebJobs.Script/Host/ScriptHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ public string InstanceId
/// </summary>
public virtual Collection<FunctionDescriptor> Functions { get; private set; }

// Maps from FunctionName to a set of errors for that function.
public virtual Dictionary<string, Collection<string>> FunctionErrors { get; private set; }

public virtual bool IsPrimary
Expand Down Expand Up @@ -515,6 +516,8 @@ protected virtual void Initialize()
List<Type> types = new List<Type>();
types.Add(type);

AddDirectTypes(types, functions);

hostConfig.TypeLocator = new TypeLocator(types);

Functions = functions;
Expand Down Expand Up @@ -563,6 +566,78 @@ private static IEnumerable<string> DiscoverBindingTypes(IEnumerable<FunctionMeta
return bindingTypes;
}

// Validate that for any precompiled assembly, all functions have the same configuration precedence.
private void VerifyPrecompileStatus(IEnumerable<FunctionDescriptor> functions)
{
HashSet<string> illegalScriptAssemblies = new HashSet<string>();

Dictionary<string, bool> mapAssemblySettings = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
foreach (var function in functions)
{
var metadata = function.Metadata;
var scriptFile = metadata.ScriptFile;
if (scriptFile.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
{
bool isDirect = metadata.IsDirect;
bool prevIsDirect;
if (mapAssemblySettings.TryGetValue(scriptFile, out prevIsDirect))
{
if (prevIsDirect != isDirect)
{
illegalScriptAssemblies.Add(scriptFile);
}
}
mapAssemblySettings[scriptFile] = isDirect;
}
}

foreach (var function in functions)
{
var metadata = function.Metadata;
var scriptFile = metadata.ScriptFile;

if (illegalScriptAssemblies.Contains(scriptFile))
{
// Error. All entries pointing to the same dll must have the same value for IsDirect
string msg = string.Format(CultureInfo.InvariantCulture, "Configuration error: All functions in {0} must have the same config precedence,",
scriptFile);

// Adding a function error will cause this function to get ignored
AddFunctionError(this.FunctionErrors, metadata.Name, msg);

TraceWriter.Info(msg);
_startupLogger?.LogInformation(msg);
}

return;
}
}

private static void AddDirectTypes(List<Type> types, Collection<FunctionDescriptor> functions)
{
HashSet<Type> visitedTypes = new HashSet<Type>();

foreach (var function in functions)
{
var metadata = function.Metadata;
if (!metadata.IsDirect)
{
continue;
}

string path = metadata.ScriptFile;
var typeName = Utility.GetFullClassName(metadata.EntryPoint);

Assembly assembly = Assembly.LoadFrom(path);
var type = assembly.GetType(typeName);

if (visitedTypes.Add(type))
{
types.Add(type);
}
}
}

private IMetricsLogger CreateMetricsLogger()
{
IMetricsLogger metricsLogger = ScriptConfig.HostConfig.GetService<IMetricsLogger>();
Expand All @@ -581,30 +656,7 @@ private void LoadCustomExtensions()
{
foreach (var dir in Directory.EnumerateDirectories(extensionsPath))
{
foreach (var path in Directory.EnumerateFiles(dir, "*.dll"))
{
// We don't want to load and reflect over every dll.
// By convention, restrict to based on filenames.
var filename = Path.GetFileName(path);
if (!filename.ToLowerInvariant().Contains("extension"))
{
continue;
}

try
{
// See GetNugetPackagesPath() for details
// Script runtime is already setup with assembly resolution hooks, so use LoadFrom
Assembly assembly = Assembly.LoadFrom(path);
LoadExtensions(assembly, path);
}
catch (Exception e)
{
string msg = $"Failed to load custom extension from '{path}'.";
TraceWriter.Error(msg, e);
_startupLogger.LogError(0, e, msg);
}
}
LoadCustomExtensions(dir);
}
}

Expand All @@ -616,6 +668,34 @@ private void LoadCustomExtensions()
generalProvider.CompleteInitialization(metadataProvider);
}

private void LoadCustomExtensions(string extensionsPath)
{
foreach (var path in Directory.EnumerateFiles(extensionsPath, "*.dll"))
{
// We don't want to load and reflect over every dll.
// By convention, restrict to based on filenames.
var filename = Path.GetFileName(path);
if (!filename.ToLowerInvariant().Contains("extension"))
{
continue;
}

try
{
// See GetNugetPackagesPath() for details
// Script runtime is already setup with assembly resolution hooks, so use LoadFrom
Assembly assembly = Assembly.LoadFrom(path);
LoadExtensions(assembly, path);
}
catch (Exception e)
{
string msg = $"Failed to load custom extension from '{path}'.";
TraceWriter.Error(msg, e);
_startupLogger.LogError(0, e, msg);
}
}
}

private void LoadExtensions(Assembly assembly, string locationHint)
{
foreach (var type in assembly.ExportedTypes)
Expand Down Expand Up @@ -930,6 +1010,20 @@ private static FunctionMetadata ParseFunctionMetadata(string functionName, JObje
functionMetadata.IsExcluded = (bool)value;
}

JToken isDirect;
if (configMetadata.TryGetValue("configurationSource", StringComparison.OrdinalIgnoreCase, out isDirect))
{
var isDirectValue = isDirect.ToString();
if (string.Equals(isDirectValue, "attributes", StringComparison.OrdinalIgnoreCase))
{
functionMetadata.IsDirect = true;
}
else if (!string.Equals(isDirectValue, "config", StringComparison.OrdinalIgnoreCase))
{
throw new FormatException($"Illegal value '{isDirectValue}' for 'configurationSource' property in {functionMetadata.Name}'.");
}
}

return functionMetadata;
}

Expand Down Expand Up @@ -1032,7 +1126,6 @@ public static Collection<FunctionMetadata> ReadFunctionMetadata(ScriptHostConfig

// determine the script type based on the primary script file extension
functionMetadata.ScriptType = ParseScriptType(functionMetadata.ScriptFile);

functionMetadata.EntryPoint = (string)functionConfig["entryPoint"];

return true;
Expand Down Expand Up @@ -1207,6 +1300,8 @@ internal Collection<FunctionDescriptor> GetFunctionDescriptors(IEnumerable<Funct
}
}

VerifyPrecompileStatus(functionDescriptors);

return functionDescriptors;
}

Expand Down
9 changes: 9 additions & 0 deletions src/WebJobs.Script/Utility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ public static bool IsValidUserType(Type type)
return data.ToDictionary(p => p.Key, p => p.Value != null ? p.Value.ToString() : null, StringComparer.OrdinalIgnoreCase);
}

// "Namespace.Class.Method" --> "Method"
public static string GetFunctionShortName(string functionName)
{
int idx = functionName.LastIndexOf('.');
Expand All @@ -144,6 +145,14 @@ public static string GetFunctionShortName(string functionName)
return functionName;
}

// "Namespace.Class.Method" --> "Namespace.Class"
public static string GetFullClassName(string fullFunctionName)
{
int i = fullFunctionName.LastIndexOf('.');
var typeName = fullFunctionName.Substring(0, i);
return typeName;
}

internal static string GetDefaultHostId(ScriptSettingsManager settingsManager, ScriptHostConfiguration scriptConfig)
{
// We're setting the default here on the newly created configuration
Expand Down

0 comments on commit 05b4195

Please sign in to comment.