001/** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.activemq.transport.discovery.multicast; 019 020import java.io.IOException; 021import java.net.DatagramPacket; 022import java.net.InetAddress; 023import java.net.InetSocketAddress; 024import java.net.InterfaceAddress; 025import java.net.MulticastSocket; 026import java.net.NetworkInterface; 027import java.net.SocketAddress; 028import java.net.SocketException; 029import java.net.SocketTimeoutException; 030import java.net.URI; 031import java.util.ArrayList; 032import java.util.Enumeration; 033import java.util.Iterator; 034import java.util.List; 035import java.util.Map; 036import java.util.concurrent.ConcurrentHashMap; 037import java.util.concurrent.ExecutorService; 038import java.util.concurrent.LinkedBlockingQueue; 039import java.util.concurrent.ThreadFactory; 040import java.util.concurrent.ThreadPoolExecutor; 041import java.util.concurrent.TimeUnit; 042import java.util.concurrent.atomic.AtomicBoolean; 043 044import org.apache.activemq.command.DiscoveryEvent; 045import org.apache.activemq.transport.discovery.DiscoveryAgent; 046import org.apache.activemq.transport.discovery.DiscoveryListener; 047import org.apache.activemq.util.ThreadPoolUtils; 048import org.slf4j.Logger; 049import org.slf4j.LoggerFactory; 050 051/** 052 * A {@link DiscoveryAgent} using a multicast address and heartbeat packets 053 * encoded using any wireformat, but openwire by default. 054 * 055 * 056 */ 057public class MulticastDiscoveryAgent implements DiscoveryAgent, Runnable { 058 059 public static final String DEFAULT_DISCOVERY_URI_STRING = "multicast://239.255.2.3:6155"; 060 public static final String DEFAULT_HOST_STR = "default"; 061 public static final String DEFAULT_HOST_IP = System.getProperty("activemq.partition.discovery", "239.255.2.3"); 062 public static final int DEFAULT_PORT = 6155; 063 064 private static final Logger LOG = LoggerFactory.getLogger(MulticastDiscoveryAgent.class); 065 private static final String TYPE_SUFFIX = "ActiveMQ-4."; 066 private static final String ALIVE = "alive."; 067 private static final String DEAD = "dead."; 068 private static final String DELIMITER = "%"; 069 private static final int BUFF_SIZE = 8192; 070 private static final int DEFAULT_IDLE_TIME = 500; 071 private static final int HEARTBEAT_MISS_BEFORE_DEATH = 10; 072 073 private long initialReconnectDelay = 1000 * 5; 074 private long maxReconnectDelay = 1000 * 30; 075 private long backOffMultiplier = 2; 076 private boolean useExponentialBackOff; 077 private int maxReconnectAttempts; 078 079 private int timeToLive = 1; 080 private boolean loopBackMode; 081 private Map<String, RemoteBrokerData> brokersByService = new ConcurrentHashMap<String, RemoteBrokerData>(); 082 private String group = "default"; 083 private URI discoveryURI; 084 private InetAddress inetAddress; 085 private SocketAddress sockAddress; 086 private DiscoveryListener discoveryListener; 087 private String selfService; 088 private MulticastSocket mcast; 089 private Thread runner; 090 private long keepAliveInterval = DEFAULT_IDLE_TIME; 091 private String mcInterface; 092 private String mcNetworkInterface; 093 private String mcJoinNetworkInterface; 094 private long lastAdvertizeTime; 095 private AtomicBoolean started = new AtomicBoolean(false); 096 private boolean reportAdvertizeFailed = true; 097 private ExecutorService executor = null; 098 099 class RemoteBrokerData extends DiscoveryEvent { 100 long lastHeartBeat; 101 long recoveryTime; 102 int failureCount; 103 boolean failed; 104 105 public RemoteBrokerData(String brokerName, String service) { 106 super(service); 107 setBrokerName(brokerName); 108 this.lastHeartBeat = System.currentTimeMillis(); 109 } 110 111 public synchronized void updateHeartBeat() { 112 lastHeartBeat = System.currentTimeMillis(); 113 114 // Consider that the broker recovery has succeeded if it has not 115 // failed in 60 seconds. 116 if (!failed && failureCount > 0 && (lastHeartBeat - recoveryTime) > 1000 * 60) { 117 if (LOG.isDebugEnabled()) { 118 LOG.debug("I now think that the " + serviceName + " service has recovered."); 119 } 120 failureCount = 0; 121 recoveryTime = 0; 122 } 123 } 124 125 public synchronized long getLastHeartBeat() { 126 return lastHeartBeat; 127 } 128 129 public synchronized boolean markFailed() { 130 if (!failed) { 131 failed = true; 132 failureCount++; 133 134 long reconnectDelay; 135 if (!useExponentialBackOff) { 136 reconnectDelay = initialReconnectDelay; 137 } else { 138 reconnectDelay = (long)Math.pow(backOffMultiplier, failureCount); 139 if (reconnectDelay > maxReconnectDelay) { 140 reconnectDelay = maxReconnectDelay; 141 } 142 } 143 144 if (LOG.isDebugEnabled()) { 145 LOG.debug("Remote failure of " + serviceName + " while still receiving multicast advertisements. Advertising events will be suppressed for " + reconnectDelay 146 + " ms, the current failure count is: " + failureCount); 147 } 148 149 recoveryTime = System.currentTimeMillis() + reconnectDelay; 150 return true; 151 } 152 return false; 153 } 154 155 /** 156 * @return true if this broker is marked failed and it is now the right 157 * time to start recovery. 158 */ 159 public synchronized boolean doRecovery() { 160 if (!failed) { 161 return false; 162 } 163 164 // Are we done trying to recover this guy? 165 if (maxReconnectAttempts > 0 && failureCount > maxReconnectAttempts) { 166 if (LOG.isDebugEnabled()) { 167 LOG.debug("Max reconnect attempts of the " + serviceName + " service has been reached."); 168 } 169 return false; 170 } 171 172 // Is it not yet time? 173 if (System.currentTimeMillis() < recoveryTime) { 174 return false; 175 } 176 177 if (LOG.isDebugEnabled()) { 178 LOG.debug("Resuming event advertisement of the " + serviceName + " service."); 179 } 180 failed = false; 181 return true; 182 } 183 184 public boolean isFailed() { 185 return failed; 186 } 187 } 188 189 /** 190 * Set the discovery listener 191 * 192 * @param listener 193 */ 194 public void setDiscoveryListener(DiscoveryListener listener) { 195 this.discoveryListener = listener; 196 } 197 198 /** 199 * register a service 200 */ 201 public void registerService(String name) throws IOException { 202 this.selfService = name; 203 if (started.get()) { 204 doAdvertizeSelf(); 205 } 206 } 207 208 /** 209 * @return Returns the loopBackMode. 210 */ 211 public boolean isLoopBackMode() { 212 return loopBackMode; 213 } 214 215 /** 216 * @param loopBackMode The loopBackMode to set. 217 */ 218 public void setLoopBackMode(boolean loopBackMode) { 219 this.loopBackMode = loopBackMode; 220 } 221 222 /** 223 * @return Returns the timeToLive. 224 */ 225 public int getTimeToLive() { 226 return timeToLive; 227 } 228 229 /** 230 * @param timeToLive The timeToLive to set. 231 */ 232 public void setTimeToLive(int timeToLive) { 233 this.timeToLive = timeToLive; 234 } 235 236 /** 237 * @return the discoveryURI 238 */ 239 public URI getDiscoveryURI() { 240 return discoveryURI; 241 } 242 243 /** 244 * Set the discoveryURI 245 * 246 * @param discoveryURI 247 */ 248 public void setDiscoveryURI(URI discoveryURI) { 249 this.discoveryURI = discoveryURI; 250 } 251 252 public long getKeepAliveInterval() { 253 return keepAliveInterval; 254 } 255 256 public void setKeepAliveInterval(long keepAliveInterval) { 257 this.keepAliveInterval = keepAliveInterval; 258 } 259 260 public void setInterface(String mcInterface) { 261 this.mcInterface = mcInterface; 262 } 263 264 public void setNetworkInterface(String mcNetworkInterface) { 265 this.mcNetworkInterface = mcNetworkInterface; 266 } 267 268 public void setJoinNetworkInterface(String mcJoinNetwrokInterface) { 269 this.mcJoinNetworkInterface = mcJoinNetwrokInterface; 270 } 271 272 /** 273 * start the discovery agent 274 * 275 * @throws Exception 276 */ 277 public void start() throws Exception { 278 279 if (started.compareAndSet(false, true)) { 280 281 if (group == null || group.length() == 0) { 282 throw new IOException("You must specify a group to discover"); 283 } 284 String type = getType(); 285 if (!type.endsWith(".")) { 286 LOG.warn("The type '" + type + "' should end with '.' to be a valid Discovery type"); 287 type += "."; 288 } 289 290 if (discoveryURI == null) { 291 discoveryURI = new URI(DEFAULT_DISCOVERY_URI_STRING); 292 } 293 294 if (LOG.isTraceEnabled()) { 295 LOG.trace("start - discoveryURI = " + discoveryURI); 296 } 297 298 String myHost = discoveryURI.getHost(); 299 int myPort = discoveryURI.getPort(); 300 301 if (DEFAULT_HOST_STR.equals(myHost)) { 302 myHost = DEFAULT_HOST_IP; 303 } 304 305 if (myPort < 0) { 306 myPort = DEFAULT_PORT; 307 } 308 309 if (LOG.isTraceEnabled()) { 310 LOG.trace("start - myHost = " + myHost); 311 LOG.trace("start - myPort = " + myPort); 312 LOG.trace("start - group = " + group); 313 LOG.trace("start - interface = " + mcInterface); 314 LOG.trace("start - network interface = " + mcNetworkInterface); 315 LOG.trace("start - join network interface = " + mcJoinNetworkInterface); 316 } 317 318 this.inetAddress = InetAddress.getByName(myHost); 319 this.sockAddress = new InetSocketAddress(this.inetAddress, myPort); 320 mcast = new MulticastSocket(myPort); 321 mcast.setLoopbackMode(loopBackMode); 322 mcast.setTimeToLive(getTimeToLive()); 323 if (mcJoinNetworkInterface != null) { 324 mcast.joinGroup(sockAddress, NetworkInterface.getByName(mcJoinNetworkInterface)); 325 } 326 else { 327 mcast.setNetworkInterface(findNetworkInterface()); 328 mcast.joinGroup(inetAddress); 329 } 330 mcast.setSoTimeout((int)keepAliveInterval); 331 if (mcInterface != null) { 332 mcast.setInterface(InetAddress.getByName(mcInterface)); 333 } 334 if (mcNetworkInterface != null) { 335 mcast.setNetworkInterface(NetworkInterface.getByName(mcNetworkInterface)); 336 } 337 runner = new Thread(this); 338 runner.setName(this.toString() + ":" + runner.getName()); 339 runner.setDaemon(true); 340 runner.start(); 341 doAdvertizeSelf(); 342 } 343 } 344 345 private NetworkInterface findNetworkInterface() throws SocketException { 346 Enumeration<NetworkInterface> ifcs = NetworkInterface.getNetworkInterfaces(); 347 List<NetworkInterface> possibles = new ArrayList<NetworkInterface>(); 348 if (ifcs != null) { 349 while (ifcs.hasMoreElements()) { 350 NetworkInterface ni = ifcs.nextElement(); 351 try { 352 if (ni.supportsMulticast() 353 && ni.isUp()) { 354 for (InterfaceAddress ia : ni.getInterfaceAddresses()) { 355 if (ia != null && ia.getAddress() instanceof java.net.Inet4Address 356 && !ia.getAddress().isLoopbackAddress() 357 && (ni.getDisplayName()==null || !ni.getDisplayName().startsWith("vnic"))) { 358 possibles.add(ni); 359 } 360 } 361 } 362 } catch (SocketException ignored) {} 363 } 364 } 365 return possibles.isEmpty() ? null : possibles.get(possibles.size() - 1); 366 } 367 368 /** 369 * stop the channel 370 * 371 * @throws Exception 372 */ 373 public void stop() throws Exception { 374 if (started.compareAndSet(true, false)) { 375 doAdvertizeSelf(); 376 if (mcast != null) { 377 mcast.close(); 378 } 379 if (runner != null) { 380 runner.interrupt(); 381 } 382 if (executor != null) { 383 ThreadPoolUtils.shutdownNow(executor); 384 executor = null; 385 } 386 } 387 } 388 389 public String getType() { 390 return group + "." + TYPE_SUFFIX; 391 } 392 393 public void run() { 394 byte[] buf = new byte[BUFF_SIZE]; 395 DatagramPacket packet = new DatagramPacket(buf, 0, buf.length); 396 while (started.get()) { 397 doTimeKeepingServices(); 398 try { 399 mcast.receive(packet); 400 if (packet.getLength() > 0) { 401 String str = new String(packet.getData(), packet.getOffset(), packet.getLength()); 402 processData(str); 403 } 404 } catch (SocketTimeoutException se) { 405 // ignore 406 } catch (IOException e) { 407 if (started.get()) { 408 LOG.error("failed to process packet: " + e); 409 } 410 } 411 } 412 } 413 414 private void processData(String str) { 415 if (discoveryListener != null) { 416 if (str.startsWith(getType())) { 417 String payload = str.substring(getType().length()); 418 if (payload.startsWith(ALIVE)) { 419 String brokerName = getBrokerName(payload.substring(ALIVE.length())); 420 String service = payload.substring(ALIVE.length() + brokerName.length() + 2); 421 processAlive(brokerName, service); 422 } else { 423 String brokerName = getBrokerName(payload.substring(DEAD.length())); 424 String service = payload.substring(DEAD.length() + brokerName.length() + 2); 425 processDead(service); 426 } 427 } 428 } 429 } 430 431 private void doTimeKeepingServices() { 432 if (started.get()) { 433 long currentTime = System.currentTimeMillis(); 434 if (currentTime < lastAdvertizeTime || ((currentTime - keepAliveInterval) > lastAdvertizeTime)) { 435 doAdvertizeSelf(); 436 lastAdvertizeTime = currentTime; 437 } 438 doExpireOldServices(); 439 } 440 } 441 442 private void doAdvertizeSelf() { 443 if (selfService != null) { 444 String payload = getType(); 445 payload += started.get() ? ALIVE : DEAD; 446 payload += DELIMITER + "localhost" + DELIMITER; 447 payload += selfService; 448 try { 449 byte[] data = payload.getBytes(); 450 DatagramPacket packet = new DatagramPacket(data, 0, data.length, sockAddress); 451 mcast.send(packet); 452 } catch (IOException e) { 453 // If a send fails, chances are all subsequent sends will fail 454 // too.. No need to keep reporting the 455 // same error over and over. 456 if (reportAdvertizeFailed) { 457 reportAdvertizeFailed = false; 458 LOG.error("Failed to advertise our service: " + payload, e); 459 if ("Operation not permitted".equals(e.getMessage())) { 460 LOG.error("The 'Operation not permitted' error has been know to be caused by improper firewall/network setup. " 461 + "Please make sure that the OS is properly configured to allow multicast traffic over: " + mcast.getLocalAddress()); 462 } 463 } 464 } 465 } 466 } 467 468 private void processAlive(String brokerName, String service) { 469 if (selfService == null || !service.equals(selfService)) { 470 RemoteBrokerData data = brokersByService.get(service); 471 if (data == null) { 472 data = new RemoteBrokerData(brokerName, service); 473 brokersByService.put(service, data); 474 fireServiceAddEvent(data); 475 doAdvertizeSelf(); 476 } else { 477 data.updateHeartBeat(); 478 if (data.doRecovery()) { 479 fireServiceAddEvent(data); 480 } 481 } 482 } 483 } 484 485 private void processDead(String service) { 486 if (!service.equals(selfService)) { 487 RemoteBrokerData data = brokersByService.remove(service); 488 if (data != null && !data.isFailed()) { 489 fireServiceRemovedEvent(data); 490 } 491 } 492 } 493 494 private void doExpireOldServices() { 495 long expireTime = System.currentTimeMillis() - (keepAliveInterval * HEARTBEAT_MISS_BEFORE_DEATH); 496 for (Iterator<RemoteBrokerData> i = brokersByService.values().iterator(); i.hasNext();) { 497 RemoteBrokerData data = i.next(); 498 if (data.getLastHeartBeat() < expireTime) { 499 processDead(data.getServiceName()); 500 } 501 } 502 } 503 504 private String getBrokerName(String str) { 505 String result = null; 506 int start = str.indexOf(DELIMITER); 507 if (start >= 0) { 508 int end = str.indexOf(DELIMITER, start + 1); 509 result = str.substring(start + 1, end); 510 } 511 return result; 512 } 513 514 public void serviceFailed(DiscoveryEvent event) throws IOException { 515 RemoteBrokerData data = brokersByService.get(event.getServiceName()); 516 if (data != null && data.markFailed()) { 517 fireServiceRemovedEvent(data); 518 } 519 } 520 521 private void fireServiceRemovedEvent(final RemoteBrokerData data) { 522 if (discoveryListener != null && started.get()) { 523 // Have the listener process the event async so that 524 // he does not block this thread since we are doing time sensitive 525 // processing of events. 526 getExecutor().execute(new Runnable() { 527 public void run() { 528 DiscoveryListener discoveryListener = MulticastDiscoveryAgent.this.discoveryListener; 529 if (discoveryListener != null) { 530 discoveryListener.onServiceRemove(data); 531 } 532 } 533 }); 534 } 535 } 536 537 private void fireServiceAddEvent(final RemoteBrokerData data) { 538 if (discoveryListener != null && started.get()) { 539 540 // Have the listener process the event async so that 541 // he does not block this thread since we are doing time sensitive 542 // processing of events. 543 getExecutor().execute(new Runnable() { 544 public void run() { 545 DiscoveryListener discoveryListener = MulticastDiscoveryAgent.this.discoveryListener; 546 if (discoveryListener != null) { 547 discoveryListener.onServiceAdd(data); 548 } 549 } 550 }); 551 } 552 } 553 554 private ExecutorService getExecutor() { 555 if (executor == null) { 556 final String threadName = "Notifier-" + this.toString(); 557 executor = new ThreadPoolExecutor(1, 1, 30, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), new ThreadFactory() { 558 public Thread newThread(Runnable runable) { 559 Thread t = new Thread(runable, threadName); 560 t.setDaemon(true); 561 return t; 562 } 563 }); 564 } 565 return executor; 566 } 567 568 public long getBackOffMultiplier() { 569 return backOffMultiplier; 570 } 571 572 public void setBackOffMultiplier(long backOffMultiplier) { 573 this.backOffMultiplier = backOffMultiplier; 574 } 575 576 public long getInitialReconnectDelay() { 577 return initialReconnectDelay; 578 } 579 580 public void setInitialReconnectDelay(long initialReconnectDelay) { 581 this.initialReconnectDelay = initialReconnectDelay; 582 } 583 584 public int getMaxReconnectAttempts() { 585 return maxReconnectAttempts; 586 } 587 588 public void setMaxReconnectAttempts(int maxReconnectAttempts) { 589 this.maxReconnectAttempts = maxReconnectAttempts; 590 } 591 592 public long getMaxReconnectDelay() { 593 return maxReconnectDelay; 594 } 595 596 public void setMaxReconnectDelay(long maxReconnectDelay) { 597 this.maxReconnectDelay = maxReconnectDelay; 598 } 599 600 public boolean isUseExponentialBackOff() { 601 return useExponentialBackOff; 602 } 603 604 public void setUseExponentialBackOff(boolean useExponentialBackOff) { 605 this.useExponentialBackOff = useExponentialBackOff; 606 } 607 608 public void setGroup(String group) { 609 this.group = group; 610 } 611 612 @Override 613 public String toString() { 614 return "MulticastDiscoveryAgent-" 615 + (selfService != null ? "advertise:" + selfService : "listener:" + this.discoveryListener); 616 } 617}