September 3, 2014

Consuming Task-based Asynchronous Methods from OpenEdge ABL Part 2

It's been a very long time since the first part but I didn't want this series to remain incomplete. The solution presented in part 1 solved the basic problem and it's successfully used in production for 6 months now, but some topics where omitted for simplicity.

  • How to deal with Task<TResult>?
  • Exception Handling
  • Using Tasks in non UI applications, e.g. AppServer

In this post we will be looking at exception handling and Task<TResult>, that means async methods which "return" a value.

How to get the result

In part 1 the TaskCompleted event was only used for signaling, so the compulsory sender and EventArgs parameters were not of any interest. And it's just the same in this part - at least for the beginning. One might think that we need to pass the result to the event handler using a custom EventArgs class. Altough this has advantages, as we will see later, it's not necessary to access the task result. In fact we don't need any changes on the TaskAwaiter in order to work with Task<TResult>.

Let's do an example. We will be using the following fictive .NET method throughout the examples.

public Task<string> GetStringAsync()

And here's how to use the TaskAwaiter to await the result:

DEF VAR v-awaiter AS TaskAwaiter  NO-UNDO.
DEF VAR v-task    AS "Task<CHAR>" NO-UNDO.

v-awaiter = NEW TaskAwaiter().
v-awaiter:TaskCompleted:SUBSCRIBE("HandleResult").

v-task = GetStringAsync().
v-awaiter:Await(v-task).

PROCEDURE HandleResult:
    DEF INPUT PARAM sender AS System.Object.
    DEF INPUT PARAM args   AS System.EventArgs.

    MESSAGE v-task:Result
        VIEW-AS ALERT-BOX.
END PROCEDURE.

The important part is to store the return value (the task object) of the GetStringAsync method to a variable (v-task), which we can access in the event handling procedure (HandleResult). Using the await include presented at the end of part 1 would save us from some boilerplate code for the TaskAwaiter but the explicit version is better suited for illustration.

Behavior of the result property

