1

I am using TaskParallelLibrary DataFlow combined with Try library designed by Stephen Cleary (https://github.com/StephenCleary/Try) to achieve what is called "railroad programming" so I could pass Exception data down the pipe. I would like to know to if it is somehow possible in ActionBlock to get some context of what or (in my case) exactly which item caused the Exception? Here is small sample code:

public async Task TestRailroadException(List<int> constructIds)
{
    var downloadBlock = new TransformBlock<int, Try<int>>(
        construct => Try.Create(() =>
    {
        //ThisMethodMyThrowException();
        return 1;
    }));

    var processBlock = new TransformBlock<Try<int>, Try<int>>(
        construct => construct.Map(value =>
    {
        //ThisMethodMyAlsoThrowException();
        return 1;
    }));

    var resultsBlock = new ActionBlock<Try<int>>(construct =>
    {
        if (construct.IsException)
        {
            var type = construct.Exception.GetType();
            //Here it would be nice to know which item(id) was faulted.
        }
    });
    downloadBlock.LinkTo(processBlock, new DataflowLinkOptions
        { PropagateCompletion = true });
    processBlock.LinkTo(resultsBlock, new DataflowLinkOptions
        { PropagateCompletion = true });
    foreach (var constructId in constructIds)
    {
        await downloadBlock.SendAsync(constructId);
    }

    downloadBlock.Complete();
    await resultsBlock.Completion;
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
niks
  • 579
  • 1
  • 3
  • 14
  • Take a look at this: [AsyncLocal values not correct with TPL Dataflow](https://stackoverflow.com/questions/58179359/asynclocal-values-not-correct-with-tpl-dataflow). TL;DR the [`AsyncLocal`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.asynclocal-1) class cannot be used for passing ambient context data along with each message. So there is no alternative to passing the context data manually from block to block, by using tuples or custom classes or structs as messages. – Theodor Zoulias May 26 '20 at 23:42
  • @Theodor. Thank you for answer. Do you think catching errors in each block, adding `exception.Data` with faulted items id and then re-throwing them to catch them again in `resultsBlock` is a good idea? That way I can get to know who caused `exception` and I don't have to repeat all `catch` logic in each block. It is not very beautiful solution but it gets the job done. – niks May 27 '20 at 11:12
  • Yeap, it's OK. But using Stephen Cleary's [Try](https://github.com/StephenCleary/Try) library may be cleaner. It is also technically superior, unless you are willing to learn about the [`ExceptionDispatchInfo`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.exceptionservices.exceptiondispatchinfo) class, and use this instead of the naked `Exception` for storing the exceptions. :-) – Theodor Zoulias May 27 '20 at 11:25
  • @Theodor Yes, yes I am using Stephen's Try library. But the main problem is that when I unwrap `exception` in `resultsBlock` I have no idea about which item caused it. If I add data to it and re-throw as described in previous comment, then I can be sure which item exactly was faulted. – niks May 27 '20 at 12:05
  • 1
    How about `var downloadBlock = new TransformBlock)>`? The result is a `ValueTuple` with two members, the original item and its result, with the result wrapped in a `Try`. Then continue passing the original item from block to block, using tuples as `TInput` and `TOutput`. – Theodor Zoulias May 27 '20 at 12:20
  • 1
    @Theodor Ah! But of course! So simple, yet brilliant! Thank you! If you post this as an answer along with a small sample I will most definitely accept it! – niks May 27 '20 at 15:35

1 Answers1

1

You could use ValueTuple<TId, Try<TResult>> structs as messages for the pipeline, but it may be slightly more convenient to create a custom wrapper of the Try class that also holds the id. Since this wrapper will have two type parameters, it is allowed to name it Try as well:

public readonly struct Try<TId, TResult>
{
    public static Try<TId, TResult> Create(TId id, Func<TResult> func)
        => new Try<TId, TResult>(id, Try.Create(func));

    public static async Task<Try<TId, TResult>> Create(TId id,
        Func<Task<TResult>> func)
        => new Try<TId, TResult>(id, await Try.Create(func).ConfigureAwait(false));

    public readonly TId Id { get; }
    public readonly Try<TResult> Result { get; }

    private Try(TId id, Try<TResult> result) { Id = id; Result = result; }

    public Try<TId, TNewResult> Map<TNewResult>(Func<TResult, TNewResult> func)
        => new Try<TId, TNewResult>(Id, Result.Map(func));

    public async Task<Try<TId, TNewResult>> Map<TNewResult>(
        Func<TResult, Task<TNewResult>> func)
        => new Try<TId, TNewResult>(Id, await Result.Map(func).ConfigureAwait(false));
}

You could then use it like this:

var downloadBlock = new TransformBlock<int, Try<int, int>>(
    construct => Try<int, int>.Create(construct, async () =>
{
    await SometimesThrowsAsync();
    return 1;
}));

var processBlock = new TransformBlock<Try<int, int>, Try<int, int>>(
    construct => construct.Map(async value =>
{
    await SometimesThrowsAsync();
    return 1;
}));

var resultsBlock = new ActionBlock<Try<int, int>>(construct =>
{
    if (construct.Result.IsException)
    {
        var type = construct.Result.Exception.GetType();
        //Log that the {construct.Id} has failed.
    }
});
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Very nice wrapper! However, I am struggling with how to implement this if `ThisMethodMyThrowException` is async method that should be awaited. I just can't get it to work. Could you please share any tips? – niks May 28 '20 at 08:39
  • @niks I updated the `Try` with async-friendly methods. It should be integrated OK now with async TPL Dataflow blocks. – Theodor Zoulias May 28 '20 at 09:47
  • 1
    Thank you once again! Took me some time to grasp it, `TPL` and `async` stuff is not my strongest side..I wouldn't be able to come up with such a solution on my own! Once again I am simply amazed how this site works and how people from all around the world are willing to invest their time in helping other people:) Efcharistó! – niks May 29 '20 at 06:37
  • @niks Ευχαρίστησή μου my friend. async/await is awesome! – Theodor Zoulias May 29 '20 at 06:55