1

I wanted to implement something like two-phase commit protocol for consuming messages.

In order to do it, I implemented ITargetBlock myself:

  public class Worker : ITargetBlock<Message>
  {
    // Is connected to remote server
    // Maintaining connection removed for brevity in this example
    private bool _isConnectionAlive;
    private readonly ActionBlock<MessageWithSource> _action;

    public Worker()
    {
      _action = new ActionBlock<MessageWithSource>(DoWork);
    }

    public DataflowMessageStatus OfferMessage(
      DataflowMessageHeader messageHeader, Message messageValue,
      ISourceBlock<Message> source, bool consumeToAccept)
    {
      if (consumeToAccept || source == null)
      {
        return DataflowMessageStatus.Declined;
      }

      if (!_isConnectionAlive)
      {
        return DataflowMessageStatus.Postponed;
      }

      var reservedMessage = source.ReserveMessage(messageHeader, this);
      if (reservedMessage)
      {
        _action.Post(new MessageWithSource(messageValue, source, messageHeader));
      }

      return DataflowMessageStatus.Postponed;
    }

    // Other methods removed for brevity

    private async Task DoWork(MessageWithSource value)
    {
      try
      {
        // sending message to the server removed for brevity


        // commit that we finished processing without error
        var message = value.SourceBlock.ConsumeMessage(value.MessageHeader, this, out _);

        if (message != value.Message)
        {
          // In which cases can we get here?
          throw new InvalidOperationException("Consumed some other message... oh my");
        }
      }
      catch (WebSocketException)
      {
        // Release reservation if we can't finish work, so other Workers can pickup this message and process it
        value.SourceBlock.ReleaseReservation(value.MessageHeader, this);
      }
    }

    private class MessageWithSource
    {
      public Message Message { get; }
      public ISourceBlock<Message> SourceBlock { get; }
      public DataflowMessageHeader MessageHeader { get; }
    }
  }

In the docs it says ConsumeMessage can return a different instance than it was previously offered.

I wonder in which cases and way it happens?

Veikedo
  • 1,453
  • 1
  • 18
  • 25
  • @StephenCleary will really appreciate if you can take a look – Veikedo Mar 15 '19 at 14:23
  • Why use transaction semantics in a *dataflow*, much less two-phase commit? Which transaction coodinators are involved? The dataflow way of handling problems is to redirect the failed message or error message to another block, eg using a predicate in `LinkTo()`. This way you avoid blocking other messages in the input queue. – Panagiotis Kanavos Mar 15 '19 at 15:00
  • Redirection becomes a lot easier if messages are wrapped in an "envelope" that indicates whether this is a good or "bad" message. The envelope could include a retry count to ensure the same message isn't retried indefinitely. – Panagiotis Kanavos Mar 15 '19 at 15:02
  • @PanagiotisKanavos I wanted transaction semantics because I need to ensure ordering and some of these `Worker` instances can be broken for a while. May be there is another good way to do this? – Veikedo Mar 18 '19 at 08:46
  • My initial task sounds like - many clients send messages to the server (A). The server processes each message and then sends it further to other server (B) via websockets. I need to ensure order of messages for each client, but I don't care if second client's messages are sent before first client. I have several connections (aka `Worker`s) to server B, so if one connection is broken then other connection should pickup this message, still ensure ordering – Veikedo Mar 18 '19 at 08:52
  • 1
    What you describe is queueing, messaging and retrying. It has nothing to do with transaction semantics. Transaction semantics mean that either all servers accept and commit an operation or all servers discard it. 2PC is how they do it. – Panagiotis Kanavos Mar 18 '19 at 10:17

0 Answers0