All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.scalecube.cluster.gossip.GossipProtocolImpl Maven / Gradle / Ivy

The newest version!
package io.scalecube.cluster.gossip;

import static reactor.core.publisher.Sinks.EmitFailureHandler.busyLooping;

import io.scalecube.cluster.ClusterMath;
import io.scalecube.cluster.Member;
import io.scalecube.cluster.membership.MembershipEvent;
import io.scalecube.cluster.transport.api.Message;
import io.scalecube.cluster.transport.api.Transport;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.Disposable;
import reactor.core.Disposables;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoSink;
import reactor.core.publisher.Sinks;
import reactor.core.scheduler.Scheduler;

public final class GossipProtocolImpl implements GossipProtocol {

  private static final Logger LOGGER = LoggerFactory.getLogger(GossipProtocol.class);

  // Qualifiers

  public static final String GOSSIP_REQ = "sc/gossip/req";

  // Injected

  private final Member localMember;
  private final Transport transport;
  private final GossipConfig config;

  // Local State

  private long currentPeriod = 0;
  private long gossipCounter = 0;
  private final Map sequenceIdCollectors = new HashMap<>();
  private final Map gossips = new HashMap<>();
  private final Map> futures = new HashMap<>();

  private final List remoteMembers = new ArrayList<>();
  private int remoteMembersIndex = -1;

  // Disposables

  private final Disposable.Composite actionsDisposables = Disposables.composite();

  // Subject

  private final Sinks.Many sink = Sinks.many().multicast().directBestEffort();

  // Scheduled

  private final Scheduler scheduler;

  /**
   * Creates new instance of gossip protocol with given memberId, transport and settings.
   *
   * @param localMember local cluster member
   * @param transport cluster transport
   * @param membershipProcessor membership event processor
   * @param config gossip protocol settings
   * @param scheduler scheduler
   */
  public GossipProtocolImpl(
      Member localMember,
      Transport transport,
      Flux membershipProcessor,
      GossipConfig config,
      Scheduler scheduler) {

    this.transport = Objects.requireNonNull(transport);
    this.config = Objects.requireNonNull(config);
    this.localMember = Objects.requireNonNull(localMember);
    this.scheduler = Objects.requireNonNull(scheduler);

    // Subscribe
    actionsDisposables.addAll(
        Arrays.asList(
            membershipProcessor // Listen membership events to update remoteMembers
                .publishOn(scheduler)
                .subscribe(
                    this::onMembershipEvent,
                    ex -> LOGGER.error("[{}][onMembershipEvent][error] cause:", localMember, ex)),
            transport
                .listen() // Listen gossip requests
                .publishOn(scheduler)
                .filter(this::isGossipRequest)
                .subscribe(
                    this::onGossipRequest,
                    ex -> LOGGER.error("[{}][onGossipRequest][error] cause:", localMember, ex))));
  }

  @Override
  public void start() {
    actionsDisposables.add(
        scheduler.schedulePeriodically(
            this::doSpreadGossip,
            config.gossipInterval(),
            config.gossipInterval(),
            TimeUnit.MILLISECONDS));
  }

  @Override
  public void stop() {
    // Stop accepting gossip requests and spreading gossips
    actionsDisposables.dispose();

    // Stop publishing events
    sink.emitComplete(busyLooping(Duration.ofSeconds(3)));
  }

  @Override
  public Mono spread(Message message) {
    return Mono.just(message)
        .subscribeOn(scheduler)
        .flatMap(msg -> Mono.create(sink -> futures.put(createAndPutGossip(msg), sink)));
  }

  @Override
  public Flux listen() {
    return sink.asFlux().onBackpressureBuffer();
  }

  // ================================================
  // ============== Action Methods ==================
  // ================================================

  private void doSpreadGossip() {
    // Increment period
    long period = currentPeriod++;

    // Check segments
    checkGossipSegmentation();

    // Check any gossips exists
    if (gossips.isEmpty()) {
      return; // nothing to spread
    }

    try {
      // Spread gossips to randomly selected member(s)
      selectGossipMembers().forEach(member -> spreadGossipsTo(period, member));

      // Sweep gossips
      Set gossipsToRemove = getGossipsToRemove(period);
      if (!gossipsToRemove.isEmpty()) {
        LOGGER.debug("[{}][{}] Sweep gossips: {}", localMember, period, gossipsToRemove);
        for (String gossipId : gossipsToRemove) {
          gossips.remove(gossipId);
        }
      }

      // Check spread gossips
      Set gossipsThatSpread = getGossipsThatMostLikelyDisseminated(period);
      if (!gossipsThatSpread.isEmpty()) {
        LOGGER.debug(
            "[{}][{}] Most likely disseminated gossips: {}",
            localMember,
            period,
            gossipsThatSpread);
        for (String gossipId : gossipsThatSpread) {
          MonoSink sink = futures.remove(gossipId);
          if (sink != null) {
            sink.success(gossipId);
          }
        }
      }
    } catch (Exception ex) {
      LOGGER.warn("[{}][{}][doSpreadGossip] Exception occurred:", localMember, period, ex);
    }
  }

  // ================================================
  // ============== Event Listeners =================
  // ================================================

  private String createAndPutGossip(Message message) {
    final long period = this.currentPeriod;
    final Gossip gossip = createGossip(message);
    final GossipState gossipState = new GossipState(gossip, period);

    gossips.put(gossip.gossipId(), gossipState);
    ensureSequence(localMember.id()).add(gossip.sequenceId());

    return gossip.gossipId();
  }

