0

I am trying to create my first TPL DataFlow service in a Windows Form application using .Net 4.5.

In overview, the application loads and parses some data from a .csv text file and, after some processing/interrogation generates a SQL database record (using an Entity Framework data access layer). Once all the records have been loaded, the application generates a results text file by iterating through the generated database records. Here is the workflow...

loadListFile: iterates through text file and posts to...
   + createStateRecord: generates the database records
      + setStateRecordStatus: updates the UI for each record

WhenAll records have been created...

generateReport: iterates through the database and posts to
    + writeReport: writes a summary record to a text file
       + setReportStatus: updates the UI to show report progress

I appreciate that I could structure the relationship between these two tasks as part of the workflow, but for now I want to keep them separate

The problem I am having is that the application/TPL workflow appears to work correctly on the first pass, but if the UI is reset and the process is run a second time, the writeReport.Completion method is never called - even though the writeReport.Complete() method is called.

Here is the full method (which is invoked from a button 'click' even handler on the form).

private async void ProcessData()
{
    /********************************************************************/
    /** Create State records                                            */
    /********************************************************************/
    var createStateRecord = new TransformBlock
        <CheckData, CheckResult>(checkData =>
        {
            return DalServices.CreateStateRecord(checkData);
        }, new ExecutionDataflowBlockOptions
        {
            CancellationToken = _cancellationToken.Token,
            MaxDegreeOfParallelism = 10
        });

    var setStateRecordStatus = new ActionBlock <CheckResult>(
        result =>
        {
            Interlocked.Increment(ref _noRecordsProcessed);
            if (!result.Success)
                Interlocked.Increment(ref _noRecordsFailed);

            pbarLoad.Minimum = 0;
            pbarLoad.Maximum = _noRecordsInFile;
            pbarLoad.Value = _noRecordsProcessed;
        }, new ExecutionDataflowBlockOptions
        {
            CancellationToken = _cancellationToken.Token,
            TaskScheduler = TaskScheduler
                .FromCurrentSynchronizationContext()
        });

     var loadListFile = new ActionBlock<string>(
        listFilePath =>
        {
            using (var reader =
                new DataProviderService(_listFileInfo.FullName))
            {
                _noRecordsProcessed = 0;
                _noRecordsInFile = reader.NoRows;

                foreach (var item in reader)
                {
                    createStateRecord.Post(new CheckData(item));
                }
                createStateRecord.Complete();
            }
        },
        new ExecutionDataflowBlockOptions
        {
            CancellationToken = _cancellationToken.Token
        });

    /*
     * Link StateRecord tasks
     */
    createStateRecord.LinkTo(setStateRecordStatus);
    createStateRecord.Completion.ContinueWith(t =>
        {
            if (t.IsFaulted)
            {
                ((IDataflowBlock) setStateRecordStatus)
                    .Fault(t.Exception);
                return;
            }
            setStateRecordStatus.Complete();
        }, _cancellationToken.Token);

    /********************************************************************/
    /** Reporting tasks                                                 */
    /********************************************************************/
    var writeReport = new TransformBlock<ReportData, State>(
        reportData =>
        {
            using (
                var writer = new StreamWriter(
                reportData.ResultsFilePath, true))
            {
                writer.WriteLine(reportData.State.ToString());
            }
            return reportData.State;
        },
            new ExecutionDataflowBlockOptions
            {
                CancellationToken = _cancellationToken.Token,
                MaxDegreeOfParallelism = 1
            });

        var setReportStatus = new ActionBlock<State>(
            result =>
            {
                Interlocked.Increment(ref _noRecordsReported);
                pbarReport.Minimum = 0;
                pbarReport.Maximum = _noRecordsInFile;
                pbarReport.Value = _noRecordsReported;
            },
            new ExecutionDataflowBlockOptions
            {
                CancellationToken = _cancellationToken.Token,
                TaskScheduler = TaskScheduler
                    .FromCurrentSynchronizationContext()
            });

        var generateReport = new ActionBlock<string>(
            listFilePath =>
            {
                using (var stateService = DalService.GetStateService())
                {
                    var resultsFilePath = listFilePath + ".results";
                    foreach (var state in stateService.GetStates())
                    {
                        writeReport.Post(
                        new ReportData
                        {
                            State = state,
                            ResultsFilePath = resultsFilePath
                        });
                    }
                    // This alwaus gets called
                    writeReport.Complete();
                }
            },
            new ExecutionDataflowBlockOptions
            {
                CancellationToken = _cancellationToken.Token
            });

    /*
     * Link Reporting tasks
     */
    writeReport.LinkTo(setReportStatus);
    writeReport.Completion.ContinueWith(t =>
        {
            // This only get called on first run!
            if (t.IsFaulted)
            {
                ((IDataflowBlock) setStateRecordStatus).Fault(
                    t.Exception);
                return;
            }
            setReportStatus.Complete();
        }, _cancellationToken.Token);

    /********************************************************************/
    /** Run the tasks                                                   */
    /********************************************************************/
    try
    {
        loadListFile.Post(_listFileInfo.FullName);
        loadListFile.Complete();
        await Task.WhenAll(createStateRecord.Completion);

        generateReport.Post(_listFileInfo.FullName);
        generateReport.Complete();
        await Task.WhenAll(writeReport.Completion);
    }
    catch (TaskCanceledException)
    {
        MessageBox.Show(
            "Job cancelled by user.", "Job Cancelled",
             MessageBoxButtons.OK);
        SetUiAfterProcessing("Job cancelled by user.");
    }
    catch (AggregateException aex)
    {
        MessageBox.Show(
        aex.ListExceptions(), "Task Errors", MessageBoxButtons.OK);
        SetUiAfterProcessing("Task processing error - job cancelled.");
    }
    catch (Exception ex)
    {
        MessageBox.Show(
        ex.ToString(), "Unhandled Exception", MessageBoxButtons.OK);
        SetUiAfterProcessing("Application error - job cancelled.");
    }
    finally
    {
        SetUiAfterProcessing("Job complete.");
    }
}

