Dealing with Long Running Activities

When you run a long-running activity the workflow become “unresponsive”. Even if the UI might not be affected, it can cause problem in e.g. TFS build: basically the build cannot be interrupted during this long-running activity. We will put focus on that now.

As you may already know, our key will be calling these activities asynchronously, in a separate thread. You can read further on this topic on many sites. E.g.:

Using AsyncCodeActivity – The APM pattern

Yes, this pattern is pretty handy with the AsyncCodeActivity class. You can just simply create your derived class like this:

public class SimpleAsyncCodeActivity AsyncCodeActivity
{
    private const int BufferSize = 255;

    private byte[] _buffer;
    private FileStream _stream;

    public InArgument<string>  InputFile { getset; }
    public OutArgument<string> Result    { getset; }

    protected override IAsyncResult BeginExecute(AsyncCodeActivityContext context,
                                                                    AsyncCallback callback, object state)
    {
        var inputFileName = context.GetValue(InputFile);
        _buffer = new byte[BufferSize];
        _stream = new FileStream(inputFileName, FileMode.Open, FileAccess.Read);

        var cnt = Math.Max((int)_stream.Length, BufferSize);

        return _stream.BeginRead(_buffer, 0, cnt, callback, state);
    }

    protected override void EndExecute(AsyncCodeActivityContext context, IAsyncResult result)
    {
        _stream.EndRead(result);

        context.SetValue(Result, String.Join("", _buffer));
    }
}

Event-based Asynchronous Pattern

Sorry I did not spend effort on this topic. That might be not the most straightforward. Maybe I will go around this also, however the below described Task-based  way looks more preferred way – you may consider using this instead.

Task-based Asynchronous Pattern

Looks the most interesting. 🙂 I implemented it by creating an abstract activity class, based on AsyncCodeActivity.

public abstract class TaskBasedActivity<T> : AsyncCodeActivity<T>
{
    private readonly CancellationTokenSource _cancellationTokenSource;

    public OutArgument<Exception> Error { getset; }

    protected TaskBasedActivity()
    {
        _cancellationTokenSource = new CancellationTokenSource();
    }

    protected sealed override IAsyncResult BeginExecute(AsyncCodeActivityContext context, AsyncCallback callback,
                                                    object state)
    {
        LoadArguments(context);

        var task = new Task<T>(o => Execute(_cancellationTokenSource.Token, state),
                                state, _cancellationTokenSource.Token);

        task.ContinueWith(s => callback(s));
        task.Start();

        return task;
    }

    protected sealed override T EndExecute(AsyncCodeActivityContext context, IAsyncResult result)
    {
        var task = result as Task<T>;
        if (task == nullthrow new InvalidOperationException(String.Format("Activity returned with invalid result ({0}).", result.GetType().Name));

        context.SetValue(Error, task.Exception);

        if (task.IsCompleted && !task.IsFaulted && !task.IsCanceled)
            return task.Result;

        return NullResultValue;
    }

    protected override void Cancel(AsyncCodeActivityContext context)
    {
        base.Cancel(context);
        _cancellationTokenSource.Cancel(false);
    }

    protected abstract T Execute(CancellationToken cancellationToken, object state);
    protected abstract void LoadArguments(AsyncCodeActivityContext context);

    protected abstract T NullResultValue { get; }
}

The usage then looks like this:

public class SimpleTaskActivity TaskBasedActivity<int>
{
    private int _data;

    public InArgument<int> Data { getset; }

    protected override int NullResultValue
    {
        get return -1; }
    }

    protected override void LoadArguments(AsyncCodeActivityContext context)
    {
        _data = context.GetValue(Data);
    }

    protected override int Execute(CancellationToken cancellationToken, object state)
    {
        // ... do what you want ..

        // handle cancellation request like this:        
        cancellationToken.ThrowIfCancellationRequested();

        // .. or like this:
        if (cancellationToken.IsCancellationRequested) return NullResultValue;

        int result = 1;
        return result;
    }
}

That is not bad I think. However few things might need some explanation:

  • Activity parameters could be loaded only  synchronously, as after BeginExecute() the context object is being destroyed. In order to do this, they are loaded LoadArguments() “in advance”, and stored in local fields.
  • Calling the ContinueWith() method of the task ensures, that the workflow completition is realized, and EndExecute() is called accordingly.
  • NullResultValue is involved, which is returned by the EndExecute() method, when failure/cancellation happens. (This way could be nicer, any suggestion is welcomed.)

Advertisements
About

Mainly a developer from the .Net and also the PHP world. Many others staffz are attached also.

Tagged with: , , , , ,
Posted in .Net, TFS Build, Workflow Foundation

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: