Durable Functions - basic concepts and justification

Recently team responsible for Azure Functions announced, that new cool feature is entering an alpha preview stage - Durable Functions. Because this introduces a completely new way of thinking about serverless in Azure, I decided to go in-depth and prepare a few blog posts regarding both new capabilities and the foundations of Durable Functions.

Concepts

Conceptually Durable Functions are something, what forces you to rethink what you've already learnt about serverless in Azure. When writing common functions like inserting something into a database or passing a message to a queue, you've always been trying to avoid storing state and perform all actions as quickly as possible. This had many advantages:

  • it was easy to write a simple function, which performs basic operations without preparing boilerplate code
  • dividing your module into small services really helped during maintaining your solution
  • scaling was quite simple and unequivocal

All right - it seems that we had all we needed, why one tries to introduce a completely different concept, which raises learning curve of Functions? 

Communication

Normally if you want to communicate between functions, you will have to use queues. It's a perfectly valid solution and in simple scenarios the whole solution won't be cumbersome. However if you're creating a bigger system with several functions orchestrating work between each other, sooner than later you'll hit a wall - communication using queues will become a bottleneck of your solution and maintenance will become a nightmare. 

Additionally - more advanced scenarios(like fan-in/fan-out) are ridiculously hard to achieve.

What about stateless?

For some people concept of introducing a state into functions destroys what is best about them - possibility to scale out seamlessly and treating them as independent fragments of your system. In fact, you have to distinguish "traditional" functions and durable ones - they have different use cases and reasoning between differs a lot. The former should be used as a reaction to an event, they're ideal when you have to take an action in answer to a message. The latter are more sublime during adoption - you'll use them for orchestrating workflows and pipelines, which let you easily perform an end-to-end processing of a message.

Pricing

One more thing considering Durable Functions is pricing, mostly because it is what makes serverless so interesting. In Durable Functions it doesn't change - you'll still pay only for the time, when a function executes - when a function awaits for a result of running other functions, no cost is allocated here. This is thanks to the fact, that once a task is scheduled, execution of a function returns to the Durable Task Framework layer and waits for further actions there.

I strongly recommend you to take a look try something with Durable Functions. This feature is still in an early preview so it might be unstable in some way, but it gives so many possibilities now, that it's really worth a try. You can find more info here: Alpha Preview for Durable Functions.

You shall not forget - reminding about pull requests in VSTS #2

In the previous post I presented you how easily we can determine whether a PR has been reviewed or not. Now we'll create a real solution, which will send a notification to a Slack channel containing information about waiting pull request.

Creating an Azure Function

To handle our solution we'll develop a function, which will do following:

  • call VSTS API to check whether there're active PRs
  • filter those PRs, which haven't been reviewed during a specific interval
  • send a message to a Slack channel with specific information regarding forgotten PRs

Now let's create a function. Go to Azure Portal and create a new Function App(if you don't have one). Once created add a function as a TimerTrigger(schedule doesn't matter in this moment). Once you're done, you should get following function:

Now - to authenticate our calls to the VSTS API we'll need a personal access token. To create one go to the instance of VSTS you're going to use and go to Security screen.

On the left you should see Personal access tokens tab. Go there and create a new personal access token, which we'll use in our function.

Once we have a PAT we can write add some code to our function - for this moment we'll try to check whether there're active PRs.

Fetching active PRs from VSTS

This is a simple task - we have to call an API with a generated PAT. Consider following code:

/
using System;
using System.Net;
using System.Net.Http.Headers;

public static async Task Run(TimerInfo myTimer, TraceWriter log)
{
    log.Info($"C# Timer trigger function executed at: {DateTime.Now}");

    try
    {
        var personalaccesstoken = "THIS_IS_YOUR_PAT";

        using (HttpClient client = new HttpClient())
        {
            client.DefaultRequestHeaders.Accept.Add(
                new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));

            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
                Convert.ToBase64String(
                    System.Text.ASCIIEncoding.ASCII.GetBytes(
                        string.Format("{0}:{1}", "", personalaccesstoken))));

            using (HttpResponseMessage response = client.GetAsync(
                        "https://{instance}.visualstudio.com/{project}/_apis/git/repositories/{repository}/pullRequests?api-version=3.0").Result)
            {
                response.EnsureSuccessStatusCode();
                string responseBody = await response.Content.ReadAsStringAsync();
                log.Info(responseBody);
            }
        }
    }
    catch (Exception ex)
    {
        log.Info(ex.ToString());
    }
}

