Orchestrating workflows in c# with Durable Task Framework

About Me

Blog: http://blog.codenova.pl/

Twitter: @Kamil_Mrzyglod

GitHub: https://github.com/kamil-mrzyglod

StackOverflow: https://stackoverflow.com/users/1874991/kamo

LinkedIn: www.linkedin.com/in/kamil-mrzygłód-31470376

Scenario

Account 1

Account 2

Database 1

Database 2

Distributed Transaction

Scenario

Debit amount from Account A

Successful?

Credit amount to Account B

Successful?

Yes

No

Yes

No

Undo debit from Account A

Durable Task Framework

Core Concepts

Durable Task Framework

Service Bus

Durable Task Framework

How does it work?

Task Hub Worker

Task Orchestrations

Task Activity

Task Activity

Task Hub Client

Task Hub

Task Activity

Durable Task Framework

Orchestration example

public class EncodeVideoOrchestration : TaskOrchestration<string, string>
{
    public override async Task<string> RunTask(OrchestrationContext context, 
                                                 string input)
    {
        string encodedUrl = 
              await context.ScheduleTask<string>(typeof (EncodeActivity), input);
        await context.ScheduleTask<object>(typeof (EmailActivity), input);
        return encodedUrl;
    }
}

Orchestrations are single-threaded!

Durable Task Framework

Activities example

public class EncodeActivity : TaskActivity<string, string>
{
    protected override string Execute(TaskContext context, string input)
    {
        Console.WriteLine("Encoding video " + input);
        // TODO : actually encode the video to a destination
        return "http://<azurebloblocation>/encoded_video.avi";
    }
}

public class EmailActivity : TaskActivity<string, object>
{
    protected override object Execute(TaskContext context, string input)
    {
        // TODO : actually send email to user
        return null;
    }
}

Guaranteed to called at-least-once!

Durable Task Framework

Task hub worker & Client example

string serviceBusConnString 
    = "Endpoint=sb://<namespace>.servicebus.windows.net/;SharedSecretIssuer=[issuer];SharedSecretValue=[value]";

TaskHubWorker hubWorker = new TaskHubWorker("myvideohub", serviceBusConnString)
    .AddTaskOrchestrations(typeof (EncodeVideoOrchestration))
    .AddTaskActivities(typeof (EncodeActivity), typeof (EmailActivity))
    .Start();
string serviceBusConnString 
    = "Endpoint=sb://<namespace>.servicebus.windows.net/;SharedSecretIssuer=[issuer];SharedSecretValue=[value]";

TaskHubClient client = new TaskHubClient("myvideohub", serviceBusConnString);
client.CreateOrchestrationInstance(typeof (EncodeVideoOrchestration), "http://<azurebloblocation>/MyVideo.mpg");

Orchestrations

Constraints

Task Orchestration

Activity 1

Activity 2

Activity 3

Replay

Replay

Replay

Execution History

Orchestrations

Error handling

public class DebitCreditOrchestration : 
    TaskOrchestration<object, DebitCreditOperation>
{
    public override async Task<object> RunTask(OrchestrationContext context, 
        DebitCreditOperation operation)
    {
        bool failed = false;
        bool debited = false;
        try
        {
            await context.ScheduleTask<object>(typeof (DebitAccount),
                            new Tuple<string, float>(operation.SourceAccount, operation.Amount));
            debited = true;

            await context.ScheduleTask<object>(typeof(CreditAccount),
                            new Tuple<string, float>(operation.TargetAccount, operation.Amount));
        }
        catch (TaskFailedException exception)
        {
            failed = true;
        }

        if (failed)
        {
            if (debited)
            {
                // can build a try-catch around this as well, in which case the 
                // orchestration may either retry a few times or log the inconsistency for review
                await context.ScheduleTask<object>(typeof(CreditAccount),
                            new Tuple<string, float>(operation.SourceAccount, operation.Amount));
            }
        }
        return null;
    }
}

Orchestrations

Automatic retries

