/* * Copyright 2014 WANdisco * * WANdisco licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package c5db.discovery; import c5db.codec.UdpProtostuffDecoder; import c5db.codec.UdpProtostuffEncoder; import c5db.discovery.generated.Availability; import c5db.discovery.generated.ModuleDescriptor; import c5db.interfaces.DiscoveryModule; import c5db.interfaces.ModuleInformationProvider; import c5db.interfaces.discovery.NewNodeVisible; import c5db.interfaces.discovery.NodeInfo; import c5db.interfaces.discovery.NodeInfoReply; import c5db.interfaces.discovery.NodeInfoRequest; import c5db.messages.generated.ModuleType; import c5db.util.C5Futures; import c5db.util.FiberOnly; import c5db.util.FiberSupplier; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.net.InetAddresses; import com.google.common.util.concurrent.AbstractService; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.nio.NioDatagramChannel; import org.jetbrains.annotations.NotNull; import org.jetlang.channels.MemoryChannel; import org.jetlang.channels.MemoryRequestChannel; import org.jetlang.channels.Request; import org.jetlang.channels.RequestChannel; import org.jetlang.channels.Subscriber; import org.jetlang.fibers.Fiber; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import static c5db.codec.UdpProtostuffEncoder.UdpProtostuffMessage; /** * Uses broadcast UDP packets to discover 'adjacent' nodes in the cluster. Maintains * a state table for them, and provides information to other modules as they request it. * <p> * Currently UDP broadcast has some issues on Mac OSX vs Linux. The big question, * specifically, is what happens when multiple processes bind to 255.255.255.255:PORT * and send packets? Which processes receive such packets? * <ul> * <li>On Mac OSX 10.8/9, all processes reliably receive all packets including * the originating process</li> * <li>On Linux (Ubuntu, modern) a variety of things appear to occur: * <ul> * <li>First to bind receives all packets</li> * <li>All processes receives all packets</li> * <li>No one receives any packets</li> * <li>Please fill this doc in!</li> * </ul></li> * </ul> * <p> * The beacon service needs to be refactored and different discovery methods need to be * pluggable but all behind the discovery module interface. */ public class BeaconService extends AbstractService implements DiscoveryModule { private static final Logger LOG = LoggerFactory.getLogger(BeaconService.class); private static final InetAddress BROADCAST_ADDRESS = InetAddresses.forString("255.255.255.255"); private static final int BEACON_SERVICE_INITIAL_BROADCAST_DELAY_MILLISECONDS = 2000; private static final int BEACON_SERVICE_BROADCAST_PERIOD_MILLISECONDS = 10000; @Override public ModuleType getModuleType() { return ModuleType.Discovery; } @Override public boolean hasPort() { return true; } @Override public int port() { return discoveryPort; } @Override public String acceptCommand(String commandString) { return null; } private final RequestChannel<NodeInfoRequest, NodeInfoReply> nodeInfoRequests = new MemoryRequestChannel<>(); @Override public RequestChannel<NodeInfoRequest, NodeInfoReply> getNodeInfo() { return nodeInfoRequests; } @Override public ListenableFuture<NodeInfoReply> getNodeInfo(long nodeId, ModuleType module) { SettableFuture<NodeInfoReply> future = SettableFuture.create(); fiber.execute(() -> { NodeInfo peer = peerNodeInfoMap.get(nodeId); if (peer == null) { future.set(NodeInfoReply.NO_REPLY); } else { Integer servicePort = peer.modules.get(module); if (servicePort == null) { future.set(NodeInfoReply.NO_REPLY); } else { List<String> peerAddresses = peer.availability.getAddressesList(); future.set(new NodeInfoReply(true, peerAddresses, servicePort)); } } }); return future; } @FiberOnly private void handleNodeInfoRequest(Request<NodeInfoRequest, NodeInfoReply> message) { NodeInfoRequest req = message.getRequest(); NodeInfo peer = peerNodeInfoMap.get(req.nodeId); if (peer == null) { message.reply(NodeInfoReply.NO_REPLY); return; } Integer servicePort = peer.modules.get(req.moduleType); if (servicePort == null) { message.reply(NodeInfoReply.NO_REPLY); return; } List<String> peerAddresses = peer.availability.getAddressesList(); if (peerAddresses == null || peerAddresses.isEmpty()) { message.reply(NodeInfoReply.NO_REPLY); return; } // does this module run on that peer? message.reply(new NodeInfoReply(true, peerAddresses, servicePort)); } @Override public String toString() { return "BeaconService{" + "discoveryPort=" + discoveryPort + ", nodeId=" + nodeId + '}'; } // For main system modules/pubsub stuff. private final long nodeId; private final int discoveryPort; private final EventLoopGroup eventLoopGroup; private final InetSocketAddress broadcastAddress; private final InetSocketAddress loopbackAddress; private final Map<Long, NodeInfo> peerNodeInfoMap = new HashMap<>(); private final org.jetlang.channels.Channel<Availability> incomingMessages = new MemoryChannel<>(); private final org.jetlang.channels.Channel<NewNodeVisible> newNodeVisibleChannel = new MemoryChannel<>(); private final ModuleInformationProvider moduleInformationProvider; private final FiberSupplier fiberSupplier; // These should be final, but they are initialized in doStart(). private Channel broadcastChannel = null; private Bootstrap bootstrap = null; private List<String> localIPs; private Fiber fiber; // This field is updated when modules' availability changes. It must only be accessed from the fiber. private ImmutableMap<ModuleType, Integer> onlineModuleToPortMap = ImmutableMap.of(); private class BeaconMessageHandler extends SimpleChannelInboundHandler<Availability> { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { LOG.warn("Exception, ignoring datagram", cause); } @Override protected void channelRead0(ChannelHandlerContext ctx, Availability msg) throws Exception { incomingMessages.publish(msg); } } /** * @param nodeId the id of this node. * @param discoveryPort the port to send discovery beacon messages on, and to listen to * for messages from others * @param eventLoopGroup An EventLoopGroup that's not shut down. * @param moduleInformationProvider Used to receive module availability updates */ public BeaconService(long nodeId, int discoveryPort, EventLoopGroup eventLoopGroup, ModuleInformationProvider moduleInformationProvider, FiberSupplier fiberSupplier ) { this.nodeId = nodeId; this.discoveryPort = discoveryPort; this.eventLoopGroup = eventLoopGroup; this.moduleInformationProvider = moduleInformationProvider; this.fiberSupplier = fiberSupplier; this.broadcastAddress = new InetSocketAddress(BROADCAST_ADDRESS, discoveryPort); this.loopbackAddress = new InetSocketAddress(InetAddress.getLoopbackAddress(), discoveryPort); } @Override public ListenableFuture<ImmutableMap<Long, NodeInfo>> getState() { final SettableFuture<ImmutableMap<Long, NodeInfo>> future = SettableFuture.create(); fiber.execute(() -> { future.set(getCopyOfState()); }); return future; } @Override public Subscriber<NewNodeVisible> getNewNodeNotifications() { return newNodeVisibleChannel; } private ImmutableMap<Long, NodeInfo> getCopyOfState() { return ImmutableMap.copyOf(peerNodeInfoMap); } @FiberOnly private void sendBeacon() { if (broadcastChannel == null) { LOG.debug("Channel not available yet, deferring beacon send"); return; } LOG.trace("Sending beacon broadcast message to {}", broadcastAddress); List<ModuleDescriptor> msgModules = new ArrayList<>(onlineModuleToPortMap.size()); for (ModuleType moduleType : onlineModuleToPortMap.keySet()) { msgModules.add( new ModuleDescriptor(moduleType, onlineModuleToPortMap.get(moduleType)) ); } if (!localIPs.isEmpty()) { Availability beaconMessage = new Availability(nodeId, 0, localIPs, msgModules); broadcastChannel.writeAndFlush(new UdpProtostuffMessage<>(broadcastAddress, beaconMessage)) .addListener( future -> { if (!future.isSuccess()) { LOG.warn("node {} error sending message {} to broadcast address {}", nodeId, beaconMessage, broadcastAddress); } }); } List<String> loopbackIps = Lists.newArrayList(loopbackAddress.getAddress().getHostAddress()); Availability localMessage = new Availability(nodeId, 0, loopbackIps, msgModules); broadcastChannel.writeAndFlush(new UdpProtostuffMessage<>(loopbackAddress, localMessage)) .addListener( future -> { if (!future.isSuccess()) { LOG.warn("node {} error sending message {} to loopback address", nodeId, localMessage); } }); // Fix issue #76, feed back the beacon Message to our own database: processWireMessage(localMessage); } @FiberOnly private void processWireMessage(Availability message) { LOG.trace("Got incoming message {}", message); if (message.getNodeId() == 0) { // if (!message.hasNodeId()) { LOG.error("Incoming availability message does not have node id, ignoring!"); return; } // Always just overwrite what was already there for now. // TODO consider a more sophisticated merge strategy? NodeInfo nodeInfo = new NodeInfo(message); if (!peerNodeInfoMap.containsKey(message.getNodeId())) { newNodeVisibleChannel.publish(new NewNodeVisible(message.getNodeId(), nodeInfo)); } peerNodeInfoMap.put(message.getNodeId(), nodeInfo); } @Override protected void doStart() { eventLoopGroup.next().execute(() -> { bootstrap = new Bootstrap(); bootstrap.group(eventLoopGroup) .channel(NioDatagramChannel.class) .option(ChannelOption.SO_BROADCAST, true) .option(ChannelOption.SO_REUSEADDR, true) .handler(new ChannelInitializer<DatagramChannel>() { @Override protected void initChannel(DatagramChannel ch) throws Exception { ChannelPipeline p = ch.pipeline(); p.addLast("protobufDecoder", new UdpProtostuffDecoder<>(Availability.getSchema(), false)); p.addLast("protobufEncoder", new UdpProtostuffEncoder<>(Availability.getSchema(), false)); p.addLast("beaconMessageHandler", new BeaconMessageHandler()); } }); // Wait, this is why we are in a new executor... //noinspection RedundantCast bootstrap.bind(discoveryPort).addListener((ChannelFutureListener) future -> { if (future.isSuccess()) { broadcastChannel = future.channel(); } else { LOG.error("Unable to bind! ", future.cause()); notifyFailed(future.cause()); } }); try { localIPs = getLocalIPs(); } catch (SocketException e) { LOG.error("SocketException:", e); notifyFailed(e); return; } fiber = fiberSupplier.getNewFiber(this::notifyFailed); fiber.start(); // Schedule fiber tasks and subscriptions. incomingMessages.subscribe(fiber, this::processWireMessage); nodeInfoRequests.subscribe(fiber, this::handleNodeInfoRequest); moduleInformationProvider.moduleChangeChannel().subscribe(fiber, this::updateCurrentModulePorts); if (localIPs.isEmpty()) { LOG.warn("Found no IP addresses to broadcast to other nodes; as a result, only sending to loopback"); } fiber.scheduleAtFixedRate(this::sendBeacon, BEACON_SERVICE_INITIAL_BROADCAST_DELAY_MILLISECONDS, BEACON_SERVICE_BROADCAST_PERIOD_MILLISECONDS, TimeUnit.MILLISECONDS); C5Futures.addCallback(moduleInformationProvider.getOnlineModules(), (ImmutableMap<ModuleType, Integer> onlineModuleToPortMap) -> { updateCurrentModulePorts(onlineModuleToPortMap); notifyStarted(); }, this::notifyFailed, fiber); }); } @Override protected void doStop() { fiber.dispose(); fiber = null; eventLoopGroup.next().execute(this::notifyStopped); } @FiberOnly private void updateCurrentModulePorts(ImmutableMap<ModuleType, Integer> onlineModuleToPortMap) { if (onlineModuleToPortMap == null) { notifyFailed(new NullPointerException("received null instead of a map of online modules to their ports")); return; } this.onlineModuleToPortMap = onlineModuleToPortMap; } @NotNull private List<String> getLocalIPs() throws SocketException { List<String> ips = new LinkedList<>(); for (Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); interfaces.hasMoreElements(); ) { NetworkInterface networkInterface = interfaces.nextElement(); if (networkInterface.isPointToPoint()) { continue; //ignore tunnel type interfaces } for (Enumeration<InetAddress> addrs = networkInterface.getInetAddresses(); addrs.hasMoreElements(); ) { InetAddress addr = addrs.nextElement(); if (addr.isLoopbackAddress() || addr.isLinkLocalAddress() || addr.isAnyLocalAddress()) { continue; } ips.add(addr.getHostAddress()); } } return ips; } }