In the above example, access to the task (and it's Result property) is not limited to the event handling method. We could as well use the the Result property before the TaskCompleted event is raised. Altough this would work, the TaskAwaiter would become useless because the Result property blocks until the Task is completed. This has the same effect as calling the Wait method on a task.

That doesn't meant using the Result property on a not completed task should never be done, but if you want to await the task, make sure to only access the result after the task is completed.

Exception Handling

If an exception is thrown during task execution which is not caught inside the task, it will not be magically thrown again on the awaiting thread. Instead it will remain unperceived as long as you dont't explicitly check the task's state or access the result property (which will throw an exception on a faulted task).

You can check for unhandeled exceptions using the IsFaulted property of the task. In case it's true the Exception is stored in the eponymous property. Beware that the real Exception is always wrapped with an AggregateException by the TPL. It's available via the InnerException property.

Here's an example displaying the message of the inner exception if the task is faulted:

IF v-task:IsFaulted THEN
    MESSAGE v-task:Exception:InnerException:Message
        VIEW-AS ALERT-BOX.

However AggregateExceptions can contain mulitple exceptions (Stackoverflow: When can an AggregateException contain more than 1 inner exception?):

DEF VAR v-i AS INT NO-UNDO.

IF v-task:IsFaulted THEN DO:

    DO v-i = 0 TO v-task:Exception:InnerExceptions:Count - 1:

        MESSAGE v-task:Exception:InnerExceptions[v-i]:Message
            VIEW-AS ALERT-BOX.
    END.
END.

In case of a Task<TResult> it's not necessary to check state as above. We can just access the Result property and handle potential exceptions the usual way.

Old-fashioned:

DEF VAR v-result AS CHAR NO-UNDO.

v-result = v-task:Result NO-ERROR.

IF ERROR-STATUS:ERROR THEN
    MESSAGE ERROR-STATUS:GET-MESSAGE(1)
        VIEW-AS ALERT-BOX.

Modern:

DEF VAR v-error  AS Progress.Lang.Error NO-UNDO.
DEF VAR v-result AS CHAR               NO-UNDO.

v-result = v-task:Result.

CATCH v-error AS Progress.Lang.Error:

    MESSAGE v-error:GetMessage(1)
        VIEW-AS ALERT-BOX.
END CATCH.

But, even in this case the AggregateException is thrown, not the real one.

If you have a non generic task (without Result) and want to use the normal error handling instead of checking the IsFaulted property, there is a trick to throw the exception. Just invoke Wait() on the task after it's completed. It will do nothing if the task is completed successfully, but it will throw the AggregateException if it's faulted:

DEF VAR v-error  AS Progress.Lang.Error NO-UNDO.

v-task:Wait().

CATCH v-error AS Progress.Lang.Error:

    MESSAGE v-error:GetMessage(1)
        VIEW-AS ALERT-BOX.
END CATCH.

Minimizing Scope

The solution presented above works, but we need a global variable to hold the task or an instance variable when used in a class. In most cases it would be sufficient to access the task in the event handling procedure / method (HandleResult in the example above). To achieve this we can change the EventArgs type of the TaskCompleted event to a custom type containing a Task property.

The extended TaskAwaiter including the new EventArgs class (changed lines in bold):

public class TaskAwaiter
{
    public event EventHandler<TaskCompletedEventArgs> TaskCompleted;

    public void Await(Task task)
    {
        task.ContinueWith(
            t => OnTaskCompleted(t),
            TaskScheduler.FromCurrentSynchronizationContext());
    }

    private void OnTaskCompleted(Task task)
    {
        var handler = TaskCompleted;
        if (handler != null)
            TaskCompleted(this, new TaskCompletedEventArgs(task));
    }
}

public class TaskCompletedEventArgs : EventArgs
{
    public Task Task { get; private set; }

    public TaskCompletedEventArgs(Task task)
    {
        Task = task;
    }
}

This allows us to use the task in the event handling procedure as follows:

PROCEDURE HandleResult:
    DEF INPUT PARAM sender AS System.Object.
    DEF INPUT PARAM args   AS TaskCompletedEventArgs.

    IF args:Task:IsFaulted THEN
        ...

To access the Result property of a Task<TResult> we need to cast:

DEF VAR v-result AS CHAR NO-UNDO.

v-result = CAST(args:Task, "Task<CHAR>"):Result.

Adding a state parameter

When working with events it's not unusual to offer a state parameter which allows the user to transfer an arbitrary object to the event handling code. Otherwise this would lead to global state as seen with the task above.

That's why I added an overload to the Await method which takes a state parameter:

public class TaskAwaiter
{
    public event EventHandler<TaskCompletedEventArgs> TaskCompleted;

    public void Await(Task task)
    {
        Await(task, null);
    }

    public void Await(Task task, object state)
    {
        task.ContinueWith(
            t => OnTaskCompleted(t, state),
            TaskScheduler.FromCurrentSynchronizationContext());
    }

    private void OnTaskCompleted(Task task, object state)
    {
        var handler = TaskCompleted;
        if (handler != null)
            TaskCompleted(this, new TaskCompletedEventArgs(task, state));
    }
}

public class TaskCompletedEventArgs : EventArgs
{
    public Task Task { get; private set; }
    public object State { get; private set; }

    public TaskCompletedEventArgs(Task task, object state)
    {
        Task = task;
        State = state;
    }
}

Here's an example how it could be used from OpenEdge:

DEF VAR v-awaiter AS TaskAwaiter NO-UNDO.

v-awaiter = NEW TaskAwaiter().
v-awaiter:TaskCompleted:SUBSCRIBE("HandleResult").
v-awaiter:Await(GetStringAsync(), "useful object").

PROCEDURE HandleResult:
    DEF INPUT PARAM sender AS System.Object.
    DEF INPUT PARAM args   AS TaskCompletedEventArgs.

    MESSAGE args:State
        VIEW-AS ALERT-BOX.

END PROCEDURE.

This may seem like overkill in a simple procedural example, but it's definitely useful when working with classes or in a more complex procedure than this.

However you can only pass simple ABL types like char and int (which have a .NET type mapping) and objects derived from System.Object. Instances of Progress.Lang.Object can't be referenced from .NET (...it would be possible with a wrapper class but this is another topic).

Adapting the await include

Using the TaskAwaiter without the await include presented in part 1 introduces a lot of boilerplate code. So we should change it to support the optional state parameter:

&SCOPED-DEFINE id {&SEQUENCE}   

DEF VAR v-awaiter{&id} AS TaskAwaiter NO-UNDO.

v-awaiter{&id} = NEW TaskAwaiter().
v-awaiter{&id}:TaskCompleted:Subscribe({2}).

&IF "{3}" = "" &THEN
v-awaiter{&id}:Await({1}).
&ELSE
v-awaiter{&id}:Await({1}, {3}).
&ENDIF

Then we end up with this in OpenEdge:

{await GetStringAsync() 'HandleResult' 'useful value'}

PROCEDURE HandleResult:
    DEF INPUT PARAM sender AS System.Object.
    DEF INPUT PARAM args   AS TaskCompletedEventArgs.

    MESSAGE args:State
        VIEW-AS ALERT-BOX.

END PROCEDURE.

Links

Consuming Task-based Asynchronous Methods from OpenEdge ABL Part 1

2 comments:

  1. Nice post,it's very informative.i found the best information.I updated my knowledge with this blog.it can help me to crack GIS jobs in Hyderabad.

    ReplyDelete
  2. PokerStars - Gaming & Slots at Aprcasino
    Join the fun at novcasino Aprcasino and gri-go.com play the best of the best PokerStars casino games including Slots, Blackjack, Roulette, Video https://febcasino.com/review/merit-casino/ Poker and apr casino more! https://sol.edu.kg/

    ReplyDelete