Open SeanFeldman opened 2 years ago
I've had some success trying to reflect it. Here's a snippet of my fiddling in an attempt to re-create some singleton functionality for single-instance hosts (since https://github.com/Azure/azure-functions-dotnet-worker/issues/274 seems to be moving along very slowly).
class SingletonFunctionMiddleware : IFunctionsWorkerMiddleware
{
static readonly SemaphoreSlim _mutex = new(1);
public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
{
var workerAssembly = Assembly.GetExecutingAssembly();
// EntryPoint is namespace + type + method, we need to isolate the namespace+type and method separately
var entryPointParts = context.FunctionDefinition.EntryPoint.Split(".");
// after splitting on "." the typename should be every part except that last
var workerTypeName = string.Join(".", entryPointParts[..^1]);
// which is the method name
var workerFunctionName = entryPointParts.Last();
// use reflection to get the MethodInfo (should be cached for performance)
var workerType = workerAssembly.GetType(workerTypeName);
var workerFunction = workerType.GetMethod(workerFunctionName);
if (workerFunction.GetCustomAttribute<SingletonAttribute>() is not null)
{
var logger = context.GetLogger<SingletonFunctionMiddleware>();
logger.LogTrace($"Singleton invocation '{context.InvocationId}' waiting for semaphore...");
await _mutex.WaitAsync();
logger.LogTrace($"Singleton invocation '{context.InvocationId}' entered sempahore");
try
{
await next(context);
_mutex.Release();
logger.LogTrace($"Singleton invocation '{context.InvocationId}' released sempahore");
}
catch (Exception)
{
_mutex.Release();
logger.LogTrace($"Singleton invocation '{context.InvocationId}' released sempahore");
throw;
}
}
else
{
await next(context);
}
}
}
In short, the idea is to get the context.FunctionDefinition.EntryPoint
and use that to look up the type/method in the executing assembly (assuming the function is from the same assembly, otherwise you'd need a reference to that assembly).
@SeanFeldman wanted to add a comment here. This is something we do intend to expose. We're hoping we can prioritize this work soon and will share the design/experience for feedback ahead of implementation.
Any progress on this?
Thanks you BenjaBobs We will use your code and this lib https://github.com/madelson/DistributedLock/blob/master/docs/Semaphores.md to replace this missing SingletonAttribute
@laurentAstonIf / @BenjaBobs - I believe this may not give you the existing experience you are looking for. For me, I am using a ServiceBusTrigger
and I want singleton-like behavior so that the messages are processed absolutely in order; therefore, only a single function instance should process the queue, which the existing Singleton
attributed enabled.
The code above is implemented as middleware, which will get invoked once a message has been peek+lock, so will either semaphore lock that message, or wait until it can, either way the message is peeked. Any additional process in running in parallel will peek+lock the next regardless. If the semaphore lock time is greater than max for message the lock will be released and another process can get it. Please correct me if I am wrong?
Therefore, there is a need for a worker-based Singleton
that ensures single execution before a lock dequeue/lock occurs. Maybe, it could be added as a property on the ServiceBusTrigger
(and others) if a generic Singleton
is not possible?
Finally, @fabiocav is there any progress? 🙏
To add to what @chullybun said, it will negatively impact all triggers. This approach is buffering requests/messages/etc, which will drive functions to scale out, translating into cost increase and errors. Also, for triggers such as ASB, for some it will result in unnecessary message dead-lettering. This is not a problem for middleware but for the host.
@laurentAstonIf @BenjaBobs FYI we're using the same solution and hope there will be a solid solution in the host in the near future.
I'm add a point where I'm avoiding azure functions where I can, and use MassTransit and Quartz.net instead to fill the same role. We mostly use timer triggers and queue triggers, so it was an easy jump for us.
We've implemented a singleton functions middleware like above (https://github.com/Azure/azure-functions-dotnet-worker/issues/938#issuecomment-1167329390). As chullybun and SeanFeldman stated this doesn't play nice with existing triggers and in the end only works on a servicebustrigger with a NewBatchThreshhold of 0.
@fabiocav is there any progress or guidance how to solve this? Is it possible to implement a custom servicebustrigger?
Also looking for a singleton implementation as the last piece of our Azure function migration
This item is for behavior within the middleware pipeline and accessing function metadata. Please track #274 for singleton discussions.
I'm trying to figure out if the called function code can be reflected or not. The passed into middleware
FunctionContext
has aFunctionDefinition
property. The property contains a string that represents the class and the function in the code. But I cannot use reflection to gain access to the type/method to reflect custom attributes.My question is does middleware suppose to be able to access the type hosting the function and if yes, would it be possible to break out the type and the method into two properties under
FunctionDefinition
instead of a singlecontext.FunctionDefinition.EntryPoint
.