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);
Receive<Send>(send =>
{
var routees = new List<Routee>();
Bucket bucket;
if (_registry.TryGetValue(_cluster.SelfAddress, out bucket))
{
ValueHolder valueHolder;
if (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(Utils.WrapIfNeeded(send.Message), Sender);
}
});
Receive<SendToAll>(sendToAll =>
{
PublishMessage(sendToAll.Path, sendToAll.Message, sendToAll.ExcludeSelf);
});
Receive<Publish>(publish =>
{
var topic = Uri.EscapeDataString(publish.Topic);
var path = Self.Path / topic;
if (publish.SendOneMessageToEachGroup)
PublishToEachGroup(path.ToStringWithoutAddress(), publish.Message);
else
PublishMessage(path.ToStringWithoutAddress(), publish.Message);
});
Receive<Put>(put =>
{
if (!string.IsNullOrEmpty(put.Ref.Path.Address.Host))
Log.Warning("Registered actor must be local: [{0}]", put.Ref);
else
{
PutToRegistry(put.Ref.Path.ToStringWithoutAddress(), 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 topic = Uri.EscapeDataString(subscribe.Topic);
var child = Context.Child(topic);
if (!ActorRefs.Nobody.Equals(child))
{
child.Forward(subscribe);
}
else
{
var t = Context.ActorOf(Actor.Props.Create(() =>
new Topic(_settings.RemovedTimeToLive, _settings.RoutingLogic)), topic);
t.Forward(subscribe);
HandleRegisterTopic(t);
}
});
Receive<RegisterTopic>(register =>
{
HandleRegisterTopic(register.TopicRef);
});
Receive<GetTopics>(getTopics =>
{
Sender.Tell(new CurrentTopics(GetCurrentTopics().ToArray()));
});
Receive<Subscribed>(subscribed =>
{
subscribed.Subscriber.Tell(subscribed.Ack);
});
Receive<Unsubscribe>(unsubscribe =>
{
var topic = Uri.EscapeDataString(unsubscribe.Topic);
var child = Context.Child(topic);
if (!ActorRefs.Nobody.Equals(child))
child.Forward(unsubscribe);
});
Receive<Unsubscribed>(unsubscribed =>
{
unsubscribed.Subscriber.Tell(unsubscribed.Ack);
});
Receive<Status>(status =>
{
// 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 (OtherHasNewerVersions(status.Versions))
Sender.Tell(new Status(OwnVersions));
});
Receive<Delta>(delta =>
{
// 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.Add(bucket.Owner, new Bucket(myBucket.Owner, bucket.Version, myBucket.Content.AddRange(bucket.Content)));
}
}
}
}
});
Receive<GossipTick>(_ => HandleGossip());
Receive<Prune>(_ => HandlePrune());
Receive<Terminated>(terminated =>
{
var key = terminated.ActorRef.Path.ToStringWithoutAddress();
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
}
}
});
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.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);
});
}