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

io.servicefabric.cluster.ClusterMembership Maven / Gradle / Ivy

There is a newer version: 0.0.4
Show newest version
package io.servicefabric.cluster;

import static io.servicefabric.cluster.ClusterMemberStatus.SHUTDOWN;
import static io.servicefabric.cluster.ClusterMemberStatus.TRUSTED;
import static io.servicefabric.cluster.ClusterMembershipDataUtils.*;
import static io.servicefabric.cluster.ClusterMembershipQualifiers.*;
import static io.servicefabric.transport.TransportEndpoint.tcp;

import java.util.*;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import io.servicefabric.cluster.gossip.IGossipProtocolSpi;
import io.servicefabric.transport.protocol.Message;
import io.servicefabric.transport.TransportTypeRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import rx.Observable;
import rx.Scheduler;
import rx.Subscriber;
import rx.Subscription;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.observers.Subscribers;
import rx.subjects.PublishSubject;
import rx.subjects.SerializedSubject;
import rx.subjects.Subject;

import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import io.servicefabric.cluster.fdetector.FailureDetectorEvent;
import io.servicefabric.cluster.fdetector.IFailureDetector;
import io.servicefabric.cluster.gossip.IGossipProtocol;
import io.servicefabric.transport.ITransport;
import io.servicefabric.transport.TransportEndpoint;
import io.servicefabric.transport.TransportMessage;

public final class ClusterMembership implements IClusterMembership {
	private static final Logger LOGGER = LoggerFactory.getLogger(ClusterMembership.class);

	private IFailureDetector failureDetector;
	private IGossipProtocolSpi gossipProtocol;
	private int syncTime = 10 * 1000;
	private int syncTimeout = 3 * 1000;
	private int maxSuspectTime = 60 * 1000;
	private int maxShutdownTime = 60 * 1000;
	private String syncGroup = "default";
	private List wellknownMembers = new ArrayList<>();
	private ITransport transport;
	private final ClusterEndpoint localEndpoint;
	private final Scheduler scheduler;
	private volatile Subscription cmTask;
	private Timer timer;
	private AtomicInteger periodNbr = new AtomicInteger();
	private ClusterMembershipTable membership = new ClusterMembershipTable();
	private Subject subject = new SerializedSubject(PublishSubject.create());
	private Map localMetadata = new HashMap<>();

	/** Merges incoming SYNC data, merges it and sending back merged data with SYNC_ACK. */
	private Subscriber onSyncSubscriber = Subscribers.create(new Action1() {
		@Override
		public void call(TransportMessage transportMessage) {
			ClusterMembershipData data = (ClusterMembershipData) transportMessage.message().data();
			List updates = membership.merge(data);
			TransportEndpoint endpoint = transportMessage.originEndpoint();
			if (!updates.isEmpty()) {
				LOGGER.debug("Received Sync from {}, updates: {}", endpoint, updates);
				processUpdates(updates, true/*spread gossip*/);
			} else {
				LOGGER.debug("Received Sync from {}, no updates", endpoint);
			}
			String correlationId = transportMessage.message().correlationId();
			ClusterMembershipData syncAckData = new ClusterMembershipData(membership.asList(), syncGroup);
			Message message = new Message(SYNC_ACK, syncAckData, correlationId);
			send(endpoint, message);
		}
	});

	/** Merges FD updates and processes them. */
	private Subscriber onFdSubscriber = Subscribers.create(new Action1() {
		@Override
		public void call(FailureDetectorEvent input) {
			List updates = membership.merge(input);
			if (!updates.isEmpty()) {
				LOGGER.debug("Received FD event {}, updates: {}", input, updates);
				processUpdates(updates, true/*spread gossip*/);
			}
		}
	});

	/** Merges gossip's {@link ClusterMembershipData} (not spreading gossip further). */
	private Subscriber onGossipSubscriber = Subscribers.create(new Action1() {
		@Override
		public void call(ClusterMembershipData data) {
			List updates = membership.merge(data);
			if (!updates.isEmpty()) {
				LOGGER.debug("Received gossip, updates: {}", updates);
				processUpdates(updates, false/*spread gossip*/);
			}
		}
	});

	ClusterMembership(ClusterEndpoint localEndpoint, Scheduler scheduler) {
		this.localEndpoint = localEndpoint;
		this.scheduler = scheduler;
	}

	public void setFailureDetector(IFailureDetector failureDetector) {
		this.failureDetector = failureDetector;
	}

	public void setGossipProtocol(IGossipProtocolSpi gossipProtocol) {
		this.gossipProtocol = gossipProtocol;
	}

	public void setSyncTime(int syncTime) {
		this.syncTime = syncTime;
	}

	public void setSyncTimeout(int syncTimeout) {
		this.syncTimeout = syncTimeout;
	}

	public void setMaxSuspectTime(int maxSuspectTime) {
		this.maxSuspectTime = maxSuspectTime;
	}