  private void onGossipRequest(Message message) {
    final long period = this.currentPeriod;
    final GossipRequest gossipRequest = message.data();
    for (Gossip gossip : gossipRequest.gossips()) {
      GossipState gossipState = gossips.get(gossip.gossipId());
      if (ensureSequence(gossip.gossiperId()).add(gossip.sequenceId())) {
        if (gossipState == null) { // new gossip
          gossipState = new GossipState(gossip, period);
          gossips.put(gossip.gossipId(), gossipState);
          sink.emitNext(gossip.message(), busyLooping(Duration.ofSeconds(3)));
        }
      }
      if (gossipState != null) {
        gossipState.addToInfected(gossipRequest.from());
      }
    }
  }

  private void checkGossipSegmentation() {
    final int intervalsThreshold = config.gossipSegmentationThreshold();
    for (Entry entry : sequenceIdCollectors.entrySet()) {
      // Size of sequenceIdCollector could grow only if we never received some messages.
      // Which is possible only if current node wasn't available(suspected) for some time
      // or network issue
      final SequenceIdCollector sequenceIdCollector = entry.getValue();
      if (sequenceIdCollector.size() > intervalsThreshold) {
        LOGGER.warn(
            "[{}][{}] Too many missed gossip messages from original gossiper: {}, "
                + "current node({}) was SUSPECTED much for a long time or connection problem",
            localMember,
            currentPeriod,
            entry.getKey(),
            localMember);

        sequenceIdCollector.clear();
      }
    }
  }

  private void onMembershipEvent(MembershipEvent event) {
    Member member = event.member();
    if (event.isRemoved()) {
      boolean removed = remoteMembers.remove(member);
      sequenceIdCollectors.remove(member.id());
      if (removed) {
        LOGGER.debug(
            "[{}][{}] Removed {} from remoteMembers list (size={})",
            localMember,
            currentPeriod,
            member,
            remoteMembers.size());
      }
    }
    if (event.isAdded()) {
      remoteMembers.add(member);
      LOGGER.debug(
          "[{}][{}] Added {} to remoteMembers list (size={})",
          localMember,
          currentPeriod,
          member,
          remoteMembers.size());
    }
  }

  // ================================================
  // ============== Helper Methods ==================
  // ================================================

  private boolean isGossipRequest(Message message) {
    return GOSSIP_REQ.equals(message.qualifier());
  }

  private Gossip createGossip(Message message) {
    return new Gossip(localMember.id(), message, gossipCounter++);
  }

  private SequenceIdCollector ensureSequence(String key) {
    return sequenceIdCollectors.computeIfAbsent(key, s -> new SequenceIdCollector());
  }

  private void spreadGossipsTo(long period, Member member) {
    // Select gossips to send
    List gossips = selectGossipsToSend(period, member);
    if (gossips.isEmpty()) {
      return; // nothing to spread
    }

    // Send gossip request
    String address = member.address();

    gossips.stream()
        .map(this::buildGossipRequestMessage)
        .forEach(
            message ->
                transport
                    .send(address, message)
                    .subscribe(
                        null,
                        ex ->
                            LOGGER.debug(
                                "[{}][{}] Failed to send GossipReq({}) to {}, cause: {}",
                                localMember,
                                period,
                                message,
                                address,
                                ex.toString())));
  }

  private List selectGossipsToSend(long period, Member member) {
    int periodsToSpread =
        ClusterMath.gossipPeriodsToSpread(config.gossipRepeatMult(), remoteMembers.size() + 1);
    return gossips.values().stream()
        .filter(
            gossipState -> gossipState.infectionPeriod() + periodsToSpread >= period) // max rounds
        .filter(gossipState -> !gossipState.isInfected(member.id())) // already infected
        .map(GossipState::gossip)
        .collect(Collectors.toList());
  }

  private List selectGossipMembers() {
    int gossipFanout = config.gossipFanout();
    if (remoteMembers.size() < gossipFanout) { // select all
      return remoteMembers;
    } else { // select random members
      // Shuffle members initially and once reached top bound
      if (remoteMembersIndex < 0 || remoteMembersIndex + gossipFanout > remoteMembers.size()) {
        Collections.shuffle(remoteMembers);
        remoteMembersIndex = 0;
      }

      // Select members
      List selectedMembers =
          gossipFanout == 1
              ? Collections.singletonList(remoteMembers.get(remoteMembersIndex))
              : remoteMembers.subList(remoteMembersIndex, remoteMembersIndex + gossipFanout);

      // Increment index and return result
      remoteMembersIndex += gossipFanout;
      return selectedMembers;
    }
  }

  private Message buildGossipRequestMessage(Gossip gossip) {
    GossipRequest gossipRequest = new GossipRequest(gossip, localMember.id());
    return Message.withData(gossipRequest).qualifier(GOSSIP_REQ).build();
  }

  private Set getGossipsToRemove(long period) {
    // Select gossips to sweep
    int periodsToSweep =
        ClusterMath.gossipPeriodsToSweep(config.gossipRepeatMult(), remoteMembers.size() + 1);
    return gossips.values().stream()
        .filter(gossipState -> period > gossipState.infectionPeriod() + periodsToSweep)
        .map(gossipState -> gossipState.gossip().gossipId())
        .collect(Collectors.toSet());
  }

  private Set getGossipsThatMostLikelyDisseminated(long period) {
    // Select gossips to spread
    int periodsToSpread =
        ClusterMath.gossipPeriodsToSpread(config.gossipRepeatMult(), remoteMembers.size() + 1);
    return gossips.values().stream()
        .filter(gossipState -> period > gossipState.infectionPeriod() + periodsToSpread)
        .map(gossipState -> gossipState.gossip().gossipId())
        .collect(Collectors.toSet());
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy