in Sources/DistributedActors/Cluster/MembershipGossip/Cluster+MembershipGossip.swift [61:112]
mutating func mergeForward(incoming: MembershipGossip) -> MergeDirective {
var incoming = incoming
// 1) decide the relationship between this gossip and the incoming one
let causalRelation: VersionVector.CausalRelation = self.version.compareTo(incoming.version)
// 1.1) Protect the node from any gossip from a .down node (!), it cannot and MUST NOT be trusted.
let incomingGossipOwnerKnownLocally = self.membership.uniqueMember(incoming.owner)
guard let incomingOwnerMember = incoming.membership.uniqueMember(incoming.owner) else {
return .init(causalRelation: causalRelation, effectiveChanges: [])
}
switch incomingGossipOwnerKnownLocally {
case .some(let locallyKnownMember) where locallyKnownMember.status.isDown:
// we have NOT removed it yet, but it is down, so we ignore it
return .init(causalRelation: causalRelation, effectiveChanges: [])
case .none where incomingOwnerMember.status.isAtLeast(.down):
// we have likely removed it, and it is down anyway, so we ignore it completely
return .init(causalRelation: causalRelation, effectiveChanges: [])
default:
() // ok, so it is fine and still alive
}
// 1.2) Protect from zombies: Any nodes that we know are dead or down, we should not accept any information from
let incomingConcurrentDownMembers = incoming.membership.members(atLeast: .down)
for pruneFromIncomingBeforeMerge in incomingConcurrentDownMembers
where self.membership.uniqueMember(pruneFromIncomingBeforeMerge.uniqueNode) == nil {
_ = incoming.pruneMember(pruneFromIncomingBeforeMerge)
}
// 2) calculate membership changes; if this gossip is strictly more recent than the incoming one,
// we can skip this as we "know" that we already know everything that the incoming has to offer (optimization)
let changes: [MembershipChange]
if case .happenedAfter = causalRelation {
// ignore all changes >>
// our local view happened strictly _after_ the incoming one, thus it is guaranteed
// it will not provide us with new information; This is only an optimization, and would work correctly without it.
changes = []
} else {
// incoming is concurrent, ahead, or same
changes = self.membership.mergeFrom(incoming: incoming.membership, myself: self.owner)
}
self.seen.merge(selfOwner: self.owner, incoming: incoming.seen)
// 3) if any removals happened, we need to prune the removed nodes from the seen table
for change in changes
where change.status.isRemoved && change.member.uniqueNode != self.owner {
self.seen.prune(change.member.uniqueNode)
}
return .init(causalRelation: causalRelation, effectiveChanges: changes)
}