	public void setMaxShutdownTime(int maxShutdownTime) {
		this.maxShutdownTime = maxShutdownTime;
	}

	public void setSyncGroup(String syncGroup) {
		this.syncGroup = syncGroup;
	}

	public void setWellknownMemberList(Collection wellknownMemberList) {
		Set set = new HashSet<>(wellknownMemberList);
		set.remove(localEndpoint.endpoint());
		this.wellknownMembers = new ArrayList<>(set);
	}

	public void setWellknownMembers(String wellknownMembers) {
		List memberList = new ArrayList<>();
		for (String token : new HashSet<>(Splitter.on(',').splitToList(wellknownMembers))) {
			if (token.length() != 0) {
				try {
					memberList.add(tcp(token));
				} catch (IllegalArgumentException e) {
					LOGGER.warn("Skipped setting wellknown_member, caught: " + e);
				}
			}
		}
		// filter accidental dublicates/locals
		Set set = new HashSet<>(memberList);
		for (Iterator i = set.iterator(); i.hasNext();) {
			TransportEndpoint endpoint = i.next();
			String hostAddress = localEndpoint.endpoint().getHostAddress();
			int port = localEndpoint.endpoint().getPort();
			if (endpoint.getPort() == port && endpoint.getHostAddress().equals(hostAddress)) {
				i.remove();
			}
		}
		setWellknownMemberList(set);
	}

	public void setTransport(ITransport transport) {
		this.transport = transport;
	}

	public void setLocalMetadata(Map localMetadata) {
		this.localMetadata = localMetadata;
	}

	public List getWellknownMembers() {
		return new ArrayList<>(wellknownMembers);
	}

	@Override
	public Observable listenUpdates() {
		return subject;
	}

	@Override
	public List members() {
		return membership.asList();
	}

	@Override
	public ClusterMember localMember() {
		return membership.get(localEndpoint);
	}

	@Override
	public void start() {
		// Start timer
		timer = new Timer();
		timer.start();

		// Register data types
		TransportTypeRegistry.getInstance().registerType(SYNC, ClusterMembershipData.class);
		TransportTypeRegistry.getInstance().registerType(SYNC_ACK, ClusterMembershipData.class);
		TransportTypeRegistry.getInstance().registerType(GOSSIP_MEMBERSHIP, ClusterMembershipData.class);

		// Register itself initially before SYNC/SYNC_ACK
		List updates = membership.merge(new ClusterMember(localEndpoint, TRUSTED, localMetadata));
		processUpdates(updates, false/*spread gossip*/);

		// Listen to SYNC requests from joining/synchronizing members
		transport.listen()
				.filter(syncFilter())
				.filter(syncGroupFilter(syncGroup))
				.map(filterData(localEndpoint))
				.subscribe(onSyncSubscriber);

		// Listen to 'suspected/trusted' events from FailureDetector
		failureDetector.listenStatus().subscribe(onFdSubscriber);

		// Listen to 'membership' message from GossipProtocol
		gossipProtocol.listen()
				.filter(gossipMembershipFilter())
				.map(gossipFilterData(localEndpoint))
				.subscribe(onGossipSubscriber);

		// Conduct 'initialization phase': take wellknown addresses, send SYNC to all and get at least one SYNC_ACK from any of them
		if (!wellknownMembers.isEmpty()) {
			LOGGER.debug("Initialization phase: making first Sync (wellknown_members={})", wellknownMembers);
			doBlockingSync(wellknownMembers);
		}

		// Schedule 'running phase': select randomly single wellknown address, send SYNC and get SYNC_ACK
		if (!wellknownMembers.isEmpty()) {
			cmTask = scheduler.createWorker().schedulePeriodically(new Action0() {
				@Override
				public void call() {
					try {
						List members = selectRandomMembers(wellknownMembers);
						LOGGER.debug("Running phase: making Sync (selected_members={}))", members);
						doSync(members, scheduler);
					} catch (Exception e) {
						LOGGER.error("Unhandled exception: {}", e, e);
					}
				}
			}, syncTime, syncTime, TimeUnit.MILLISECONDS);
		}
	}

	@Override
	public void stop() {
		if (cmTask != null) {
			cmTask.unsubscribe();
		}
		subject.onCompleted();
		onGossipSubscriber.unsubscribe();
		onSyncSubscriber.unsubscribe();
		onFdSubscriber.unsubscribe();
		timer.stop();
	}

	private void doBlockingSync(List members) {
		String period = "" + periodNbr.incrementAndGet();
		sendSync(members, period);

		Future future = transport.listen()
				.filter(syncAckFilter(period))
				.filter(syncGroupFilter(syncGroup))
				.map(filterData(localEndpoint))
				.take(1).toBlocking().toFuture();

		TransportMessage message;
		try {
			message = future.get(syncTimeout, TimeUnit.MILLISECONDS);
		} catch (Exception e) {
			LOGGER.info("Timeout getting SyncAck from {}", members);
			return;
		}
		onSyncAck(message);
	}

