Practical tips while working with Azure Functions
1) You will probably need to create a lot of functions to solve a business problem. It is not wise to put every function inside one function app. It is better to categorize/segregate into different function apps, as a function app is a deployable unit.
2) Unless you have a strong reason not to, its better to use a "consumption plan" as opposed to "app service plan".
3) If your function app has functions with service bus trigger, make sure the following configuration is set -
a) The function app is set to "always on" to "true". (This is not needed in consumption plan though).
b) When multiple messages come to the service bus queue/topic, multiple parallel instances of your functions are created (default 15). Make sure you throttle it appropriately. Otherwise your app service can have spikes in memory/CPU consumption and there can be impact in downstream systems (example - The downstream system cannot accept 15 simultaneous update and might timeout). Below is the setting required in host.json to throttle -
"extensions": {
"serviceBus": {
"messageHandlerOptions": {
"autoComplete": false,
"maxConcurrentCalls": "5"
}
}
}
}
c) It is always good to have autoComplete in a service bus triggered function as "false". That way you can maintain transactions and Abandon/Deadletter/Complete as per the outcome/requirement. Below is some code snippet to do the same.
public class ProductReceivedFunction
{
private readonly ProductContext productDbContext;
private IConfiguration configuration;
private IHttpService httpService;
private ILogger logger;
public ProductReceivedFunction(IConfiguration configuration,ProductContext productDbContext, IHttpService httpService)
{
this.productDbContext = productDbContext;
this.configuration = configuration;
this.httpService = httpService;
}
[FunctionName("ProductReceivedFunction")]
public async Task Run([ServiceBusTrigger("productreceived", Connection = "sbqueueconnection-productreceived-listen")]string queueMessage, ILogger logger, MessageReceiver messageReceiver, string lockToken)
{
this.logger = logger;
try
{
this.logger.LogInformation($"C# ServiceBus queue trigger function processed message: {queueMessage}");
//do your stuff
await messageReceiver.CompleteAsync(lockToken);
}
catch (Exception ex)
{
this.logger.LogError(ex, "Error in processing");
await messageReceiver.DeadLetterAsync(lockToken);
}
}
}
d) Never inject IConfiguration in startup.cs of a function app. It is injected by default. Infact if you explicitly inject, it will mess up with autoComplete=false setting. It will try to complete the message (when actually you have already done it in code manually) and give a messagelocklost exception
e) Double check that in all paths (if, else, try, catch etc) you are completing/deadlettering/abandoning the message and that too only once. If its done twice, you will get a messagelocklost exception.
4) Set your queue TTL and lock duration appropriately. In my experience, the lock duration of 1 minute should be good for most scenarios. The TTL however depends on your design. If there is an upstream process that runs periodically and copies the state (as messages in a queue), the TTL of the queue should be <= the upstream process frequency, otherwise there can be duplicate messages. (In such scenarios, the Receiver should also be made idempotent).
5) Always integrate your Function app with App insights. Sample kusto queries for diagnosis -
traces | where operationName == '<FunctionName>'
exceptions | take 10
traces | where severityLevel == 3 // for Errors
6) Always perform load testing on your functions and monitor the app service plan metrics like - CPU percentage/Memory percentage.
7) Monitor your function app using Azure Monitor. You can monitor the deadletter queue, main queue (if 100 msgs are there for over 10 minutes). You can also monitor on the results of log analytics workspace query (point 5 above).