1

I am having system that continuously processing messages. I want to make sure that I request messages from an external queue only when previous message was processed. Lets imagine that GetMessages method requests messages from external queue.

  • Got event 1. Will push it
  • Pushed 1
  • Got event 2. Will push it - my concert is here. As we get item before processing previous
  • Processing 1
  • Processed 1
  • Deleted 1

Code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            EventProcessor a = new EventProcessor();
            Task task = Task.Run(async ()=> await a.Process());

            task.Wait();
        }
    }

    public class EventProcessor
    {
        private readonly TransformBlock<int, string> _startBlock;
        private readonly ActionBlock<string> _deleteBlock;
        private readonly ActionBlock<int> _recieveBlock;

        public EventProcessor()
        {
            var executionDataflowBlockOptions = new ExecutionDataflowBlockOptions {
                MaxDegreeOfParallelism = 1,
                BoundedCapacity = 1,
            };

            this._startBlock = new TransformBlock<int, string>(
                async @event => await this.ProcessNotificationEvent(@event),
                executionDataflowBlockOptions
            );

            this._deleteBlock = new ActionBlock<string>(async @event => {
                await this.DeleteMessage(@event);
            }, executionDataflowBlockOptions);
            var trashBin = DataflowBlock.NullTarget<string>();


            var dataflowLinkOptions = new DataflowLinkOptions {
                PropagateCompletion = true,
            };

            this._startBlock.LinkTo(
                this._deleteBlock,
                dataflowLinkOptions,
                (result => result != "o")
            );

            this._startBlock.LinkTo(
                trashBin,
                dataflowLinkOptions,
                (result => result == "o")
            );
        }

        private async Task<string> ProcessNotificationEvent(int @event)
        {
            Console.WriteLine($"Processing {@event}");
            await Task.Delay(5000);
            Console.WriteLine($"Processed {@event}");
            return @event.ToString();
        }

        public async Task Process()
        {

            //while (this._cancellationTokenSource.IsCancellationRequested == false) {
            foreach (var notificationEvent in GetMessages()) {
                Console.WriteLine($"Got event {notificationEvent}. Will push it");
                if (await this._startBlock.SendAsync(notificationEvent) == false) {
                    Console.WriteLine($"Failed to push {notificationEvent}");
                    return;
                }
                Console.WriteLine($"Pushed {notificationEvent}");
            }
            //}
            this._startBlock.Complete();
            this._deleteBlock.Completion.Wait();
        }

        private static IEnumerable<int> GetMessages() {
            return Enumerable.Range(1, 5);
        }

        private async Task DeleteMessage(string @event)
        {
            Console.WriteLine($"Deleted {@event}");
        }
    }
}

Output will be

Got event 1. Will push it
Pushed 1
Got event 2. Will push it
Processing 1
Processed 1
Deleted 1
Processing 2
Pushed 2
Got event 3. Will push it
Processed 2
Processing 3
Deleted 2
Pushed 3
Got event 4. Will push it
Processed 3
Deleted 3
Processing 4
Pushed 4
Processed 4
Deleted 4
Press any key to continue . . .

I thought that i can create TDL DataFlow for the each message, but i think it will be an overkill.

Vadym K
  • 1,647
  • 1
  • 10
  • 14
  • Does it work if you make `ProcessNotificationEvent` synchronous rather than async? – mjwills Jul 17 '17 at 12:46
  • @mjwills Nope. Same behavior – Vadym K Jul 17 '17 at 12:52
  • Side note: if your `trashBin` is a `NullTarget`, you shouldn't filter messages for it. And don't do ` == false` compare, use `!` for booleans. – VMAtm Jul 17 '17 at 16:49
  • You want to impose a concurrency limitation that exceeds the boundaries of a single block. Take a look at [this](https://stackoverflow.com/questions/38400875/dataflow-tpl-implementing-pipeline-with-precondition/62356881#62356881) answer for the available options. In your case only the `SemaphoreSlim` solution is applicable, because the operation you need to throttle is asynchronous. – Theodor Zoulias Jun 16 '20 at 19:43

1 Answers1

3

The problem is that you have a buffer of one so your producer loop will always be working on the next item while the first is processing. This is a natural consequence of using TPL Dataflow.

If you want to literally process one at a time, the easiest approach is probably to remove TPL Dataflow:

public class EventProcessor
{
  private async Task<string> ProcessNotificationEvent(int @event)
  {
    Console.WriteLine($"Processing {@event}");
    await Task.Delay(5000);
    Console.WriteLine($"Processed {@event}");
    return @event.ToString();
  }

  public async Task Process()
  {
    foreach (var notificationEvent in GetMessages()) {
      Console.WriteLine($"Got event {notificationEvent}. Will push it");
      var result = await this.ProcessNotificationEvent(notificationEvent);
      if (result != "o")
        await DeleteMessage(result);
    }
  }

  private static IEnumerable<int> GetMessages() => Enumerable.Range(1, 5);

  private async Task DeleteMessage(string @event) => Console.WriteLine($"Deleted {@event}");
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks for the reply. Yep i was thinking about it. It was just an example and of course i have more dataflow blocks there. And each step actually adds one more item to buffer. So as i understand i have only two options: 1) remove Dataflow 2) Create flow for each message and await completion of last block. Right? – Vadym K Jul 17 '17 at 14:10
  • 1
    If this is in the middle of a Dataflow mesh, then just combine the "download" step and "process" step into a single TransformBlock. – Stephen Cleary Jul 17 '17 at 14:53