I have tried refactoring the methods and simplifying the internal actions of each Block, but I am still no closer to identifying what I am doing wrong - any help would be much appreciated. Thank you.

Neilski
  • 4,385
  • 5
  • 41
  • 74
  • Why do you set a `TaskScheduler`? – i3arnon Feb 22 '15 at 16:46
  • The short answer is because I thought I needed to to update the UI components on the form (e.g. Progress Bar). The TaskScheduler.FromCurrentSynchronizationContext() is used in the setStateRecordStatus () and setReportStatus () ActionBlocks. Interestingly(?) the problem goes away, if I remove all UI associated calls/references from the setReportStatus() ActionBlock. Is this approach wrong? – Neilski Feb 22 '15 at 16:56
  • `SynchronizationContext` is usually behind such blocks. I would use `Progress` instead to update the UI. It captures the context too but it's not required for the entire block. – i3arnon Feb 22 '15 at 16:58
  • I have not come across Progress. From a quick search, I cannot find an example of how this would be implemented in an ActionBlock. Sorry to ask, but can you provide any reference examples for TPL ActionBlocks? – Neilski Feb 22 '15 at 17:32
  • 1
    `Progress` has nothing to do with TPL Dataflow. It just allows you to report progress (mainly to the UI). You can use it inside a block just like in any other place... http://blog.stephencleary.com/2012/02/reporting-progress-from-async-tasks.html – i3arnon Feb 22 '15 at 17:37
  • Ah, got it, thank you - I was being stupid and trying to define the Progress action as a member of the ProcessData() method rather than passing in the Progress object as a parameter - which actually makes more sense to keep the UI and the DataFlow separate - thank you. – Neilski Feb 22 '15 at 18:02
  • Whilst the Progress approach is very clean, I am still unsure as to why my original approach failed as it is used in many examples/tutorials on TPL. – Neilski Feb 22 '15 at 18:03
  • 2
    Using the UI Sync Context is dangerous as it can easily lead to deadlocks since it uses a singe thread. Your code is too long to tell you anything deeper than that. – i3arnon Feb 22 '15 at 18:06
  • Thank you i3arnon, you have been very helpful. – Neilski Feb 22 '15 at 19:05

0 Answers0