When you replace dummy values with values of your own, you should receive in the Logs window a serialized response containing active pull requests(of course if there's any). So far so good - let's try to find if any requires our attention.

Finding outdated PRs

To find outdate PRs we'll have to change our code a little - for now we have a raw string and are unable to query any of its properties. For the purpose of this post I decided to use a dynamic object, just not to trouble with creating a DTO for a response. To do so just create responseBody variable to:

/
dynamic data = await response.Content.ReadAsAsync<object>();

No we can easily query PRs and find those, which haven't been reviewed during a specific interval(let's say 24 hours). Let's add following code to our function:

/
using (HttpResponseMessage response = client.GetAsync(
			"https://{instance}.visualstudio.com/{project}/_apis/git/repositories/{project}/pullRequests?api-version=3.0").Result)
{
	response.EnsureSuccessStatusCode();
	dynamic data = await response.Content.ReadAsAsync<object>();

	foreach(var pr in data.value) {
		foreach(var reviewer in pr.reviewers) {
			if(reviewer.vote == 0 && (DateTime.Now - DateTime.Parse(pr.creationDate.ToString())).Hours > 24) {
				log.Info($"Reviewer {reviewer.displayName} still hasn't reviewed a PR!");
			}
		}
	}
}

With the above code we're able to find reviewers, which haven't got a chance to take a look and review a pull request. In the last part we'll send this information to a Slack channel.

Notifying in a Slack channel

To send something to a Slack channel we have to set up a webhook integration which is described here. Once you have added a webhook to your channel and got an endpoint, which can be used to send messages to a channel, we can extend our function. Here you have the complete code:

/
using System;
using System.Net;
using System.Net.Http.Headers;

public static async Task Run(TimerInfo myTimer, TraceWriter log)
{
    log.Info($"C# Timer trigger function executed at: {DateTime.Now}");

    try
    {
        var personalaccesstoken = "YOUR_PAT";

        using (HttpClient client = new HttpClient())
        {
            client.DefaultRequestHeaders.Accept.Add(
                new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));

            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
                Convert.ToBase64String(
                    System.Text.ASCIIEncoding.ASCII.GetBytes(
                        string.Format("{0}:{1}", "", personalaccesstoken))));

            dynamic data = null;
            using (HttpResponseMessage response = await client.GetAsync(
                        "https://{instance}.visualstudio.com/{project}/_apis/git/repositories/{repository}/pullRequests?api-version=3.0"))
            {
                response.EnsureSuccessStatusCode();
                data = await response.Content.ReadAsAsync<object>();
            }

            foreach(var pr in data.value) {
                foreach(var reviewer in pr.reviewers) {
                    if(reviewer.vote == 0 && (DateTime.Now - DateTime.Parse(pr.creationDate.ToString())).Hours > 24) {
                        var textToSend = $"Reviewer {reviewer.displayName} still hasn't reviewed a PR!";

                        await client.PostAsync("https://hooks.slack.com/{...}", new StringContent("{\"text\":\"" + textToSend + "\"}"));
                    }
                }
            }
        }
    }
    catch (Exception ex)
    {
        log.Info(ex.ToString());
    }
}

Summary

VSTS and its API give you many flexibility and are really fun to play with. The solution presented here is not ideal - there's still a possibility to improve things. Anyway, I'd like to encourage you to your own experiments with VSTS and different integrations - you'll be surprised what you can achieve with a few lines of code.