/// <summary> /// TBD /// </summary> /// <param name="settings">TBD</param> /// <exception cref="ArgumentException">TBD</exception> /// <returns>TBD</returns> public DistributedPubSubMediator(DistributedPubSubSettings settings) { if (settings.RoutingLogic is ConsistentHashingRoutingLogic) { throw new ArgumentException("Consistent hashing routing logic cannot be used by the pub-sub mediator"); } _settings = settings; if (!string.IsNullOrEmpty(_settings.Role) && !_cluster.SelfRoles.Contains(_settings.Role)) { throw new ArgumentException($"The cluster member [{_cluster.SelfAddress}] doesn't have the role [{_settings.Role}]"); } //Start periodic gossip to random nodes in cluster _gossipCancelable = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable(_settings.GossipInterval, _settings.GossipInterval, Self, GossipTick.Instance, Self); _pruneInterval = new TimeSpan(_settings.RemovedTimeToLive.Ticks / 2); _pruneCancelable = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable(_pruneInterval, _pruneInterval, Self, Prune.Instance, Self); _buffer = new PerGroupingBuffer(); Receive <Send>(send => { var routees = new List <Routee>(); ValueHolder valueHolder; if (_registry.TryGetValue(_cluster.SelfAddress, out var bucket) && bucket.Content.TryGetValue(send.Path, out valueHolder) && send.LocalAffinity) { var routee = valueHolder.Routee; if (routee != null) { routees.Add(routee); } } else { foreach (var entry in _registry) { if (entry.Value.Content.TryGetValue(send.Path, out valueHolder)) { var routee = valueHolder.Routee; if (routee != null) { routees.Add(routee); } } } } if (routees.Count != 0) { new Router(_settings.RoutingLogic, routees.ToArray()).Route( Internal.Utils.WrapIfNeeded(send.Message), Sender); } else { IgnoreOrSendToDeadLetters(send.Message); } }); Receive <SendToAll>(sendToAll => { PublishMessage(sendToAll.Path, sendToAll.Message, sendToAll.ExcludeSelf); }); Receive <Publish>(publish => { string path = Internal.Utils.MakeKey(Self.Path / Internal.Utils.EncodeName(publish.Topic)); if (publish.SendOneMessageToEachGroup) { PublishToEachGroup(path, publish.Message); } else { PublishMessage(path, publish.Message); } }); Receive <Put>(put => { if (put.Ref.Path.Address.HasGlobalScope) { Log.Warning("Registered actor must be local: [{0}]", put.Ref); } else { PutToRegistry(Internal.Utils.MakeKey(put.Ref), put.Ref); Context.Watch(put.Ref); } }); Receive <Remove>(remove => { if (_registry.TryGetValue(_cluster.SelfAddress, out var bucket)) { if (bucket.Content.TryGetValue(remove.Path, out var valueHolder) && !valueHolder.Ref.IsNobody()) { Context.Unwatch(valueHolder.Ref); PutToRegistry(remove.Path, null); } } }); Receive <Subscribe>(subscribe => { // each topic is managed by a child actor with the same name as the topic var encodedTopic = Internal.Utils.EncodeName(subscribe.Topic); _buffer.BufferOr(Internal.Utils.MakeKey(Self.Path / encodedTopic), subscribe, Sender, () => { var child = Context.Child(encodedTopic); if (!child.IsNobody()) { child.Forward(subscribe); } else { NewTopicActor(encodedTopic).Forward(subscribe); } }); }); Receive <RegisterTopic>(register => { HandleRegisterTopic(register.TopicRef); }); Receive <NoMoreSubscribers>(msg => { var key = Internal.Utils.MakeKey(Sender); _buffer.InitializeGrouping(key); Sender.Tell(TerminateRequest.Instance); }); Receive <NewSubscriberArrived>(msg => { var key = Internal.Utils.MakeKey(Sender); _buffer.ForwardMessages(key, Sender); }); Receive <GetTopics>(getTopics => { Sender.Tell(new CurrentTopics(GetCurrentTopics().ToImmutableHashSet())); }); Receive <Subscribed>(subscribed => { subscribed.Subscriber.Tell(subscribed.Ack); }); Receive <Unsubscribe>(unsubscribe => { var encodedTopic = Internal.Utils.EncodeName(unsubscribe.Topic); _buffer.BufferOr(Internal.Utils.MakeKey(Self.Path / encodedTopic), unsubscribe, Sender, () => { var child = Context.Child(encodedTopic); if (!child.IsNobody()) { child.Forward(unsubscribe); } else { // no such topic here } }); }); Receive <Unsubscribed>(unsubscribed => { unsubscribed.Subscriber.Tell(unsubscribed.Ack); }); Receive <Status>(status => { // only accept status from known nodes, otherwise old cluster with same address may interact // also accept from local for testing purposes if (_nodes.Contains(Sender.Path.Address) || Sender.Path.Address.HasLocalScope) { // gossip chat starts with a Status message, containing the bucket versions of the other node var delta = CollectDelta(status.Versions).ToImmutableList(); if (delta.Count != 0) { Sender.Tell(new Delta(delta)); } if (!status.IsReplyToStatus && OtherHasNewerVersions(status.Versions)) { Sender.Tell(new Status(versions: OwnVersions, isReplyToStatus: true)); // it will reply with Delta } } }); Receive <Delta>(delta => { deltaCount += 1; // reply from Status message in the gossip chat // the Delta contains potential updates (newer versions) from the other node // only accept deltas/buckets from known nodes, otherwise there is a risk of // adding back entries when nodes are removed if (_nodes.Contains(Sender.Path.Address)) { foreach (var bucket in delta.Buckets) { if (_nodes.Contains(bucket.Owner)) { if (!_registry.TryGetValue(bucket.Owner, out var myBucket)) { myBucket = new Bucket(bucket.Owner); } if (bucket.Version > myBucket.Version) { _registry[bucket.Owner] = new Bucket(myBucket.Owner, bucket.Version, myBucket.Content.SetItems(bucket.Content)); } } } } }); Receive <GossipTick>(_ => HandleGossip()); Receive <Prune>(_ => HandlePrune()); Receive <Terminated>(terminated => { var key = Internal.Utils.MakeKey(terminated.ActorRef); if (_registry.TryGetValue(_cluster.SelfAddress, out var bucket)) { if (bucket.Content.TryGetValue(key, out var holder) && terminated.ActorRef.Equals(holder.Ref)) { PutToRegistry(key, null); // remove } } _buffer.RecreateAndForwardMessagesIfNeeded(key, () => NewTopicActor(terminated.ActorRef.Path.Name)); }); Receive <ClusterEvent.CurrentClusterState>(state => { var nodes = state.Members .Where(m => m.Status != MemberStatus.Joining && IsMatchingRole(m)) .Select(m => m.Address); _nodes = new HashSet <Address>(nodes); }); Receive <ClusterEvent.MemberUp>(up => { if (IsMatchingRole(up.Member)) { _nodes.Add(up.Member.Address); } }); Receive <ClusterEvent.MemberWeaklyUp>(weaklyUp => { if (IsMatchingRole(weaklyUp.Member)) { _nodes.Add(weaklyUp.Member.Address); } }); Receive <ClusterEvent.MemberLeft>(left => { if (IsMatchingRole(left.Member)) { _nodes.Remove(left.Member.Address); _registry.Remove(left.Member.Address); } }); Receive <ClusterEvent.MemberDowned>(downed => { if (IsMatchingRole(downed.Member)) { _nodes.Remove(downed.Member.Address); _registry.Remove(downed.Member.Address); } }); Receive <ClusterEvent.MemberRemoved>(removed => { var member = removed.Member; if (member.Address == _cluster.SelfAddress) { Context.Stop(Self); } else if (IsMatchingRole(member)) { _nodes.Remove(member.Address); _registry.Remove(member.Address); } }); Receive <ClusterEvent.IMemberEvent>(_ => { /* ignore */ }); Receive <Count>(_ => { var count = _registry.Sum(entry => entry.Value.Content.Count(kv => !kv.Value.Ref.IsNobody())); Sender.Tell(count); }); Receive <DeltaCount>(_ => { Sender.Tell(deltaCount); }); Receive <CountSubscribers>(msg => { var encTopic = Internal.Utils.EncodeName(msg.Topic); _buffer.BufferOr(Internal.Utils.MakeKey(Self.Path / encTopic), msg, Sender, () => { var child = Context.Child(encTopic); if (!child.IsNobody()) { child.Tell(Count.Instance, Sender); } else { Sender.Tell(0); } }); }); }
public DistributedPubSubMediator(DistributedPubSubSettings settings) { if (settings.RoutingLogic is ConsistentHashingRoutingLogic) throw new ArgumentException("Consistent hashing routing logic cannot be used by the pub-sub mediator"); _settings = settings; if (!string.IsNullOrEmpty(_settings.Role) && !_cluster.SelfRoles.Contains(_settings.Role)) throw new ArgumentException(string.Format("The cluster member [{0}] doesn't have the role [{1}]", _cluster.SelfAddress, _settings.Role)); //Start periodic gossip to random nodes in cluster _gossipCancelable = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable(_settings.GossipInterval, _settings.GossipInterval, Self, GossipTick.Instance, Self); _pruneInterval = new TimeSpan(_settings.RemovedTimeToLive.Ticks / 2); _pruneCancelable = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable(_pruneInterval, _pruneInterval, Self, Prune.Instance, Self); _buffer = new PerGroupingBuffer(); Receive<Send>(send => { var routees = new List<Routee>(); Bucket bucket; ValueHolder valueHolder; if (_registry.TryGetValue(_cluster.SelfAddress, out bucket) && bucket.Content.TryGetValue(send.Path, out valueHolder) && send.LocalAffinity) { var routee = valueHolder.Routee; if (routee != null) routees.Add(routee); } else { foreach (var entry in _registry) { if (entry.Value.Content.TryGetValue(send.Path, out valueHolder)) { var routee = valueHolder.Routee; if (routee != null) routees.Add(routee); } } } if (routees.Count != 0) { new Router(_settings.RoutingLogic, routees.ToArray()).Route( Internal.Utils.WrapIfNeeded(send.Message), Sender); } else { SendToDeadLetters(send.Message); } }); Receive<SendToAll>(sendToAll => { PublishMessage(sendToAll.Path, sendToAll.Message, sendToAll.ExcludeSelf); }); Receive<Publish>(publish => { string path = Internal.Utils.MakeKey(Self.Path / Internal.Utils.EncodeName(publish.Topic)); if (publish.SendOneMessageToEachGroup) PublishToEachGroup(path, publish.Message); else PublishMessage(path, publish.Message); }); Receive<Put>(put => { if (put.Ref.Path.Address.HasGlobalScope) { Log.Warning("Registered actor must be local: [{0}]", put.Ref); } else { PutToRegistry(Internal.Utils.MakeKey(put.Ref), put.Ref); Context.Watch(put.Ref); } }); Receive<Remove>(remove => { Bucket bucket; if (_registry.TryGetValue(_cluster.SelfAddress, out bucket)) { ValueHolder valueHolder; if (bucket.Content.TryGetValue(remove.Path, out valueHolder) && valueHolder.Ref != null) { Context.Unwatch(valueHolder.Ref); PutToRegistry(remove.Path, null); } } }); Receive<Subscribe>(subscribe => { // each topic is managed by a child actor with the same name as the topic var encodedTopic = Internal.Utils.EncodeName(subscribe.Topic); _buffer.BufferOr(Internal.Utils.MakeKey(Self.Path / encodedTopic), subscribe, Sender, () => { var child = Context.Child(encodedTopic); if (!child.IsNobody()) { child.Forward(subscribe); } else { NewTopicActor(encodedTopic).Forward(subscribe); } }); }); Receive<RegisterTopic>(register => { HandleRegisterTopic(register.TopicRef); }); Receive<NoMoreSubscribers>(msg => { var key = Internal.Utils.MakeKey(Sender); _buffer.InitializeGrouping(key); Sender.Tell(TerminateRequest.Instance); }); Receive<NewSubscriberArrived>(msg => { var key = Internal.Utils.MakeKey(Sender); _buffer.ForwardMessages(key, Sender); }); Receive<GetTopics>(getTopics => { Sender.Tell(new CurrentTopics(GetCurrentTopics().ToArray())); }); Receive<Subscribed>(subscribed => { subscribed.Subscriber.Tell(subscribed.Ack); }); Receive<Unsubscribe>(unsubscribe => { var encodedTopic = Internal.Utils.EncodeName(unsubscribe.Topic); _buffer.BufferOr(Internal.Utils.MakeKey(Self.Path / encodedTopic), unsubscribe, Sender, () => { var child = Context.Child(encodedTopic); if (!child.IsNobody()) { child.Forward(unsubscribe); } else { // no such topic here } }); }); Receive<Unsubscribed>(unsubscribed => { unsubscribed.Subscriber.Tell(unsubscribed.Ack); }); Receive<Status>(status => { // only accept status from known nodes, otherwise old cluster with same address may interact // also accept from local for testing purposes if (_nodes.Contains(Sender.Path.Address) || Sender.Path.Address.HasLocalScope) { // gossip chat starts with a Status message, containing the bucket versions of the other node var delta = CollectDelta(status.Versions).ToArray(); if (delta.Length != 0) Sender.Tell(new Delta(delta)); if (!status.IsReplyToStatus && OtherHasNewerVersions(status.Versions)) Sender.Tell(new Status(versions: OwnVersions, isReplyToStatus: true)); // it will reply with Delta } }); Receive<Delta>(delta => { deltaCount += 1; // reply from Status message in the gossip chat // the Delta contains potential updates (newer versions) from the other node // only accept deltas/buckets from known nodes, otherwise there is a risk of // adding back entries when nodes are removed if (_nodes.Contains(Sender.Path.Address)) { foreach (var bucket in delta.Buckets) { if (_nodes.Contains(bucket.Owner)) { Bucket myBucket; if (!_registry.TryGetValue(bucket.Owner, out myBucket)) myBucket = new Bucket(bucket.Owner); if (bucket.Version > myBucket.Version) { _registry[bucket.Owner] = new Bucket(myBucket.Owner, bucket.Version, myBucket.Content.SetItems(bucket.Content)); } } } } }); Receive<GossipTick>(_ => HandleGossip()); Receive<Prune>(_ => HandlePrune()); Receive<Terminated>(terminated => { var key = Internal.Utils.MakeKey(terminated.ActorRef); Bucket bucket; if (_registry.TryGetValue(_cluster.SelfAddress, out bucket)) { ValueHolder holder; if (bucket.Content.TryGetValue(key, out holder) && terminated.ActorRef.Equals(holder.Ref)) { PutToRegistry(key, null); // remove } } _buffer.RecreateAndForwardMessagesIfNeeded(key, () => NewTopicActor(terminated.ActorRef.Path.Name)); }); Receive<ClusterEvent.CurrentClusterState>(state => { var nodes = state.Members .Where(m => m.Status != MemberStatus.Joining && IsMatchingRole(m)) .Select(m => m.Address); _nodes = new HashSet<Address>(nodes); }); Receive<ClusterEvent.MemberUp>(up => { if (IsMatchingRole(up.Member)) _nodes.Add(up.Member.Address); }); Receive<ClusterEvent.MemberLeft>(left => { if (IsMatchingRole(left.Member)) { _nodes.Remove(left.Member.Address); _registry.Remove(left.Member.Address); } }); Receive<ClusterEvent.MemberRemoved>(removed => { var member = removed.Member; if (member.Address == _cluster.SelfAddress) { Context.Stop(Self); } else if (IsMatchingRole(member)) { _nodes.Remove(member.Address); _registry.Remove(member.Address); } }); Receive<ClusterEvent.IMemberEvent>(_ => { /* ignore */ }); Receive<Count>(_ => { var count = _registry.Sum(entry => entry.Value.Content.Count(kv => kv.Value.Ref != null)); Sender.Tell(count); }); Receive<DeltaCount>(_ => { Sender.Tell(deltaCount); }); }