-
Notifications
You must be signed in to change notification settings - Fork 356
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
Function Filter changes #1292
Conversation
@@ -51,6 +52,18 @@ public IFunctionInvoker Invoker | |||
get { return _invoker; } | |||
} | |||
|
|||
public object JobInstance |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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
5f95baf
to
7617c92
Compare
{ | ||
if (_jobInstance == null) | ||
{ | ||
_jobInstance = Invoker.CreateInstance(); |
There was a problem hiding this comment.
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?
- Ensures no races.
- lets the field be read-only like the others.
- It gives us a more predictable location for exceptions coming from IJobActivator.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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; } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
- Keep executing other eh filters?
- Does that become the new FunctionResult?
Throwing from an EH filter probably ought to be similar to throwing from a Post filter.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
Why are we targeting v2.x for this work? I thought the original work was based on |
// 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); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
@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. |
b4ecd5e
to
b42dfb3
Compare
b42dfb3
to
ff69f34
Compare
Merged |
Taking care of some loose ends with the new Function Invocation Filters feature. Main changes in this review: