Skip to content

Commit

Permalink
Adding DisableAttribute for multi-level job function disabling
Browse files Browse the repository at this point in the history
  • Loading branch information
mathewc committed Oct 20, 2015
1 parent 8cd539b commit f958e47
Show file tree
Hide file tree
Showing 17 changed files with 543 additions and 75 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// 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.Configuration;

namespace Microsoft.Azure.WebJobs.Host
{
/// <summary>
Expand Down Expand Up @@ -37,19 +34,8 @@ public static AmbientConnectionStringProvider Instance
public string GetConnectionString(string connectionStringName)
{
connectionStringName = GetPrefixedConnectionStringName(connectionStringName);
string connectionStringInConfig = null;
var connectionStringEntry = ConfigurationManager.ConnectionStrings[connectionStringName];
if (connectionStringEntry != null)
{
connectionStringInConfig = connectionStringEntry.ConnectionString;
}

if (!String.IsNullOrEmpty(connectionStringInConfig))
{
return connectionStringInConfig;
}

return Environment.GetEnvironmentVariable(connectionStringName) ?? connectionStringInConfig;
return ConfigurationUtility.GetConnectionFromConfigOrEnvironment(connectionStringName);
}

internal static string GetPrefixedConnectionStringName(string connectionStringName)
Expand Down
41 changes: 41 additions & 0 deletions src/Microsoft.Azure.WebJobs.Host/ConfigurationUtility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// 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.Configuration;

namespace Microsoft.Azure.WebJobs.Host
{
internal static class ConfigurationUtility
{
public static string GetSettingFromConfigOrEnvironment(string settingName)
{
string configValue = ConfigurationManager.AppSettings[settingName];
if (!string.IsNullOrEmpty(configValue))
{
// config values take precedence over environment values
return configValue;
}

return Environment.GetEnvironmentVariable(settingName) ?? configValue;
}

public static string GetConnectionFromConfigOrEnvironment(string connectionName)
{
string configValue = null;
var connectionStringEntry = ConfigurationManager.ConnectionStrings[connectionName];
if (connectionStringEntry != null)
{
configValue = connectionStringEntry.ConnectionString;
}

if (!string.IsNullOrEmpty(configValue))
{
// config values take precedence over environment values
return configValue;
}

return Environment.GetEnvironmentVariable(connectionName) ?? configValue;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ public static async Task<JobHostContext> CreateAndLogHostStartedAsync(
}

IFunctionIndex functions = await functionIndexProvider.GetAsync(combinedCancellationToken);
IListenerFactory functionsListenerFactory = new HostListenerFactory(functions.ReadAll(), singletonManager);
IListenerFactory functionsListenerFactory = new HostListenerFactory(functions.ReadAll(), singletonManager, activator, nameResolver, trace);

IFunctionExecutor hostCallExecutor;
IListener listener;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,25 +54,7 @@ public static Task<IStorageAccount> GetStorageAccountAsync(this IStorageAccountP
/// </summary>
internal static string GetAccountOverrideOrNull(ParameterInfo parameter)
{
if (parameter == null ||
parameter.GetType() == typeof(AttributeBindingSource.FakeParameterInfo))
{
return null;
}

StorageAccountAttribute attribute = parameter.GetCustomAttribute<StorageAccountAttribute>();
if (attribute != null)
{
return attribute.Account;
}

attribute = parameter.Member.GetCustomAttribute<StorageAccountAttribute>();
if (attribute != null)
{
return attribute.Account;
}

attribute = parameter.Member.DeclaringType.GetCustomAttribute<StorageAccountAttribute>();
StorageAccountAttribute attribute = TypeUtility.GetHierarchicalAttributeOrNull<StorageAccountAttribute>(parameter);
if (attribute != null)
{
return attribute.Account;
Expand Down
2 changes: 2 additions & 0 deletions src/Microsoft.Azure.WebJobs.Host/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.PropertyHelper.#.ctor(System.Reflection.PropertyInfo)")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "PropertyType", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.PropertyHelper.#PropertyType")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.PropertyHelper.#MakeFastPropertySetter`1(System.Reflection.PropertyInfo)")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "IsDisabled", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Listeners.HostListenerFactory.#IsDisabledByProvider(System.Type,System.Reflection.MethodInfo,Microsoft.Azure.WebJobs.Host.IJobActivator)")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "MethodInfo", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Listeners.HostListenerFactory.#IsDisabledByProvider(System.Type,System.Reflection.MethodInfo,Microsoft.Azure.WebJobs.Host.IJobActivator)")]
106 changes: 102 additions & 4 deletions src/Microsoft.Azure.WebJobs.Host/Listeners/HostListenerFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,33 @@

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Host.Bindings.Path;
using Microsoft.Azure.WebJobs.Host.Indexers;

namespace Microsoft.Azure.WebJobs.Host.Listeners
{
internal class HostListenerFactory : IListenerFactory
{
private static readonly MethodInfo JobActivatorCreateMethod = typeof(IJobActivator).GetMethod("CreateInstance", BindingFlags.Public | BindingFlags.Instance).GetGenericMethodDefinition();
private const string IsDisabledFunctionName = "IsDisabled";
private readonly IEnumerable<IFunctionDefinition> _functionDefinitions;
private readonly SingletonManager _singletonManager;
private readonly IJobActivator _activator;
private readonly INameResolver _nameResolver;
private readonly TraceWriter _trace;

public HostListenerFactory(IEnumerable<IFunctionDefinition> functionDefinitions, SingletonManager singletonManager)
public HostListenerFactory(IEnumerable<IFunctionDefinition> functionDefinitions, SingletonManager singletonManager, IJobActivator activator, INameResolver nameResolver, TraceWriter trace)
{
_functionDefinitions = functionDefinitions;
_singletonManager = singletonManager;
_activator = activator;
_nameResolver = nameResolver;
_trace = trace;
}

public async Task<IListener> CreateAsync(CancellationToken cancellationToken)
Expand All @@ -29,25 +39,113 @@ public async Task<IListener> CreateAsync(CancellationToken cancellationToken)
foreach (IFunctionDefinition functionDefinition in _functionDefinitions)
{
IListenerFactory listenerFactory = functionDefinition.ListenerFactory;

if (listenerFactory == null)
{
continue;
}

// Determine if the function is disabled
MethodInfo method = functionDefinition.Descriptor.Method;
if (IsDisabled(method, _nameResolver, _activator))
{
_trace.Info(string.Format("Function '{0}' is disabled", functionDefinition.Descriptor.ShortName), TraceSource.Host);
continue;
}

IListener listener = await listenerFactory.CreateAsync(cancellationToken);

// if the listener is a Singleton, wrap it with our SingletonListener
SingletonAttribute singletonAttribute = SingletonManager.GetListenerSingletonOrNull(listener.GetType(), functionDefinition.Descriptor.Method);
SingletonAttribute singletonAttribute = SingletonManager.GetListenerSingletonOrNull(listener.GetType(), method);
if (singletonAttribute != null)
{
listener = new SingletonListener(functionDefinition.Descriptor.Method, singletonAttribute, _singletonManager, listener);
listener = new SingletonListener(method, singletonAttribute, _singletonManager, listener);
}

listeners.Add(listener);
}

return new CompositeListener(listeners);
}

internal static bool IsDisabled(MethodInfo method, INameResolver nameResolver, IJobActivator activator)
{
ParameterInfo triggerParameter = method.GetParameters().FirstOrDefault();
if (triggerParameter != null)
{
// look for the first DisableAttribute up the hierarchy
DisableAttribute disableAttribute = TypeUtility.GetHierarchicalAttributeOrNull<DisableAttribute>(triggerParameter);
if (disableAttribute != null)
{
if (!string.IsNullOrEmpty(disableAttribute.SettingName))
{
return IsDisabledBySetting(disableAttribute.SettingName, method, nameResolver);
}
else if (disableAttribute.ProviderType != null)
{
// a custom provider Type has been specified
return IsDisabledByProvider(disableAttribute.ProviderType, method, activator);
}
else
{
// the default constructor was used
return true;
}
}
}

return false;
}

internal static bool IsDisabledBySetting(string settingName, MethodInfo method, INameResolver nameResolver)
{
if (nameResolver != null)
{
settingName = nameResolver.ResolveWholeString(settingName);
}

BindingTemplate bindingTemplate = BindingTemplate.FromString(settingName);
Dictionary<string, string> bindingData = new Dictionary<string, string>();
bindingData.Add("MethodName", string.Format(CultureInfo.InvariantCulture, "{0}.{1}", method.DeclaringType.Name, method.Name));
bindingData.Add("MethodShortName", method.Name);
settingName = bindingTemplate.Bind(bindingData);

// check the target setting and return false (disabled) if the value exists
// and is "falsey"
string value = ConfigurationUtility.GetSettingFromConfigOrEnvironment(settingName);
if (!string.IsNullOrEmpty(value) &&
(string.Compare(value, "1", StringComparison.OrdinalIgnoreCase) == 0 ||
string.Compare(value, "true", StringComparison.OrdinalIgnoreCase) == 0))
{
return true;
}

return false;
}

internal static bool IsDisabledByProvider(Type providerType, MethodInfo jobFunction, IJobActivator activator)
{
MethodInfo methodInfo = providerType.GetMethod(IsDisabledFunctionName, BindingFlags.Public | BindingFlags.Static, null, new Type[] { typeof(MethodInfo) }, null);
if (methodInfo == null)
{
methodInfo = providerType.GetMethod(IsDisabledFunctionName, BindingFlags.Public | BindingFlags.Instance, null, new Type[] { typeof(MethodInfo) }, null);
}

if (methodInfo == null || methodInfo.ReturnType != typeof(bool))
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
"Type '{0}' must declare a method 'IsDisabled' returning bool and taking a single parameter of Type MethodInfo.", providerType.Name));
}

if (methodInfo.IsStatic)
{
return (bool)methodInfo.Invoke(null, new object[] { jobFunction });
}
else
{
MethodInfo createMethod = JobActivatorCreateMethod.MakeGenericMethod(providerType);
object instance = createMethod.Invoke(activator, null);
return (bool)methodInfo.Invoke(instance, new object[] { jobFunction });
}
}
}
}
46 changes: 46 additions & 0 deletions src/Microsoft.Azure.WebJobs.Host/TypeUtility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// 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.Reflection;
using Microsoft.Azure.WebJobs.Host.Bindings.Runtime;

namespace Microsoft.Azure.WebJobs.Host
{
internal static class TypeUtility
{
/// <summary>
/// Walk from the parameter up to the containing type, looking for an instance
/// of the specified attribute type, returning it if found.
/// </summary>
/// <param name="parameter">The parameter to check.</param>
internal static T GetHierarchicalAttributeOrNull<T>(ParameterInfo parameter) where T : Attribute
{
if (parameter == null ||
parameter.GetType() == typeof(AttributeBindingSource.FakeParameterInfo))
{
return null;
}

T attribute = parameter.GetCustomAttribute<T>();
if (attribute != null)
{
return attribute;
}

attribute = parameter.Member.GetCustomAttribute<T>();
if (attribute != null)
{
return attribute;
}

attribute = parameter.Member.DeclaringType.GetCustomAttribute<T>();
if (attribute != null)
{
return attribute;
}

return null;
}
}
}
2 changes: 2 additions & 0 deletions src/Microsoft.Azure.WebJobs.Host/WebJobs.Host.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@
<Compile Include="Blobs\IBlobContainerArgumentBindingProvider.cs" />
<Compile Include="Blobs\Listeners\BlobQueueRegistration.cs" />
<Compile Include="CompositeTraceWriter.cs" />
<Compile Include="ConfigurationUtility.cs" />
<Compile Include="Config\ExtensionConfigContext.cs" />
<Compile Include="Config\IExtensionConfigProvider.cs" />
<Compile Include="ConsoleTraceWriter.cs" />
Expand Down Expand Up @@ -862,6 +863,7 @@
<Compile Include="Indexers\DefaultNameResolver.cs" />
<Compile Include="NameResolverExtensions.cs" />
<Compile Include="FunctionInvocationException.cs" />
<Compile Include="TypeUtility.cs" />
<Compile Include="WebJobsShutdownWatcher.cs" />
<Compile Include="Tables\TableEntityPath.cs" />
<Compile Include="HostContainerNames.cs" />
Expand Down
65 changes: 65 additions & 0 deletions src/Microsoft.Azure.WebJobs/DisableAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;

namespace Microsoft.Azure.WebJobs
{
/// <summary>
/// Attribute that can be applied to job functions, trigger parameters and classes
/// to conditionally disable triggered functions.
/// <remarks>
/// For example, by using this attribute, you can dynamically disable functions temporarily
/// by changing application settings. Note that the disable check is done on startup only.
/// If a <see cref="DisableAttribute"/> in the hierarchy (Parameter/Method/Class) exists and
/// indicates that the function should be disabled, the listener for that function will not be
/// started. The attribute only affects triggered functions.
/// </remarks>
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class DisableAttribute : Attribute
{
/// <summary>
/// Constructs a new instance.
/// </summary>
public DisableAttribute()
{
}

/// <summary>
/// Constructs a new instance.
/// </summary>
/// <param name="settingName">The name of an application setting or environment variable that
/// governs whether the function(s) should be disabled. If the specified setting exists and its
/// value is "1" or "True", the function will be disabled. The setting name can contain binding
/// parameters (e.g. {MethodName}, {MethodShortName}, %test%, etc.).</param>
public DisableAttribute(string settingName)
{
SettingName = settingName;
}

/// <summary>
/// Constructs a new instance.
/// </summary>
/// <param name="providerType">A Type which implements a method named "IsDisabled" taking
/// a <see cref="System.Reflection.MethodInfo"/> and returning <see cref="bool"/>. This
/// function will be called to determine whether the target function should be disabled.
/// </param>
public DisableAttribute(Type providerType)
{
ProviderType = providerType;
}

/// <summary>
/// Gets the name of the application setting or environment variable that will
/// be used to determine whether the function(s) will be disabled.
/// </summary>
public string SettingName { get; private set; }

/// <summary>
/// Gets the custom <see cref="Type"/> that will be invoked to determine
/// whether the function(s) will be disabled.
/// </summary>
public Type ProviderType { get; private set; }
}
}
Loading

0 comments on commit f958e47

Please sign in to comment.