public class GetQuoteOrchestration : TaskOrchestration<string, string>
{
    public override async Task<string> RunTask(OrchestrationContext context, string input)
    {
        // retry every 10 seconds upto 5 times before giving up and bubbling up the exception
        RetryOptions retryOptions = new RetryOptions(TimeSpan.FromSeconds(10), 5);
        await context.ScheduleWithRetry<object>(typeof (GetQuote), retryOptions, null);
        return null;
    }
}

Orchestrations

durable timers

public class EncodeVideoOrchestration : TaskOrchestration<string, string>
{
    public override async Task<string> RunTask(OrchestrationContext context, string input)
    {
        string encodedUrl = await context.ScheduleTask<string>(typeof (EncodeActivity), input);
        await context.CreateTimer(context.CurrentUtcDateTime.Add(TimeSpan.FromDays(1)), "timer1");
        await context.ScheduleTask<object>(typeof (EmailActivity), input);
                
        return encodedUrl;
    }
}
public class BillingOrchestration : TaskOrchestration<string, string>
{
    public override async Task<string> RunTask(OrchestrationContext context, string input)
    {
        for (int i = 0; i < 10; i++)
        {
            await context.CreateTimer(context.CurrentUtcDateTime.Add(TimeSpan.FromDays(1)), "timer1");
            await context.ScheduleTask<object>(typeof (BillingActivity));
        }
        return null;
    }
}

Orchestrations

durable timers

public class GetQuoteOrchestration : TaskOrchestration<string, string>
{
    public override async Task<string> RunTask(OrchestrationContext context, string input)
    {
        Task timer = context.CreateTimer(
            context.CurrentUtcDateTime.Add(TimeSpan.FromSeconds(5)), "timer1");
        Task getQuote = context.ScheduleTask<object>(typeof(GetQuote));
        Task winner = Task.WhenAny(timer, getQuote);
        if (timer.IsCompleted)
        {
            // request timed out, do some compensating action
        }
        else
        {
            // use getQuote task result
        }
        return null;
    }
}

Orchestrations

eternal orchestrations

public class CronOrchestration : TaskOrchestration<string, int>
{
    public override async Task<string> RunTask(OrchestrationContext context, int intervalHours)
    {
        // bounded loop
        for (int i = 0; i < 10; i++)
        {
            await context.CreateTimer<object>(
                context.CurrentUtcDateTime.Add(TimeSpan.FromHours(intervalHours)), null);
            // TODO : do something interesting 
        }

        // create a new instance of self with the same input (or different if needed)
        context.ContinueAsNew(intervalHours);
        return null;
    }
}

Orchestrations

external events

public class GetQuoteOrchestration : TaskOrchestration<string, string>
{
    TaskCompletionSource<object> getPermission = new TaskCompletionSource<object>(); 

    public override async Task<string> RunTask(OrchestrationContext context, string input)
    {
        await getPermission.Task;
        await context.ScheduleTask<object>(typeof (GetQuote), null);
        return null;
    }

    public override void OnEvent(OrchestrationContext context, string name, string input)
    {
        getPermission.SetResult(null);
    }
}
TaskHubClient client = new TaskHubClient("test", serviceBusConnString);
OrchestrationInstance instance = client.CreateOrchestrationInstance(typeof (EncodeVideoOrchestration), 
                        "http://<azurebloblocation>/MyVideo.mpg");

client.RaiseEvent(instance, "dummyEvent", "dummyData");

Durable Functions

example

public static async Task<List<string>> Run(DurableOrchestrationContext context)
{
    var outputs = new List<string>();

    outputs.Add(await context.CallFunctionAsync<string>("E1_SayHello", "Tokyo"));
    outputs.Add(await context.CallFunctionAsync<string>("E1_SayHello", "Seattle"));
    outputs.Add(await context.CallFunctionAsync<string>("E1_SayHello", "London"));

    // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
    return outputs;
}

Questions?

deck

By kamil_mrzyglod

deck

  • 759