	private void doSync(final List members, Scheduler scheduler) {
		String period = "" + periodNbr.incrementAndGet();
		sendSync(members, period);
		transport.listen()
				.filter(syncAckFilter(period))
				.filter(syncGroupFilter(syncGroup))
				.map(filterData(localEndpoint))
				.take(1)
				.timeout(syncTimeout, TimeUnit.MILLISECONDS, scheduler)
				.subscribe(Subscribers.create(new Action1() {
					@Override
					public void call(TransportMessage transportMessage) {
						onSyncAck(transportMessage);
					}
				}, new Action1() {
					@Override
					public void call(Throwable throwable) {
						LOGGER.info("Timeout getting SyncAck from {}", members);
					}
				}));
	}

	private void sendSync(List members, String period) {
		ClusterMembershipData syncData = new ClusterMembershipData(membership.asList(), syncGroup);
		Message message = new Message(SYNC, syncData, period/*correlationId*/);
		for (TransportEndpoint endpoint : members) {
			send(endpoint, message);
		}
	}

	private void onSyncAck(TransportMessage transportMessage) {
		ClusterMembershipData data = (ClusterMembershipData) transportMessage.message().data();
		TransportEndpoint endpoint = transportMessage.originEndpoint();
		List updates = membership.merge(data);
		if (!updates.isEmpty()) {
			LOGGER.debug("Received SyncAck from {}, updates: {}", endpoint, updates);
			processUpdates(updates, true/*spread gossip*/);
		} else {
			LOGGER.debug("Received SyncAck from {}, no updates", endpoint);
		}
	}

	private void send(TransportEndpoint endpoint, Message message) {
		transport.to(endpoint).send(message, null);
	}

	private List selectRandomMembers(List members) {
		List list = new ArrayList<>(members);
		Collections.shuffle(list, ThreadLocalRandom.current());
		return ImmutableList.of(list.get(ThreadLocalRandom.current().nextInt(list.size())));
	}

	/**
	 * Takes {@code updates} and process them in next order:
	 * 
    *
  • recalculates 'cluster members' for {@link #gossipProtocol} and {@link #failureDetector} * by filtering out {@code REMOVED/SHUTDOWN} members *
  • *
  • if {@code spreadGossip} was set {@code true} -- converts {@code updates} to {@link ClusterMembershipData} * and send it to cluster via {@link #gossipProtocol}
  • *
  • publishes updates locally (see {@link #listenUpdates()})
  • *
  • iterates on {@code updates}, if {@code update} become {@code SUSPECTED} -- * schedules a timer ({@link #maxSuspectTime}) to remove the member (on {@code TRUSTED} -- cancels the timer)
  • *
  • iterates on {@code updates}, if {@code update} become {@code SHUTDOWN} -- * schedules a timer ({@link #maxShutdownTime}) to remove the member
  • *
* * @param updates list of updates after merge * @param spreadGossip flag indicating should updates be gossiped to cluster */ private void processUpdates(List updates, boolean spreadGossip) { if (updates.isEmpty()) return; // Reset cluster members on FailureDetector and Gossip Map members = membership.getTrustedOrSuspected(); failureDetector.setClusterMembers(members.keySet()); gossipProtocol.setClusterMembers(members.keySet()); // Publish updates to cluster if (spreadGossip) { gossipProtocol.spread(GOSSIP_MEMBERSHIP, new ClusterMembershipData(updates, syncGroup)); } // Publish updates locally for (ClusterMember update : updates) { subject.onNext(update); } // Check state transition for (final ClusterMember member : updates) { LOGGER.debug("Member {} became {}", member.endpoint(), member.status()); switch (member.status()) { case SUSPECTED: failureDetector.suspect(member.endpoint()); timer.schedule(member.endpoint().endpointId(), new Runnable() { @Override public void run() { LOGGER.debug("Time to remove SUSPECTED member={} from membership", member.endpoint()); processUpdates(membership.remove(member.endpoint()), false/*spread gossip*/); } }, maxSuspectTime, TimeUnit.MILLISECONDS); break; case TRUSTED: failureDetector.trust(member.endpoint()); timer.cancel(member.endpoint().endpointId()); break; case SHUTDOWN: timer.schedule(new Runnable() { @Override public void run() { LOGGER.debug("Time to remove SHUTDOWN member={} from membership", member.endpoint()); membership.remove(member.endpoint()); } }, maxShutdownTime, TimeUnit.MILLISECONDS); break; } } } @Override public void publishShutdown() { ClusterMember r1 = new ClusterMember(localEndpoint, SHUTDOWN, localMetadata); gossipProtocol.spread(GOSSIP_MEMBERSHIP, new ClusterMembershipData(ImmutableList.of(r1), syncGroup)); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy