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

Function Filter changes #1292

Closed
wants to merge 1 commit into from
Closed

Function Filter changes #1292

wants to merge 1 commit into from

Conversation

mathewc
Copy link
Member

@mathewc mathewc commented Aug 11, 2017

Taking care of some loose ends with the new Function Invocation Filters feature. Main changes in this review:

  • adding a new filter type for errors/exceptions (see FunctionExceptionFilterAttribute and related classes)
  • temporarily removing some of the stuff that was added for Azure Functions integration. We'll re-add that back when we address Invocation Filter Integration azure-functions-host#1761
  • bug fixes
  • some attribute/interface renames

@@ -51,6 +52,18 @@ public IFunctionInvoker Invoker
get { return _invoker; }
}

public object JobInstance
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The job instance was needed earlier in the pipeline for exception filters, so I'm moving to a create/cache model so other places in the pipeline can access the cached instance

/// <summary>
/// Base context class for <see cref="IFunctionExceptionFilter.OnExceptionAsync(FunctionExceptionContext, System.Threading.CancellationToken)"/>.
/// </summary>
public class FunctionExceptionContext : FunctionFilterContext
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because exception filters can run before any input arguments are bound, the base class isn't FunctionInvocationContext (i.e. no Arguments dictionary). We might try to conditionally expose the arguments if the exception happened after they were bound. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is good. We can always add more later. It's safer to not have arguments yet - people may come to rely on them and break when they get an early exception.


In reply to: 132725208 [](ancestors = 132725208)

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved all the Filters stuff under a new Host/Filters directory. That's why many of these show as additions instead of modifications.

/// <summary>
/// Gets the property bag that can be used to pass information between filters.
/// </summary>
public IDictionary<string, object> Properties { get; }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking I need to move this to the base FunctionFilterContext so exception filters also have access to this property bag. I think all filters should have access to the shared bag for the invocation scope. In ASP.NET, this can be done via ActionContext.HttpContext.Items which is available to all filter types

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed - I think this should be on the base class.


In reply to: 132727087 [](ancestors = 132727087)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to base class and added test

{
/// <summary>
/// Base class for declarative function invocation filters.
/// </summary>
public abstract class InvocationFilterAttribute : Attribute, IFunctionInvocationFilter
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public abstract class FunctionInvocationFilterAttribute : Attribute, IFunctionInvocationFilter
Copy link
Member Author

@mathewc mathewc Aug 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renaming so attribute and interface names are better in sync. Other filters follow the same pattern (IFunctionXXXFilter, FunctionXXXFilterAttribute)

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using Microsoft.Azure.WebJobs.Host.Executors;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These files all moved to the Filters directory

// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these test scenarios are now covered via the unit test suite more concisely

@@ -35,15 +35,8 @@ public void TestConcreteTypeNoConverter()
TestWorker<ConfigConcreteTypeNoConverter>();
}

public class ConfigConcreteTypeNoConverter : IExtensionConfigProvider, ITest<ConfigConcreteTypeNoConverter>
public class ConfigConcreteTypeNoConverter : BindingPathAttribute.Extension, ITest<ConfigConcreteTypeNoConverter>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merged in tests that Mike added in Hamza's branch

