Revolutionizing Azure Functions with Golang
Harnessing the Power of Custom Handlers for Timer Triggers

By Daniel Laczkowski


Why Azure Functions?

If you need isolated scalability, cloud functions seem to be the perfect choice. While designing AssortMail.com I was comparing different ways in which I could routinely refresh Oauth tokens.

Sure, I could use a cron job on the server, but then I cannot scale this service individually.

Yes, I could use an internal goroutine like so, but then an isolated service is coupled with my main web server. Not ideal.

ticker := time.NewTicker(interval)
go func() {
    for range ticker.C {
        //Refresh Oauth Tokens
    }
}()

Naturally this lead me to explore alternative methods, so I noticed Azure's generous "1 Million Free Monthly Executions", and decided to give it a try for the potential of hosting other independent parts of my infrastructure.


Setting up

Omitting the checklist setup, assume a standard function app with a custom handler. For my purposes, I also added the line shown below to our "hosts.json" file so that our executable may receive any http request.

"enableForwardingHttpRequest": true

The request route for timer functions is equal to the name of your timer function, with the request method being POST.

app.Post("/TimerTriggerTest", TimerTest) //Go Fiber

It is important to note that to stop the function from erroneously reporting a failed execution, it should return an "InvokeResponse"

InvokeResponse struct {
    Outputs     map[string]interface{}
    Logs        []string
    ReturnValue interface{}
}

The function would still work as normal otherwise, but it is better to avoid an ominous "384 failed executions in the last 30 days" so you can track the real failed executions.


Using environment variables

Environment variables are standard practice to avoid credential leakage, plus it avoids recompilation if you need to change the value. Once again in our "host.json" file, we can specify arguments to pass at runtime to our executable like so.


{
    "version": "2.0",
    "logging": {},
    "extensionBundle": {},
    "customHandler": {
        "description": {
        "defaultExecutablePath": "server",
        "workingDirectory": "",
        "arguments": ["--MICROSOFT_SECRET_PERSONAL", "%MICROSOFT_SECRET_PERSONAL%"]
        },
        "enableForwardingHttpRequest": true
    }
}

We then need to access our Function App "Application Settings" to set the value of our arguments, in the pictured sub menu.

Azure Application Settings configuration menu showing custom environment variables

Challenges Faced and Mistakes Made

My initial excitement lead me to build and test on live functions directly, and so for a short while, I was puzzled as to why my function was not behaving correctly. It turns out I was returning before actually handling the response.

Tip: test your program locally first to differentiate cloud mistakes from program mistakes

On the positive side, this lead me to discover the various debugging tools Azure have available, including every developer's favourite method, printing straight to the console log...


Tracking failed executions in Azure Function

To access any printed logs in relation to a failed execution, I created the following query to use in Azure under "Function App -> Monitoring -> Logs"

let targetTimestamp = toscalar(
    traces
    | where operation_Id == 'your_operation_id'
    | project timestamp
);
traces 
| where timestamp > targetTimestamp
| project timestamp, message, severityLevel
| order by timestamp asc
| limit 20

By using any operation ID in this query, you can view the next logs which were printed immediately after. If you expect your logs to be printed later, you may log a unique request ID (to be generated by your program) on execution, and then append it to any following logs for an easy search.

Screenshot of Azure Function App Monitoring showcasing a failed execution's operation id. A query using Kusto Query Language finding and displaying logs related to a given operation id.

Now we can easily debug our cloud function.


Limitations and Summary

Considering that potentially sparing a few seconds for cold starts here is not a problem and neither is any latency, the main downside of this approach (for Oauth token renewal) is that fresh tokens may be unnecessarily renewed when operating on a simple timer, suggesting that cloud functions may be more suited to webhooks instead where the function only runs when invoked.

Alternatively, instead of using a timer function, access tokens may instead be renewed after every use, effectively outsourcing the task to a standard http trigger function.

Likewise, if a bulk timer function is not programmed to deal with a token renewal failing such as by adding it to a queue of retries, it may never be renewed, or worse, compromise the rest of the tokens waiting to be processed.

The true limitation here is the lack of benchmarks justifying my use of Go, instead of going for an "officially supported" language such as Javascript, which could somewhat simplify the Azure learning curve.

Otherwise, we now have a function guaranteed to run in our set interval, completely independent from our other infrastructure possibly leading to higher reliability, if implemented correctly.


Thank you!

Quick note, I'd like to thank you for reaching (and hopefully enjoying) my first technical post. If you have any feedback whatsoever, I would love to hear it from you on LinkedIn, and would greatly appreciate you sharing my post.

More content:

Redis VSS in Go