package com.hazelcast.samples.eureka.partition.groups; import com.hazelcast.nio.Address; import com.hazelcast.spi.discovery.DiscoveryNode; import com.hazelcast.spi.discovery.SimpleDiscoveryNode; import com.hazelcast.spi.discovery.integration.DiscoveryService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import static com.hazelcast.spi.partitiongroup.PartitionGroupMetaData.PARTITION_GROUP_ZONE; /** * A Hazelcast discovery service using Eureka. * <p> * Eureka conectivity is provided by Spring, injecting an Eureka {@link DiscoveryClient} * object. * <p> * This service does 2 things. * <ol> * <li>{@link #discoverLocalMetadata} * Called first, this method obtains the partition grouping data already stored in * Eureka by {@code my-eureka-server}. If this instance is a server and therefore storing * partitions, it needs this information to determine which partitions it can take.</li> * <li>{@link #discoverNodes} * Called next, this method finds the nodes in the cluster to connect to.</li> * </ol> */ @Component @Slf4j @SuppressWarnings("checkstyle:visibilitymodifier") public class MyEurekaDiscoveryService implements DiscoveryService { private static final String YML_SEPARATOR = "."; @Value("${eureka.client.registerWithEureka:true}") public boolean registerWithEureka; @Autowired private DiscoveryClient discoveryClient; /** * If we are a server {@code registerWithEureka==true} we need to * look up the partition group metadata, so we know which partitions * this server can host. * <p> * If we are a client we don't host partitions so can skip this. * <p> * <b>NOTE: <i>METHOD:</i></b> The method for setting the partition * group is that the metadata lists a host & port pairing with the * zone each should use. The zone is explicitly specified by * external configuration. * * @return A map with one entry, the partition group for this host. */ @Override public Map<String, Object> discoverLocalMetadata() { HashMap<String, Object> result = new HashMap<>(); /* The metadata is only for partition groups, so we don't need this if we are * a Hazelcast client. Hazelcast servers register with Eureka, clients only read. */ if (!registerWithEureka) { return result; } log.info("\n--------------------------------------------------------------------------------"); log.info("discoverLocalMetadata(): Hazelcast lookup to Eureka : start"); // Find the web port this process is using. String port = String.valueOf(discoveryClient.getLocalServiceInstance().getPort()); // Since this is a one machine example, we know which machine we are on. String hostPort = "localhost" + YML_SEPARATOR + port; discoveryClient.getInstances(Constants.CLUSTER_NAME).forEach( (ServiceInstance serviceInstance) -> { String zone = serviceInstance.getMetadata().get( Constants.HAZELCAST_ZONE_METADATA_KEY + YML_SEPARATOR + hostPort); if (zone != null) { log.info("discoverLocalMetadata(): found zone '{}' for '{}'", zone, hostPort); result.put(PARTITION_GROUP_ZONE, zone); } }); log.info("discoverLocalMetadata(): Hazelcast lookup to Eureka : end"); log.info("\n--------------------------------------------------------------------------------"); // No match will cause problems if (result.isEmpty()) { String message = String.format("discoverLocalMetadata(): found no zone for '%s'", hostPort); throw new RuntimeException(message); } else { return result; } } /** * Provide a way to discover the other nodes in the cluster, that this * instance should connect to. * <p> * Using the {@link DiscoveryClient} injected by Spring, we can connect * to Eureka and find all the nodes <b><u>currently</u></b> registered * with Eureka. * <p> * When this instance starts this method is run before this node has * registered itself with Eureka, so the list is essentially the nodes * already in the cluster. If it is an empty list, this instance is the * first, and when it gets to the registration step it will record itself * for other servers to find. * <p> * The ordering here is out of our control. Registering with Eureka * happens when the application is fully up (has connected with cluster * members). * <p> * As a consequence of this, there is a race condition. If the first * two servers start at roughly the same time, they will run this method * at roughly the same time, and <i>before</i> each other has run the * registration step. So each will get an empty list from Eureka. * * @return A list of {@code host}:{@code port} pairs. */ @Override public Iterable<DiscoveryNode> discoverNodes() { List<DiscoveryNode> nodes = new ArrayList<>(); log.info("\n--------------------------------------------------------------------------------"); log.info("discoverNodes(): Hazelcast lookup to Eureka : start"); // A mildly unnecessary lambda, to ensure we're not on the dark ages of Java 7 discoveryClient.getInstances(Constants.CLUSTER_NAME).forEach( (ServiceInstance serviceInstance) -> { try { String host = serviceInstance.getMetadata().get("instanceHost"); String port = serviceInstance.getMetadata().get("instancePort"); if (host != null && port != null) { log.info("discoverNodes(): -> found {}:{}", host, port); Address address = new Address(host, Integer.valueOf(port)); DiscoveryNode discoveryNode = new SimpleDiscoveryNode(address); nodes.add(discoveryNode); } } catch (Exception e) { log.error("discoverNodes()", e); } }); log.info("discoverNodes(): Hazelcast lookup to Eureka : end. Found {} item{}", nodes.size(), (nodes.size() == 1 ? "" : "s")); log.info("--------------------------------------------------------------------------------\n"); return nodes; } /** * Part of the interface, but not used. */ @Override public void start() { } /** * Part of the interface, but not used. */ @Override public void destroy() { } }