Akka.net: Suggested ExactlyOnceReceiveActor Class

Created on 22 May 2018  路  12Comments  路  Source: akkadotnet/akka.net

I know it's been stated many times that, in general, you don't want to implement exactly once messaging semantics for actors, as it incurs extra state that has to be managed. I agree, but I think there should be some out of the box solution to situations where you really DO need this functionality. I'm working on something, not sure if it makes sense. This code is not finished btw, just a very rough sketch of what I'm thinking. I'm hoping that child classes Of ExactlyOnceReceiveActor would be able to just inherit from this class and add custom processing code once the base class verified that it has not seen this particular message before from this particular actor.

In order to give the semantics of "exactly once" you would really need to make the receiver persist, just like the AtLeastOnceDeliveryReceiveActor persists and recovers from faults in the actor system / CLR.

    /// <summary>
    /// Each message received is processed exactly once by child classes IF implmented correctly.
    /// In the constructor of each child class, you must use the ExactlyOnceReceiveHandler to handle messages.
    /// </summary>
    public class ExactlyOnceReceiveActor : ReceivePersistentActor
    {
        protected MessageBufferEvictionPolicy EvictionPolicy { get; set; }
        protected Dictionary<IActorRef, HashSet<long>> ActorRef_ProcessedMessageIds_KVPs { get; set; }

        public override string PersistenceId => throw new NotImplementedException();

        public ExactlyOnceReceiveActor(MessageBufferEvictionPolicy evictionPolicy)
        {
            ActorRef_ProcessedMessageIds_KVPs = new Dictionary<IActorRef, HashSet<long>>();
            EvictionPolicy = evictionPolicy;
        }

        /// <summary>
        /// Wrapper around ReceiveHandler that keeps track of processed messages.
        /// Filters out messages that we have already seen.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="handler">Only executed if message is unique.</param>
        public void ExactlyOnceReceiveHandler<T>(Action<T> handler)
        {
            var value = new HashSet<long>();
            bool actorSenderNotSeenBefore = true;
            try
            {
                actorSenderNotSeenBefore = ActorRef_ProcessedMessageIds_KVPs.TryGetValue(Sender, out value);
            }
            catch (Exception e)
            {
                throw new Exception(e.Message);
            }

            if (actorSenderNotSeenBefore)
            {
                if (handler.Target is ReliableDelivery)
                {
                    ReliableDelivery msg = ((ReliableDelivery)handler.Target);
                    var seenMessages = new HashSet<long>();
                    seenMessages.Add(msg.MessageId);
                    ActorRef_ProcessedMessageIds_KVPs.Add(Sender, seenMessages);
                    handler.Invoke((T)handler.Target);
                    Sender.Tell(new ReliableDelivery(msg.MessageId));
                }
            }

            else // Have seen the actor sender before
            {
            }
        }
    }
public class ReliableDelivery
    {
        public ReliableDelivery(long messageId)
        {
            MessageId = messageId;
        }

        public long MessageId { get; private set; }
    }
public class MessageBufferEvictionPolicy
    {
        internal void SetEvictionPolicy()
        {
            throw new NotImplementedException();
        }
    }
enhancement

All 12 comments

@nathvi IMHO, we'd be open to any ideas here - so I assume that what you're working on is the _receiver_ side of the exactly once issue?

@Aaronontheweb , Yes. This receiver would have to be paired with one of the at least once delivery actors to get this functionality.

Ok, I think we'd be down for that.

A couple of things to consider:

  1. Do you want to have a standard interface for messages that perform reliable delivery? I.e. some sort of interface that exposes the equivalent of the message ID from the AtLeastOnceDeliveryActor?
  2. If we want to go ahead with item 1 on that list, we'd probably need to change the signatures to the AtLeastOnceDeliveryActor in order to extract that message ID. This would be a significant breaking change to the API there since users would need to update all of their message types, but some users might be totally onboard with that in exchange for being able to get de-duplication of messages out of the box.
  3. Alternatively, we can do what we do with consistent hash routers today - give the user the ability to supply a function that can extract the message ID without changing the message's type itself OR the ability to wrap the current user-defined messages in an envelope which contains this user ID. CHRs can do both. That way you may be able to accomplish step 2 without forcing users to update all of their message types, but at the cost of additional code or allocations.

I think there would be _a lot_ of users interested in this sort of feature, but bear in mind the cost to existing users who are already using AtLeastOnceDelivery actors et al - how much work, if any, would they need to do even if they didn't want to use this feature?

I put all of that in there to say: approach these design choices with equal parts technology and economics, where the currency at work is the user's time or effort.

And if you have questions about going about this, you can weigh in on here with prototypes et al or hit us up on Gitter with questions. We'd be happy to assist.

As a user, I would prefer going with option 3 (wrapping existing messages in an envelope with a message ID). That way I don't need to conform to new breaking changes and can keep my existing code.

@nathvi yeah, I think that'd be the approach I would choose too - I'd have users explicitly opt-in to wrapping their user-defined messages inside that envelope today if they want to use AtLeastOnceDelivery actors with this actor base type you're building.

I'm assuming that I would want to derive my ExactlyOnceRecieverActor from ReceivePersistentActor?

Nah, they're kind of different tools - you don't need Akka.Persistence for this, right? Since it's all in-memory? Or do you want to persist the prior IDs you've observed? If that's the case, then this should be in Akka.Persistence somewhere.

Now that I think about it, in order to give the semantics of "exactly once" you would really need to make the receiver persist, just like the AtLeastOnceDeliveryReceiveActor persists and recovers from faults in the actor system / CLR.

@nathvi the code you've presented is not self-explanatory. Could you describe your approach a bit?

@Horusiath , yeah, I'm going to try and modify it a bit.

and by a bit, I mean I have no idea when I can work on this again.

Was this page helpful?
0 / 5 - 0 ratings