Question

As far as I understand, the TPL Dataflow offers the Actor programming model for .NET programmers (not speaking about 3rd party solutions that were available before). The Actor model itself declares that there are three basic operations each actor may support: 'send', 'create' and 'become'. What is the 'right' way of dealing with 'become' semantics in TPL Dataflow?

Please consider the following sample:

static void TestBecome()
{
    TransformBlock<string, string> dispatcher = null;
    dispatcher = new TransformBlock<string, string>
    (
        val =>
        {
            Console.WriteLine("Received for processing {0}", val);

            switch (val)
            {
                case "CREATE":  // create linked node
                {
                    dispatcher.LinkTo(CreateNewNode().Item2);
                    break;
                } 
                case "BECOME":  // transform the node ('become' semantics)
                {
                    var newNode = CreateNewNode(); 
                    Console.WriteLine("Dispatcher transformed to {0}", newNode.Item1);
                    dispatcher = newNode.Item2;

                    break; 
                }

                default: return val;    // dispatch the value to linked node (one of)
            }

            return string.Empty;    // 'empty unit'
        }
    );

    dispatcher.SendAsync("CREATE").ContinueWith(res => Console.WriteLine("Send CREATE: {0}", res.Result));
    dispatcher.SendAsync("CREATE").ContinueWith(res => Console.WriteLine("Send CREATE: {0}", res.Result));
    dispatcher.SendAsync("msg1").ContinueWith(res => Console.WriteLine("Send msg1: {0}", res.Result));
    dispatcher.SendAsync("msg2").ContinueWith(res => Console.WriteLine("Send msg2: {0}", res.Result)); ;

    Thread.Sleep(1000);

    dispatcher.SendAsync("BECOME").ContinueWith(res => Console.WriteLine("Send  BECOME: {0}", res.Result)); ;

    Thread.Sleep(1000);

    dispatcher.SendAsync("msg3").ContinueWith(res => Console.WriteLine("Send msg3: {0}", res.Result));
    dispatcher.SendAsync("msg4").ContinueWith(res => Console.WriteLine("Send msg4: {0}", res.Result));
    dispatcher.SendAsync("msg5").ContinueWith(res => Console.WriteLine("Send msg5: {0}", res.Result));
}

static Tuple<string, TransformBlock<string, string>> CreateNewNode()
{
    var id = Guid.NewGuid().ToString("N");
    var node = new TransformBlock<string, string>
    (
        val =>
        {
            if (string.IsNullOrWhiteSpace(val)) // pass trough 'empty unit'
                return val;
            Console.WriteLine("NODE {0}: {1}", id, val);
            return val;
        }
        , new ExecutionDataflowBlockOptions { BoundedCapacity = 3 }
    );

    return Tuple.Create(id, node);
}

I find such way of 'become' clumsy: instead of changing the actor's behavior, I change he actor instance itself (what can lead to undesired effects). What is the 'right' way?

And another one question: as far as I understand, standard TDF block is either never passes messages to linked notes (e.g. ActionBlock, if such logic is not written within it by hand) or always do this (the majority of blocks). Am I right that the logic when message shall be sent only in some cases (not always) shall be implemented as custom block?

Was it helpful?

Solution

First, TPL Dataflow is based on the actor model, but most of the time, you're going to be using only a specific subset of it. For example, it's quite rare to create a new dataflow block after receiving a message or to decide what block to send a message to (that's what linking is commonly used for). Though if you want to do that, you can.

Second, yeah, what you're doing is quite clumsy and it wouldn't work in many cases (e.g. if some block is already linked to dispatcher).

To do this, I would use the fact that the delegate that executes in the block can have internal state. For simple cases, you can use a captured variable in a lambda. For example, if you had a block that prepends a GUID to a string:

var id = Guid.NewGuid().ToString("N");
var block = new TransformBlock<string, string>(
    input =>
    {
        if (input == "BECOME")
        {
            id = Guid.NewGuid().ToString("N");
            return string.Empty;
        }

        return string.Format("{0}: {1}", id, input);
    });

For more complicated cases, you can create a class that stores its state in a field and create a delegate to an instance method of that class:

class IdPrepender
{
    private string id = Guid.NewGuid().ToString("N");

    public string Process(string input)
    {
        if (input == "BECOME")
        {
            id = Guid.NewGuid().ToString("N");
            return string.Empty;
        }

        return string.Format("{0}: {1}", id, input);
    }
}

…

var block = new TransformBlock<string, string>(new IdPrepender().Process);

In both cases, if the block can execute in parallel, you have to make sure that the code is thread-safe.

Also, I would not overload string like this. In the second case, you can take advantage of the fact that TPL Dataflow is not a pure actor model and add a "become" method to the class (IdPrepender).

Am I right that the logic when message shall be sent only in some cases (not always) shall be implemented as custom block?

You don't need a custom block for this. You can use TransformManyBlock whose delegate always returns 0 or 1 items.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top