View Javadoc

1   /**
2    *  Copyright 2003-2007 Greg Luck
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   */
16  
17  package net.sf.ehcache.distribution;
18  
19  import net.sf.ehcache.CacheException;
20  import net.sf.ehcache.CacheManager;
21  import net.sf.ehcache.Ehcache;
22  import net.sf.ehcache.Status;
23  import net.sf.ehcache.event.CacheEventListener;
24  import org.apache.commons.logging.Log;
25  import org.apache.commons.logging.LogFactory;
26  
27  import java.io.IOException;
28  import java.net.InetAddress;
29  import java.net.ServerSocket;
30  import java.net.UnknownHostException;
31  import java.rmi.Naming;
32  import java.rmi.NotBoundException;
33  import java.rmi.Remote;
34  import java.rmi.RemoteException;
35  import java.rmi.registry.LocateRegistry;
36  import java.rmi.registry.Registry;
37  import java.rmi.server.ExportException;
38  import java.rmi.server.UnicastRemoteObject;
39  import java.util.ArrayList;
40  import java.util.HashMap;
41  import java.util.Iterator;
42  import java.util.List;
43  import java.util.Map;
44  import java.util.Set;
45  
46  /**
47   * A cache server which exposes available cache operations remotely through RMI.
48   * <p/>
49   * It acts as a Decorator to a Cache. It holds an instance of cache, which is a local cache it talks to.
50   * <p/>
51   * This class could specify a security manager with code like:
52   * <pre>
53   * if (System.getSecurityManager() == null) {
54   *     System.setSecurityManager(new RMISecurityManager());
55   * }
56   * </pre>
57   * Doing so would require the addition of <code>grant</code> statements in the <code>java.policy</code> file.
58   * <p/>
59   * Per the JDK documentation: "If no security manager is specified no class loading, by RMI clients or servers, is allowed,
60   * aside from what can be found in the local CLASSPATH." The classpath of each instance of this class should have
61   * all required classes to enable distribution, so no remote classloading is required or desirable. Accordingly,
62   * no security manager is set and there are no special JVM configuration requirements.
63   * <p/>
64   * This class opens a ServerSocket. The dispose method should be called for orderly closure of that socket. This class
65   * has a shutdown hook which calls dispose() as a convenience feature for developers.
66   *
67   * @author Greg Luck
68   * @version $Id: RMICacheManagerPeerListener.java 512 2007-07-10 09:18:45Z gregluck $
69   */
70  public class RMICacheManagerPeerListener implements CacheManagerPeerListener {
71  
72      private static final Log LOG = LogFactory.getLog(RMICacheManagerPeerListener.class.getName());
73      private static final int MINIMUM_SENSIBLE_TIMEOUT = 200;
74      private static final int NAMING_UNBIND_RETRY_INTERVAL = 400;
75      private static final int NAMING_UNBIND_MAX_RETRIES = 10;
76  
77      /**
78       * The cache peers. The value is an RMICachePeer.
79       */
80      protected final Map cachePeers = new HashMap();
81  
82      /**
83       * status.
84       */
85      protected Status status;
86  
87      /**
88       * The RMI listener port
89       */
90      protected Integer port;
91  
92      private Registry registry;
93      private boolean registryCreated;
94      private final String hostName;
95  
96      private CacheManager cacheManager;
97      private Integer socketTimeoutMillis;
98  
99      /**
100      * Constructor with full arguments.
101      *
102      * @param hostName            may be null, in which case the hostName will be looked up. Machines with multiple
103      *                            interfaces should specify this if they do not want it to be the default NIC.
104      * @param port                a port in the range 1025 - 65536
105      * @param cacheManager        the CacheManager this listener belongs to
106      * @param socketTimeoutMillis TCP/IP Socket timeout when waiting on response
107      */
108     public RMICacheManagerPeerListener(String hostName, Integer port, CacheManager cacheManager,
109                                        Integer socketTimeoutMillis) throws UnknownHostException {
110 
111         status = Status.STATUS_UNINITIALISED;
112 
113         if (hostName != null && hostName.length() != 0) {
114             this.hostName = hostName;
115             if (hostName.equals("localhost")) {
116                 LOG.warn("Explicitly setting the listener hostname to 'localhost' is not recommended. "
117                         + "It will only work if all CacheManager peers are on the same machine.");
118             }
119         } else {
120             this.hostName = calculateHostAddress();
121         }
122         if (port == null || port.intValue() == 0) {
123             assignFreePort(false);
124         } else {
125             this.port = port;
126         }
127         this.cacheManager = cacheManager;
128         if (socketTimeoutMillis == null || socketTimeoutMillis.intValue() < MINIMUM_SENSIBLE_TIMEOUT) {
129             throw new IllegalArgumentException("socketTimoutMillis must be a reasonable value greater than 200ms");
130         }
131         this.socketTimeoutMillis = socketTimeoutMillis;
132 
133     }
134 
135     /**
136      * Assigns a free port to be the listener port.
137      *
138      * @throws IllegalStateException if the statis of the listener is not {@link net.sf.ehcache.Status#STATUS_UNINITIALISED}
139      */
140     protected void assignFreePort(boolean forced) throws IllegalStateException {
141         if (status != Status.STATUS_UNINITIALISED) {
142             throw new IllegalStateException("Cannot change the port of an already started listener.");
143         }
144         this.port = new Integer(this.getFreePort());
145         if (forced) {
146             LOG.warn("Resolving RMI port conflict by automatically using a free TCP/IP port to listen on: " + this.port);
147         } else {
148             LOG.debug("Automatically finding a free TCP/IP port to listen on: " + this.port);
149         }
150     }
151 
152 
153     /**
154      * Calculates the host address as the default NICs IP address
155      *
156      * @throws UnknownHostException
157      */
158     protected String calculateHostAddress() throws UnknownHostException {
159         return InetAddress.getLocalHost().getHostAddress();
160     }
161 
162 
163     /**
164      * Gets a free server socket port.
165      *
166      * @return a number in the range 1025 - 65536 that was free at the time this method was executed
167      * @throws IllegalArgumentException
168      */
169     protected int getFreePort() throws IllegalArgumentException {
170         ServerSocket serverSocket = null;
171         try {
172             serverSocket = new ServerSocket(0);
173             return serverSocket.getLocalPort();
174         } catch (IOException e) {
175             throw new IllegalArgumentException("Could not acquire a free port number.");
176         } finally {
177             if (serverSocket != null && !serverSocket.isClosed()) {
178                 try {
179                     serverSocket.close();
180                 } catch (Exception e) {
181                     LOG.debug("Error closing ServerSocket: " + e.getMessage());
182                 }
183             }
184         }
185     }
186 
187 
188     /**
189      * {@inheritDoc}
190      */
191     public void init() throws CacheException {
192         RMICachePeer rmiCachePeer = null;
193         try {
194             startRegistry();
195             int counter = 0;
196             populateListOfRemoteCachePeers();
197             synchronized (cachePeers) {
198                 for (Iterator iterator = cachePeers.values().iterator(); iterator.hasNext();) {
199                     rmiCachePeer = (RMICachePeer) iterator.next();
200                     bind(rmiCachePeer.getUrl(), rmiCachePeer);
201                     counter++;
202                 }
203             }
204             LOG.debug(counter + " RMICachePeers bound in registry for RMI listener");
205             status = Status.STATUS_ALIVE;
206         } catch (Exception e) {
207             String url = null;
208             if (rmiCachePeer != null) {
209                 url = rmiCachePeer.getUrl();
210             }
211 
212             throw new CacheException("Problem starting listener for RMICachePeer "
213                     + url + ". Initial cause was " + e.getMessage(), e);
214         }
215     }
216 
217     /**
218      * Bind a cache peer
219      *
220      * @param rmiCachePeer
221      */
222     protected void bind(String peerName, RMICachePeer rmiCachePeer) throws Exception {
223         Naming.rebind(peerName, rmiCachePeer);
224     }
225 
226     /**
227      * Returns a list of bound objects.
228      * <p/>
229      * This should match the list of cachePeers i.e. they should always be bound
230      *
231      * @return a list of String representations of <code>RMICachePeer</code> objects
232      */
233     protected String[] listBoundRMICachePeers() throws CacheException {
234         try {
235             return registry.list();
236         } catch (RemoteException e) {
237             throw new CacheException("Unable to list cache peers " + e.getMessage());
238         }
239     }
240 
241     /**
242      * Returns a reference to the remote object.
243      *
244      * @param name the name of the cache e.g. <code>sampleCache1</code>
245      */
246     protected Remote lookupPeer(String name) throws CacheException {
247         try {
248             return registry.lookup(name);
249         } catch (Exception e) {
250             throw new CacheException("Unable to lookup peer for replicated cache " + name + " "
251                     + e.getMessage());
252         }
253     }
254 
255     /**
256      * Should be called on init because this is one of the last things that should happen on CacheManager startup.
257      */
258     protected void populateListOfRemoteCachePeers() throws RemoteException {
259         String[] names = cacheManager.getCacheNames();
260         for (int i = 0; i < names.length; i++) {
261             String name = names[i];
262             Ehcache cache = cacheManager.getEhcache(name);
263             synchronized (cachePeers) {
264                 if (cachePeers.get(name) == null) {
265                     if (isDistributed(cache)) {
266                         RMICachePeer peer = new RMICachePeer(cache, hostName, port, socketTimeoutMillis);
267                         cachePeers.put(name, peer);
268                     }
269                 }
270             }
271         }
272 
273     }
274 
275     /**
276      * Determine if the given cache is distributed.
277      *
278      * @param cache the cache to check
279      * @return true if a <code>CacheReplicator</code> is found in the listeners
280      */
281     protected boolean isDistributed(Ehcache cache) {
282         Set listeners = cache.getCacheEventNotificationService().getCacheEventListeners();
283         for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
284             CacheEventListener cacheEventListener = (CacheEventListener) iterator.next();
285             if (cacheEventListener instanceof CacheReplicator) {
286                 return true;
287             }
288         }
289         return false;
290     }
291 
292     /**
293      * Start the rmiregistry.
294      * <p/>
295      * The alternative is to use the <code>rmiregistry</code> binary, in which case:
296      * <ol/>
297      * <li>rmiregistry running
298      * <li>-Djava.rmi.server.codebase="file:///Users/gluck/work/ehcache/build/classes/ file:///Users/gluck/work/ehcache/lib/commons-logging-1.0.4.jar"
299      * </ol>
300      *
301      * @throws RemoteException
302      */
303     protected void startRegistry() throws RemoteException {
304         try {
305             registry = LocateRegistry.getRegistry(port.intValue());
306             try {
307                 registry.list();
308             } catch (RemoteException e) {
309                 //may not be created. Let's create it.
310                 registry = LocateRegistry.createRegistry(port.intValue());
311                 registryCreated = true;
312             }
313         } catch (ExportException exception) {
314             LOG.fatal("Exception starting RMI registry. Error was " + exception.getMessage(), exception);
315         }
316     }
317 
318     /**
319      * Stop the rmiregistry if it was started by this class.
320      *
321      * @throws RemoteException
322      */
323     protected void stopRegistry() throws RemoteException {
324         if (registryCreated) {
325             // the unexportObject call must be done on the Registry object returned
326             // by createRegistry not by getRegistry, a NoSuchObjectException is
327             // thrown otherwise
328             boolean success = UnicastRemoteObject.unexportObject(registry, true);
329             if (success) {
330                 LOG.debug("rmiregistry unexported.");
331             } else {
332                 LOG.warn("Could not unexport rmiregistry.");
333             }
334         }
335     }
336 
337     /**
338      * Stop the listener. It
339      * <ul>
340      * <li>unbinds the objects from the registry
341      * <li>unexports Remote objects
342      * </ul>
343      */
344     public void dispose() throws CacheException {
345         try {
346             int counter = 0;
347             synchronized (cachePeers) {
348                 for (Iterator iterator = cachePeers.values().iterator(); iterator.hasNext();) {
349                     RMICachePeer rmiCachePeer = (RMICachePeer) iterator.next();
350                     disposeRMICachePeer(rmiCachePeer);
351                     counter++;
352                 }
353                 stopRegistry();
354             }
355             LOG.debug(counter + " RMICachePeers unbound from registry in RMI listener");
356             status = Status.STATUS_SHUTDOWN;
357         } catch (Exception e) {
358             throw new CacheException("Problem unbinding remote cache peers. Initial cause was " + e.getMessage(), e);
359         }
360     }
361 
362     /**
363      * A template method to dispose an individual RMICachePeer. This consists of:
364      * <ol>
365      * <li>Unbinding the peer from the naming service
366      * <li>Unexporting the peer
367      * </ol>
368      * Override to specialise behaviour
369      *
370      * @param rmiCachePeer the cache peer to dispose of
371      * @throws Exception thrown if something goes wrong
372      */
373     protected void disposeRMICachePeer(RMICachePeer rmiCachePeer) throws Exception {
374         unbind(rmiCachePeer);
375     }
376 
377     /**
378      * Unbinds an RMICachePeer and unexports it.
379      * <p/>
380      * We unbind from the registry first before unexporting.
381      * Unbinding first removes the very small possibility of a client
382      * getting the object from the registry while we are trying to unexport it.
383      * <p/>
384      * This method may take up to 4 seconds to complete, if we are having trouble
385      * unexporting the peer.
386      *
387      * @param rmiCachePeer the bound and exported cache peer
388      * @throws Exception
389      */
390     protected void unbind(RMICachePeer rmiCachePeer) throws Exception {
391         String url = rmiCachePeer.getUrl();
392         try {
393             Naming.unbind(url);
394         } catch (NotBoundException e) {
395             LOG.warn(url + " not bound therefore not unbinding.");
396         }
397         // Try to gracefully unexport before forcing it.
398         boolean unexported = UnicastRemoteObject.unexportObject(rmiCachePeer, false);
399         for (int count = 1; (count < NAMING_UNBIND_MAX_RETRIES) && !unexported; count++) {
400             try {
401                 Thread.sleep(NAMING_UNBIND_RETRY_INTERVAL);
402             } catch (InterruptedException ie) {
403                 // break out of the unexportObject loop
404                 break;
405             }
406             unexported = UnicastRemoteObject.unexportObject(rmiCachePeer, false);
407         }
408 
409         // If we still haven't been able to unexport, force the unexport
410         // as a last resort.
411         if (!unexported) {
412             if (!UnicastRemoteObject.unexportObject(rmiCachePeer, true)) {
413                 LOG.warn("Unable to unexport rmiCachePeer: " + rmiCachePeer.getUrl() + ".  Skipping.");
414             }
415         }
416     }
417 
418     /**
419      * All of the caches which are listening for remote changes.
420      *
421      * @return a list of <code>RMICachePeer</code> objects. The list if not live
422      */
423     public List getBoundCachePeers() {
424         List cachePeerList = new ArrayList();
425         synchronized (cachePeers) {
426             for (Iterator iterator = cachePeers.values().iterator(); iterator.hasNext();) {
427                 RMICachePeer rmiCachePeer = (RMICachePeer) iterator.next();
428                 cachePeerList.add(rmiCachePeer);
429             }
430         }
431         return cachePeerList;
432     }
433 
434     /**
435      * Returns the listener status.
436      */
437     public Status getStatus() {
438         return status;
439     }
440 
441     /**
442      * A listener will normally have a resource that only one instance can use at the same time,
443      * such as a port. This identifier is used to tell if it is unique and will not conflict with an
444      * existing instance using the resource.
445      *
446      * @return a String identifier for the resource
447      */
448     public String getUniqueResourceIdentifier() {
449         return "RMI listener port: " + port;
450     }
451 
452     /**
453      * If a conflict is detected in unique resource use, this method signals the listener to attempt
454      * automatic resolution of the resource conflict.
455      *
456      * @throws IllegalStateException if the statis of the listener is not {@link net.sf.ehcache.Status#STATUS_UNINITIALISED}
457      */
458     public void attemptResolutionOfUniqueResourceConflict() throws IllegalStateException, CacheException {
459         assignFreePort(true);
460     }
461 
462     /**
463      * Called immediately after a cache has been added and activated.
464      * <p/>
465      * Note that the CacheManager calls this method from a synchronized method. Any attempt to call a synchronized
466      * method on CacheManager from this method will cause a deadlock.
467      * <p/>
468      * Note that activation will also cause a CacheEventListener status change notification from
469      * {@link net.sf.ehcache.Status#STATUS_UNINITIALISED} to {@link net.sf.ehcache.Status#STATUS_ALIVE}. Care should be
470      * taken on processing that notification because:
471      * <ul>
472      * <li>the cache will not yet be accessible from the CacheManager.
473      * <li>the addCaches methods whih cause this notification are synchronized on the CacheManager. An attempt to call
474      * {@link net.sf.ehcache.CacheManager#getCache(String)} will cause a deadlock.
475      * </ul>
476      * The calling method will block until this method returns.
477      * <p/>
478      * Repopulates the list of cache peers and rebinds the list.
479      * This method should be called if a cache is dynamically added
480      *
481      * @param cacheName the name of the <code>Cache</code> the operation relates to
482      * @see net.sf.ehcache.event.CacheEventListener
483      */
484     public void notifyCacheAdded(String cacheName) throws CacheException {
485 
486         if (LOG.isDebugEnabled()) {
487             LOG.debug("Adding " + cacheName + " to RMI listener");
488         }
489 
490         //Don't add if exists.
491         synchronized (cachePeers) {
492             if (cachePeers.get(cacheName) != null) {
493                 return;
494             }
495         }
496 
497         Ehcache cache = cacheManager.getEhcache(cacheName);
498         if (isDistributed(cache)) {
499             RMICachePeer rmiCachePeer = null;
500             String url = null;
501             try {
502                 rmiCachePeer = new RMICachePeer(cache, hostName, port, socketTimeoutMillis);
503                 url = rmiCachePeer.getUrl();
504                 bind(url, rmiCachePeer);
505             } catch (Exception e) {
506                 throw new CacheException("Problem starting listener for RMICachePeer "
507                         + url + ". Initial cause was " + e.getMessage(), e);
508             }
509 
510             synchronized (cachePeers) {
511                 cachePeers.put(cacheName, rmiCachePeer);
512             }
513 
514         }
515         if (LOG.isDebugEnabled()) {
516             LOG.debug(cachePeers.size() + " RMICachePeers bound in registry for RMI listener");
517         }
518     }
519 
520     /**
521      * Called immediately after a cache has been disposed and removed. The calling method will block until
522      * this method returns.
523      * <p/>
524      * Note that the CacheManager calls this method from a synchronized method. Any attempt to call a synchronized
525      * method on CacheManager from this method will cause a deadlock.
526      * <p/>
527      * Note that a {@link net.sf.ehcache.event.CacheEventListener} status changed will also be triggered. Any attempt from that notification
528      * to access CacheManager will also result in a deadlock.
529      *
530      * @param cacheName the name of the <code>Cache</code> the operation relates to
531      */
532     public void notifyCacheRemoved(String cacheName) {
533 
534         if (LOG.isDebugEnabled()) {
535             LOG.debug("Removing " + cacheName + " from RMI listener");
536         }
537 
538         //don't remove if already removed.
539         synchronized (cachePeers) {
540             if (cachePeers.get(cacheName) == null) {
541                 return;
542             }
543         }
544 
545         RMICachePeer rmiCachePeer;
546         synchronized (cachePeers) {
547             rmiCachePeer = (RMICachePeer) cachePeers.remove(cacheName);
548         }
549         String url = null;
550         try {
551             unbind(rmiCachePeer);
552         } catch (Exception e) {
553             throw new CacheException("Error removing Cache Peer "
554                     + url + " from listener. Message was: " + e.getMessage(), e);
555         }
556 
557         if (LOG.isDebugEnabled()) {
558             LOG.debug(cachePeers.size() + " RMICachePeers bound in registry for RMI listener");
559         }
560     }
561 
562 
563     /**
564      * Package local method for testing
565      */
566     void addCachePeer(String name, RMICachePeer peer) {
567         synchronized (cachePeers) {
568             cachePeers.put(name, peer);
569 
570         }
571     }
572 }