001package org.apache.commons.jcs3.utils.discovery; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import java.io.IOException; 023import java.net.Inet4Address; 024import java.net.Inet6Address; 025import java.net.InetAddress; 026import java.net.NetworkInterface; 027import java.net.UnknownHostException; 028import java.util.ArrayList; 029import java.util.Enumeration; 030import java.util.HashSet; 031import java.util.Set; 032import java.util.concurrent.ConcurrentHashMap; 033import java.util.concurrent.ConcurrentMap; 034import java.util.concurrent.CopyOnWriteArraySet; 035import java.util.concurrent.ScheduledExecutorService; 036import java.util.concurrent.ScheduledFuture; 037import java.util.concurrent.TimeUnit; 038import java.util.concurrent.atomic.AtomicBoolean; 039 040import org.apache.commons.jcs3.engine.behavior.IElementSerializer; 041import org.apache.commons.jcs3.engine.behavior.IRequireScheduler; 042import org.apache.commons.jcs3.engine.behavior.IShutdownObserver; 043import org.apache.commons.jcs3.log.Log; 044import org.apache.commons.jcs3.log.LogManager; 045import org.apache.commons.jcs3.utils.discovery.behavior.IDiscoveryListener; 046import org.apache.commons.jcs3.utils.net.HostNameUtil; 047import org.apache.commons.jcs3.utils.serialization.StandardSerializer; 048 049/** 050 * This service creates a listener that can create lateral caches and add them to the no wait list. 051 * <p> 052 * It also creates a sender that periodically broadcasts its availability. 053 * </p> 054 * <p> 055 * The sender also broadcasts a request for other caches to broadcast their addresses. 056 * </p> 057 */ 058public class UDPDiscoveryService 059 implements IShutdownObserver, IRequireScheduler 060{ 061 /** The logger */ 062 private static final Log log = LogManager.getLog( UDPDiscoveryService.class ); 063 064 /** thread that listens for messages */ 065 private Thread udpReceiverThread; 066 067 /** the runnable that the receiver thread runs */ 068 private UDPDiscoveryReceiver receiver; 069 070 /** attributes */ 071 private UDPDiscoveryAttributes udpDiscoveryAttributes; 072 073 /** Used to serialize messages */ 074 private final IElementSerializer serializer; 075 076 /** is this shut down? */ 077 private final AtomicBoolean shutdown = new AtomicBoolean(false); 078 079 /** This is a set of services that have been discovered. */ 080 private final ConcurrentMap<Integer, DiscoveredService> discoveredServices = 081 new ConcurrentHashMap<>(); 082 083 /** This a list of regions that are configured to use discovery. */ 084 private final Set<String> cacheNames = new CopyOnWriteArraySet<>(); 085 086 /** Set of listeners. */ 087 private final Set<IDiscoveryListener> discoveryListeners = new CopyOnWriteArraySet<>(); 088 089 /** Handle to cancel the scheduled broadcast task */ 090 private ScheduledFuture<?> broadcastTaskFuture; 091 092 /** Handle to cancel the scheduled cleanup task */ 093 private ScheduledFuture<?> cleanupTaskFuture; 094 095 /** 096 * Constructor 097 * 098 * @param attributes settings of the service 099 * @deprecated Specify serializer implementation explicitly 100 */ 101 @Deprecated 102 public UDPDiscoveryService(final UDPDiscoveryAttributes attributes) 103 { 104 this(attributes, new StandardSerializer()); 105 } 106 107 /** 108 * Constructor 109 * 110 * @param attributes settings of service 111 * @param serializer the serializer to use to send and receive messages 112 * @since 3.1 113 */ 114 public UDPDiscoveryService(final UDPDiscoveryAttributes attributes, IElementSerializer serializer) 115 { 116 this.udpDiscoveryAttributes = attributes.clone(); 117 this.serializer = serializer; 118 119 try 120 { 121 InetAddress multicastAddress = InetAddress.getByName( 122 getUdpDiscoveryAttributes().getUdpDiscoveryAddr()); 123 124 // Set service address if still empty 125 if (getUdpDiscoveryAttributes().getServiceAddress() == null || 126 getUdpDiscoveryAttributes().getServiceAddress().isEmpty()) 127 { 128 // Use same interface as for multicast 129 NetworkInterface serviceInterface = null; 130 if (getUdpDiscoveryAttributes().getUdpDiscoveryInterface() != null) 131 { 132 serviceInterface = NetworkInterface.getByName( 133 getUdpDiscoveryAttributes().getUdpDiscoveryInterface()); 134 } 135 else 136 { 137 serviceInterface = HostNameUtil.getMulticastNetworkInterface(); 138 } 139 140 try 141 { 142 InetAddress serviceAddress = null; 143 144 for (Enumeration<InetAddress> addresses = serviceInterface.getInetAddresses(); 145 addresses.hasMoreElements();) 146 { 147 serviceAddress = addresses.nextElement(); 148 149 if (multicastAddress instanceof Inet6Address) 150 { 151 if (serviceAddress instanceof Inet6Address && 152 !serviceAddress.isLoopbackAddress() && 153 !serviceAddress.isMulticastAddress() && 154 serviceAddress.isLinkLocalAddress()) 155 { 156 // if Multicast uses IPv6, try to publish our IPv6 address 157 break; 158 } 159 } 160 else 161 { 162 if (serviceAddress instanceof Inet4Address && 163 !serviceAddress.isLoopbackAddress() && 164 !serviceAddress.isMulticastAddress() && 165 serviceAddress.isSiteLocalAddress()) 166 { 167 // if Multicast uses IPv4, try to publish our IPv4 address 168 break; 169 } 170 } 171 } 172 173 if (serviceAddress == null) 174 { 175 // Nothing found for given interface, fall back 176 serviceAddress = HostNameUtil.getLocalHostLANAddress(); 177 } 178 179 getUdpDiscoveryAttributes().setServiceAddress(serviceAddress.getHostAddress()); 180 } 181 catch ( final UnknownHostException e ) 182 { 183 log.error( "Couldn't get local host address", e ); 184 } 185 } 186 187 // todo need some kind of recovery here. 188 receiver = new UDPDiscoveryReceiver( this, 189 getUdpDiscoveryAttributes().getUdpDiscoveryInterface(), 190 multicastAddress, 191 getUdpDiscoveryAttributes().getUdpDiscoveryPort() ); 192 } 193 catch ( final IOException e ) 194 { 195 log.error( "Problem creating UDPDiscoveryReceiver, address [{0}] " 196 + "port [{1}] we won't be able to find any other caches", 197 getUdpDiscoveryAttributes().getUdpDiscoveryAddr(), 198 getUdpDiscoveryAttributes().getUdpDiscoveryPort(), e ); 199 } 200 201 // initiate sender broadcast 202 initiateBroadcast(); 203 } 204 205 /** 206 * @see org.apache.commons.jcs3.engine.behavior.IRequireScheduler#setScheduledExecutorService(java.util.concurrent.ScheduledExecutorService) 207 */ 208 @Override 209 public void setScheduledExecutorService(final ScheduledExecutorService scheduledExecutor) 210 { 211 this.broadcastTaskFuture = scheduledExecutor.scheduleAtFixedRate( 212 this::serviceRequestBroadcast, 0, 15, TimeUnit.SECONDS); 213 214 /** removes things that have been idle for too long */ 215 // I'm going to use this as both, but it could happen 216 // that something could hang around twice the time using this as the 217 // delay and the idle time. 218 this.cleanupTaskFuture = scheduledExecutor.scheduleAtFixedRate( 219 this::cleanup, 0, 220 getUdpDiscoveryAttributes().getMaxIdleTimeSec(), TimeUnit.SECONDS); 221 } 222 223 /** 224 * This goes through the list of services and removes those that we haven't heard from in longer 225 * than the max idle time. 226 * 227 * @since 3.1 228 */ 229 protected void cleanup() 230 { 231 final long now = System.currentTimeMillis(); 232 233 // the listeners need to be notified. 234 getDiscoveredServices().stream() 235 .filter(service -> { 236 if (now - service.getLastHearFromTime() > getUdpDiscoveryAttributes().getMaxIdleTimeSec() * 1000) 237 { 238 log.info( "Removing service, since we haven't heard from it in " 239 + "{0} seconds. service = {1}", 240 getUdpDiscoveryAttributes().getMaxIdleTimeSec(), service ); 241 return true; 242 } 243 244 return false; 245 }) 246 // remove the bad ones 247 // call this so the listeners get notified 248 .forEach(this::removeDiscoveredService); 249 } 250 251 /** 252 * Initial request that the other caches let it know their addresses. 253 * 254 * @since 3.1 255 */ 256 public void initiateBroadcast() 257 { 258 log.debug( "Creating sender for discoveryAddress = [{0}] and " 259 + "discoveryPort = [{1}] myHostName = [{2}] and port = [{3}]", 260 () -> getUdpDiscoveryAttributes().getUdpDiscoveryAddr(), 261 () -> getUdpDiscoveryAttributes().getUdpDiscoveryPort(), 262 () -> getUdpDiscoveryAttributes().getServiceAddress(), 263 () -> getUdpDiscoveryAttributes().getServicePort() ); 264 265 try (UDPDiscoverySender sender = new UDPDiscoverySender( 266 getUdpDiscoveryAttributes(), getSerializer())) 267 { 268 sender.requestBroadcast(); 269 270 log.debug( "Sent a request broadcast to the group" ); 271 } 272 catch ( final IOException e ) 273 { 274 log.error( "Problem sending a Request Broadcast", e ); 275 } 276 } 277 278 /** 279 * Send a passive broadcast in response to a request broadcast. Never send a request for a 280 * request. We can respond to our own requests, since a request broadcast is not intended as a 281 * connection request. We might want to only send messages, so we would send a request, but 282 * never a passive broadcast. 283 */ 284 protected void serviceRequestBroadcast() 285 { 286 // create this connection each time. 287 // more robust 288 try (UDPDiscoverySender sender = new UDPDiscoverySender( 289 getUdpDiscoveryAttributes(), getSerializer())) 290 { 291 sender.passiveBroadcast( 292 getUdpDiscoveryAttributes().getServiceAddress(), 293 getUdpDiscoveryAttributes().getServicePort(), 294 this.getCacheNames() ); 295 296 log.debug( "Called sender to issue a passive broadcast" ); 297 } 298 catch ( final IOException e ) 299 { 300 log.error( "Problem calling the UDP Discovery Sender, address [{0}] " 301 + "port [{1}]", 302 getUdpDiscoveryAttributes().getUdpDiscoveryAddr(), 303 getUdpDiscoveryAttributes().getUdpDiscoveryPort(), e ); 304 } 305 } 306 307 /** 308 * Issues a remove broadcast to the others. 309 * 310 * @since 3.1 311 */ 312 protected void shutdownBroadcast() 313 { 314 // create this connection each time. 315 // more robust 316 try (UDPDiscoverySender sender = new UDPDiscoverySender( 317 getUdpDiscoveryAttributes(), getSerializer())) 318 { 319 sender.removeBroadcast( 320 getUdpDiscoveryAttributes().getServiceAddress(), 321 getUdpDiscoveryAttributes().getServicePort(), 322 this.getCacheNames() ); 323 324 log.debug( "Called sender to issue a remove broadcast in shutdown." ); 325 } 326 catch ( final IOException e ) 327 { 328 log.error( "Problem calling the UDP Discovery Sender", e ); 329 } 330 } 331 332 /** 333 * Adds a region to the list that is participating in discovery. 334 * <p> 335 * @param cacheName 336 */ 337 public void addParticipatingCacheName( final String cacheName ) 338 { 339 cacheNames.add( cacheName ); 340 } 341 342 /** 343 * Removes the discovered service from the list and calls the discovery listener. 344 * <p> 345 * @param service 346 */ 347 public void removeDiscoveredService( final DiscoveredService service ) 348 { 349 if (discoveredServices.remove(service.hashCode()) != null) 350 { 351 log.info( "Removing {0}", service ); 352 } 353 354 getDiscoveryListeners().forEach(listener -> listener.removeDiscoveredService(service)); 355 } 356 357 /** 358 * Add a service to the list. Update the held copy if we already know about it. 359 * <p> 360 * @param discoveredService discovered service 361 */ 362 protected void addOrUpdateService( final DiscoveredService discoveredService ) 363 { 364 // We want to replace the old one, since we may add info that is not part of the equals. 365 // The equals method on the object being added is intentionally restricted. 366 discoveredServices.merge(discoveredService.hashCode(), discoveredService, (oldService, newService) -> { 367 log.debug( "Set contains service." ); 368 log.debug( "Updating service in the set {0}", newService ); 369 370 // Update the list of cache names if it has changed. 371 // need to update the time this sucks. add has no effect convert to a map 372 if (!oldService.getCacheNames().equals(newService.getCacheNames())) 373 { 374 log.info( "List of cache names changed for service: {0}", newService ); 375 376 // replace it, we want to reset the payload and the last heard from time. 377 return newService; 378 } 379 380 if (oldService.getLastHearFromTime() != newService.getLastHearFromTime()) 381 { 382 return newService; 383 } 384 385 return oldService; 386 }); 387 388 // Always Notify the listeners 389 // If we don't do this, then if a region using the default config is initialized after notification, 390 // it will never get the service in it's no wait list. 391 // Leave it to the listeners to decide what to do. 392 getDiscoveryListeners().forEach(listener -> listener.addDiscoveredService(discoveredService)); 393 } 394 395 /** 396 * Get all the cache names we have facades for. 397 * <p> 398 * @return ArrayList 399 */ 400 protected ArrayList<String> getCacheNames() 401 { 402 return new ArrayList<>(cacheNames); 403 } 404 405 /** 406 * @param attr The UDPDiscoveryAttributes to set. 407 */ 408 public void setUdpDiscoveryAttributes( final UDPDiscoveryAttributes attr ) 409 { 410 this.udpDiscoveryAttributes = attr; 411 } 412 413 /** 414 * @return Returns the lca. 415 */ 416 public UDPDiscoveryAttributes getUdpDiscoveryAttributes() 417 { 418 return this.udpDiscoveryAttributes; 419 } 420 421 /** 422 * Return the serializer implementation 423 * 424 * @return the serializer 425 * @since 3.1 426 */ 427 public IElementSerializer getSerializer() 428 { 429 return serializer; 430 } 431 432 /** 433 * Start necessary receiver thread 434 */ 435 public void startup() 436 { 437 udpReceiverThread = new Thread(receiver); 438 udpReceiverThread.setDaemon(true); 439 // udpReceiverThread.setName( t.getName() + "--UDPReceiver" ); 440 udpReceiverThread.start(); 441 } 442 443 /** 444 * Shuts down the receiver. 445 */ 446 @Override 447 public void shutdown() 448 { 449 if (shutdown.compareAndSet(false, true)) 450 { 451 // Stop the scheduled tasks 452 if (broadcastTaskFuture != null) 453 { 454 broadcastTaskFuture.cancel(false); 455 } 456 if (cleanupTaskFuture != null) 457 { 458 cleanupTaskFuture.cancel(false); 459 } 460 461 if (receiver != null) 462 { 463 log.info( "Shutting down UDP discovery service receiver." ); 464 receiver.shutdown(); 465 } 466 467 log.info( "Shutting down UDP discovery service sender." ); 468 // also call the shutdown on the sender thread itself, which 469 // will result in a remove command. 470 shutdownBroadcast(); 471 } 472 else 473 { 474 log.debug( "Shutdown already called." ); 475 } 476 } 477 478 /** 479 * @return Returns the discoveredServices. 480 */ 481 public Set<DiscoveredService> getDiscoveredServices() 482 { 483 return new HashSet<>(discoveredServices.values()); 484 } 485 486 /** 487 * @return the discoveryListeners 488 */ 489 private Set<IDiscoveryListener> getDiscoveryListeners() 490 { 491 return discoveryListeners; 492 } 493 494 /** 495 * @return the discoveryListeners 496 */ 497 public Set<IDiscoveryListener> getCopyOfDiscoveryListeners() 498 { 499 return new HashSet<>(getDiscoveryListeners()); 500 } 501 502 /** 503 * Adds a listener. 504 * <p> 505 * @param listener 506 * @return true if it wasn't already in the set 507 */ 508 public boolean addDiscoveryListener( final IDiscoveryListener listener ) 509 { 510 return getDiscoveryListeners().add( listener ); 511 } 512 513 /** 514 * Removes a listener. 515 * <p> 516 * @param listener 517 * @return true if it was in the set 518 */ 519 public boolean removeDiscoveryListener( final IDiscoveryListener listener ) 520 { 521 return getDiscoveryListeners().remove( listener ); 522 } 523}