{
if (_jobInstance == null)
{
_jobInstance = Invoker.CreateInstance();
Copy link
Contributor

@MikeStall MikeStall Aug 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_jobInstance = Invoker.CreateInstance(); [](start = 20, length = 40)

Can we just create this upfront in the ctor?

  1. Ensures no races.
  2. lets the field be read-only like the others.
  3. It gives us a more predictable location for exceptions coming from IJobActivator.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, maybe just remove this whole property and put JobInstance on the ParameterHelper (where the other instantiated arguments live). That will solve of these issues.


In reply to: 132730667 [](ancestors = 132730667)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, done.

/// <summary>
/// Gets the function arguments.
/// </summary>
public IReadOnlyDictionary<string, object> Arguments { get; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string [](start = 35, length = 6)

Nit - can we comment that the key is the parameter name?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

/// <summary>
/// Gets the <see cref="Exception"/> that caused the function invocation to fail.
/// </summary>
public Exception Exception { get; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to capture the ExceptionDispatchInfo in this context.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will make both properties available as in ASP.NET

@@ -4,23 +4,24 @@
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Azure.WebJobs.Host
namespace Microsoft.Azure.WebJobs.Host.Filters
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filters [](start = 39, length = 7)

I'd vote against the sub namespace here. Function authors will be directly consuming this interface and itd be good to be top-level to be more discoverable.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved all back into the Host namespace

/// for filters with pre/post methods (e.g. <see cref="IFunctionInvocationFilter"/>) the executing portion
/// of the filters runs in the reverse order.
/// </summary>
private static List<TFilter> GetFilters<TFilter>(IExtensionRegistry extensionRegistry, FunctionDescriptor functionDescriptor, object instance) where TFilter : class
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

class [](start = 167, length = 5)

Could this be more specific. IFunctionFilter?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -153,6 +159,19 @@ public HostOutputMessage HostOutputMessage
return exceptionInfo != null ? new ExceptionDispatchInfoDelayedException(exceptionInfo) : null;
}

private async Task InvokeExceptionFiltersAsync(Exception exception, IFunctionInstance functionInstance, CancellationToken cancellationToken)
{
var exceptionFilters = GetFilters<IFunctionExceptionFilter>(_extensionRegistry, functionInstance.FunctionDescriptor, functionInstance.JobInstance);
Copy link
Contributor

@MikeStall MikeStall Aug 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

functionInstance.JobInstance [](start = 129, length = 28)

I think there's a bug here: currently, JobInstance is lazy; so this could call IJobActivator which could throw; and then we'd miss the exception.

Copy link
Member Author

@mathewc mathewc Aug 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JobActivator is no longer lazy - I'm creating it up front early. It is now encapsulated in the invocation try/catch so errors are handled, and I've added a test.

@@ -31,12 +31,12 @@ internal class FunctionExecutor : IFunctionExecutor
private readonly IAsyncCollector<FunctionInstanceLogEntry> _functionEventCollector;
private readonly ILogger _logger;
private readonly ILogger _resultsLogger;
private readonly IJobInvoker _jobInvoker;
private readonly IExtensionRegistry _extensionRegistry;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IExtensionRegistry [](start = 25, length = 18)

We shouldn't be passing this object around - too high level (same objection I had to passing JobHostConfig or even JobHost).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer referencing extension registry in executor now. Passing in collection of global filters.

var exceptionContext = new FunctionExceptionContext(functionInstance.Id, functionInstance.FunctionDescriptor.ShortName, _logger, exception);
foreach (var exceptionFilter in exceptionFilters)
{
await exceptionFilter.OnExceptionAsync(exceptionContext, cancellationToken);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OnExceptionAsync [](start = 42, length = 16)

This is user code - what should we do if it throws?

  1. Keep executing other eh filters?
  2. Does that become the new FunctionResult?
    Throwing from an EH filter probably ought to be similar to throwing from a Post filter.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now handling this the same way as we do for invocation filters. We keep track of the last exception, keep executing filters, and the last exception wins

@@ -130,8 +136,8 @@ public HostOutputMessage HostOutputMessage
logCompletedCancellationToken = cancellationToken;
}

await NotifyCompleteAsync(fastItem, functionCompletedMessage.Arguments, exceptionInfo);
_resultsLogger?.LogFunctionResult(fastItem);
await NotifyCompleteAsync(instanceLogEntry, functionCompletedMessage.Arguments, exceptionInfo);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NotifyCompleteAsync [](start = 22, length = 19)

This is the "Stop Billing" event. An exception coming from the new call to InvokeExceptionFiltersAsync (which is feasible since it calls user code) might skip this. The call to InvokeExceptionFiltersAsync should probably be wrapped in a try/catch.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InvokeExceptionFiltersAsync is now handling exceptions. That said, this end event should probably have been in a finally block to begin with...

@fabiocav
Copy link
Member

Why are we targeting v2.x for this work? I thought the original work was based on dev and we'd backport.

// Create a source specifically for timeouts
using (CancellationTokenSource timeoutTokenSource = new CancellationTokenSource())
{
TimeoutAttribute timeoutAttribute = instance.FunctionDescriptor.TimeoutAttribute;
bool throwOnTimeout = timeoutAttribute == null ? false : timeoutAttribute.ThrowOnTimeout;
var timer = StartFunctionTimeout(instance, timeoutAttribute, timeoutTokenSource, traceWriter, logger);
var timer = StartFunctionTimeout(instance, timeoutAttribute, timeoutTokenSource, traceWriter, _logger);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StartFunctionTimeout [](start = 32, length = 20)

The new EH Filter won't be covered by this timeout. (Although neither is parameter binding - so maybe this timeout is just too low in the stack).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll track the timeout issue in a bug. I have a list of some of the other remaining items we want to address, and I'll include this

@mathewc
Copy link
Member Author

mathewc commented Aug 11, 2017

@fabiocav I originally targeted this branch because I was doing the Azure Functions integration at the same time, and AF isn't on Core yet. Anyhow, I'll get all this into Core as well.

@mathewc mathewc force-pushed the filter_updates branch 2 times, most recently from b4ecd5e to b42dfb3 Compare August 11, 2017 22:19
@mathewc
Copy link
Member Author

mathewc commented Aug 12, 2017

Merged

@mathewc mathewc deleted the filter_updates branch May 25, 2018 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants