/****************************************************************************
* CruiseControl, a Continuous Integration Toolkit
* Copyright (c) 2001, ThoughtWorks, Inc.
* 200 E. Randolph, 25th Floor
* Chicago, IL 60601 USA
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* + Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* + Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
****************************************************************************/
package net.sourceforge.cruisecontrol.distributed.core;
import java.io.IOException;
import java.rmi.RemoteException;
import java.util.Arrays;
import java.util.List;
import java.util.ArrayList;
import net.jini.core.lookup.ServiceTemplate;
import net.jini.core.lookup.ServiceItem;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.discovery.LookupLocator;
import net.jini.core.entry.Entry;
import net.jini.lease.LeaseRenewalManager;
import net.jini.discovery.LookupDiscovery;
import net.jini.discovery.LookupDiscoveryManager;
import net.jini.discovery.DiscoveryListener;
import net.jini.discovery.DiscoveryEvent;
import net.jini.lookup.ServiceDiscoveryManager;
import net.jini.lookup.ServiceItemFilter;
import net.jini.admin.Administrable;
import net.sourceforge.cruisecontrol.distributed.BuildAgentService;
import net.sourceforge.cruisecontrol.distributed.PropertyEntry;
import org.apache.log4j.Logger;
import com.sun.jini.admin.DestroyAdmin;
/**
* Synchronizes access to shared ServiceDiscoveryManager to allow multiple threads to
* safely access discovery features.
*/
public final class MulticastDiscovery {
private static final Logger LOG = Logger.getLogger(MulticastDiscovery.class);
/** Service Type array used to find BuildAgent services. */
private static final Class[] SERVICE_CLASSES_BUILDAGENT = new Class[] {BuildAgentService.class};
/** Service Type array used to find Administtrable Lookup services (in order to shutdown a LUS). */
private static final Class[] SERVICE_CLASSES_ADMINISTRABLE = new Class[] {Administrable.class};
public static final int DEFAULT_FIND_WAIT_DUR_MILLIS = 5000;
/**
* The system property name which holds the port of the ClassServer, should be set on the command line,
* and is used to shutdown the ClassServer when the LookupServer on that host is destoyed.
*/
// @todo Make private when hack in DistributedMasterBuilder.loadJiniHttpPortIfNeeded() is fixed
//private static final String SYS_PROP_CLASSSERVER_HTTP_PORT = "jini.httpPort";
public static final String SYS_PROP_CLASSSERVER_HTTP_PORT = "jini.httpPort";
private final ServiceDiscoveryManager clientMgr;
/**
* Holds the singleton discovery instance.
* Instantiate here to avoid need to synchronize instance creation.
*/
private static MulticastDiscovery discovery = new MulticastDiscovery();
/**
* Intended only for use by unit tests.
* @param multicastDiscovery lookup helper
*/
static void setDiscovery(final MulticastDiscovery multicastDiscovery) {
if (discovery != null) {
// release any existing discovery resources
discovery.terminate();
LOG.error("WARNING: Discovery released, acceptable only in Unit Tests.");
}
if (multicastDiscovery == null) {
throw new IllegalStateException("Can't set MulticastDiscovery singleton instance to null");
}
discovery = multicastDiscovery;
}
/** @return the singleton discovery instance. */
private static MulticastDiscovery getDiscovery() {
return discovery;
}
/** @return true if the {@link #discovery} variable is set, intended only for unit tests. */
static boolean isDiscoverySet() {
return discovery != null;
}
private MulticastDiscovery() {
this(null);
}
static MulticastDiscovery getDiscoveryUnicast(final LookupLocator[] unicastLocaters) {
return new MulticastDiscovery(unicastLocaters);
}
private MulticastDiscovery(final LookupLocator[] unicastLocaters) {
final String[] lookupGroups = LookupDiscovery.ALL_GROUPS;
LOG.debug("Starting multicast discovery for groups: " + Arrays.toString(lookupGroups));
ReggieUtil.setupRMISecurityManager();
try {
final LookupDiscoveryManager discoverMgr = new LookupDiscoveryManager(lookupGroups, unicastLocaters,
new DiscoveryListener() {
public void discovered(DiscoveryEvent e) {
setDiscoveredImpl();
logDiscoveryEvent(DiscEventType.DISCOVERED, e);
}
public void discarded(DiscoveryEvent e) {
logDiscoveryEvent(DiscEventType.DISCARDED, e);
}
});
clientMgr = new ServiceDiscoveryManager(discoverMgr, new LeaseRenewalManager());
} catch (IOException e) {
final String message = "Error starting discovery";
LOG.debug(message, e);
throw new RuntimeException(message, e);
}
}
/**
* Start discovery of LUS's. Does NOT always need to be called, as calls to other methods
* will automatically start discovery.
* Only needed by short-lived classes, like JiniLookUpUtility and InteractiveBuildUtility.
*/
public static void begin() {
getDiscovery();
}
/**
* Only for use by JiniLookUpUtility and InteractiveBuilder.
* @return an array of discovered LUS's
*/
private ServiceRegistrar[] getRegistrarsImpl() {
return clientMgr.getDiscoveryManager().getRegistrars();
}
/**
* Only for use by JiniLookUpUtility and InteractiveBuilder.
* @return an array of discovered LUS's
*/
public static synchronized ServiceRegistrar[] getRegistrars() {
//@todo remove, or at least decrease to package visible?
return getDiscovery().getRegistrarsImpl();
}
/**
* Validates each LUS found by calling a method on the LUS.
* @return an array of discovered LUS's that appear to be working.
*/
public static synchronized ServiceRegistrar[] getValidRegistrars() {
final ServiceRegistrar[] registrars = getDiscovery().getRegistrarsImpl();
final List<ServiceRegistrar> lstRegistrars = new ArrayList<ServiceRegistrar>();
for (final ServiceRegistrar lus : registrars) {
// make any call to the LUS to see if it repsonds, or if errors occur
try {
lus.getGroups();
lstRegistrars.add(lus);
} catch (Exception e) {
// ignore exception from bad LUS
}
}
return lstRegistrars.toArray(new ServiceRegistrar[lstRegistrars.size()]);
}
private int getLUSCountImpl() {
return clientMgr.getDiscoveryManager().getRegistrars().length;
}
public static int getLUSCount() {
return getDiscovery().getLUSCountImpl();
}
private ServiceItem[] findBuildAgentServicesImpl(final Entry[] entries, final long waitDurMillis)
throws RemoteException {
final ServiceTemplate tmpl = new ServiceTemplate(null, SERVICE_CLASSES_BUILDAGENT, entries);
try { // minMatches must be > 0
return clientMgr.lookup(tmpl, 1, Integer.MAX_VALUE, MulticastDiscovery.FLTR_ANY, waitDurMillis);
} catch (InterruptedException e) {
throw new RuntimeException("Error finding BuildAgent services.", e);
}
}
public static ServiceItem[] findBuildAgentServices(final Entry[] entries, final long waitDurMillis)
throws RemoteException {
return getDiscovery().findBuildAgentServicesImpl(entries, waitDurMillis);
}
private ServiceItem findAvailableBuildAgentService(final Entry[] entries, final long waitDurMillis)
throws RemoteException {
final ServiceTemplate tmpl = new ServiceTemplate(null, SERVICE_CLASSES_BUILDAGENT, entries);
try {
return clientMgr.lookup(tmpl, FLTR_AVAILABLE, waitDurMillis);
} catch (InterruptedException e) {
throw new RuntimeException("Error finding BuildAgent service.", e);
}
}
private ServiceItem findMatchingServiceAndClaimImpl(final Entry[] entries, final long waitDurMillis)
throws RemoteException {
final ServiceItem result = findAvailableBuildAgentService(entries, waitDurMillis);
if (result != null) {
((BuildAgentService) result.service).claim();
}
return result;
}
/**
* This method is called concurrently by multiple threads running DistributedMasterBuilders, so until we have
* a better way to make an Agent's busy state changes atomic, this method should be synchronized.
* @param entries matching criteria to use when finding an available agent
* @param waitDurMillis milliseconds to wait for an agent to be found
* @return a matching agent that has been marked as claimed
* @throws RemoteException if something breaks
*/
public static synchronized ServiceItem findMatchingServiceAndClaim(final Entry[] entries, final long waitDurMillis)
throws RemoteException {
return getDiscovery().findMatchingServiceAndClaimImpl(entries, waitDurMillis);
}
private static final BuildAgentFilter FLTR_AVAILABLE = new BuildAgentFilter(true);
private static final BuildAgentFilter FLTR_ANY = new BuildAgentFilter(false);
/**
* Destroys the given LookupService. NOTE: the LUS.destroy() call is asynchronous, so the service may still be
* shutting down after this call returns.
* @param registrar the Lookup Service to stop.
* @param waitDurMillis the maxium number of milliseconds to wait for a LUS.Administrable for the given Registrar
* to be discovered.
* @throws RemoteException if a remote call fails.
*/
private void destroyLookupServiceImpl(final ServiceRegistrar registrar, final long waitDurMillis)
throws RemoteException {
// save LUS hostname for later use in killing ClassServer
final String lusHost = registrar.getLocator().getHost();
final ServiceItem[] serviceItems;
final ServiceTemplate tmpl = new ServiceTemplate(registrar.getServiceID(), SERVICE_CLASSES_ADMINISTRABLE, null);
try { // minMatches must be > 0
serviceItems = clientMgr.lookup(tmpl, 1, Integer.MAX_VALUE, null, waitDurMillis);
} catch (InterruptedException e) {
final String msg = "Error finding Lookup service: " + registrar;
LOG.error(msg, e);
throw new RuntimeException(msg, e);
}
if (serviceItems.length == 0) {
final String msg = "Failed to get Administrable service for registrar: " + registrar;
LOG.error(msg);
throw new IllegalStateException(msg);
} else if (serviceItems.length > 1) {
final String msg = "Found too many Administrable services for registrar: " + registrar
+ ", serviceItems: " + Arrays.asList(serviceItems).toString();
LOG.error(msg);
throw new IllegalStateException(msg);
}
final Administrable administrableLUS = (Administrable) serviceItems[0].service;
final DestroyAdmin adminLUS = (DestroyAdmin) administrableLUS.getAdmin();
// Note: destroy() call is asynchronous
adminLUS.destroy();
// Try to also destroy ClassServer from same host, ignore (and log) failures
// because some day there may not be a ClassServer on each LUS host
// (and there currently is no ClassServer started for CC unit tests).
final int classServerHttpPort;
try {
classServerHttpPort = Integer.getInteger(SYS_PROP_CLASSSERVER_HTTP_PORT);
} catch (Exception e) {
LOG.warn("Error reading ClassServer port for: " + lusHost
+ ", using System property: " + SYS_PROP_CLASSSERVER_HTTP_PORT
+ "=" + System.getProperty(SYS_PROP_CLASSSERVER_HTTP_PORT));
return;
}
try {
ClassServerUtil.shutdownClassServer(lusHost, classServerHttpPort);
} catch (Exception e) {
LOG.warn("Error shutting down ClassServer at: " + lusHost, e);
}
}
/**
* Destroys the given LookupService. NOTE: the LUS.destroy() call is asynchronous, so the service may still be
* shutting down after this call returns.
* @param registrar the Lookup Service to stop.
* @param waitDurMillis the maxium number of milliseconds to wait for a LUS.Administrable for the given Registrar
* to be discovered.
* @throws RemoteException if a remote call fails.
*/
public static void destroyLookupService(final ServiceRegistrar registrar, final long waitDurMillis)
throws RemoteException {
getDiscovery().destroyLookupServiceImpl(registrar, waitDurMillis);
}
static final class BuildAgentFilter implements ServiceItemFilter {
private final boolean findOnlyNonBusy;
private BuildAgentFilter(final boolean onlyNonBusy) {
findOnlyNonBusy = onlyNonBusy;
}
public boolean check(final ServiceItem item) {
LOG.debug("Service Filter: item.service: " + item.service);
if (!(item.service instanceof BuildAgentService)) {
return false;
}
final BuildAgentService agent = (BuildAgentService) item.service;
// read agent machine name to make sure agent is still valid
final String agentMachine;
try {
agentMachine = agent.getMachineName();
} catch (RemoteException e) {
final String msg = "Error reading agent machine name. Filtering out agent.";
LOG.debug(msg, e);
return false; // filter out this agent by returning false
}
if (!findOnlyNonBusy) {
return true; // we don't care if agent is busy or not
}
try {
return !agent.isBusy();
} catch (RemoteException e) {
final String msg = "Error checking agent busy status. Filtering out agent on machine: "
+ agentMachine;
LOG.debug(msg, e);
return false; // filter out this agent by returning false
}
}
}
private void terminate() {
if (clientMgr != null) {
clientMgr.terminate();
}
}
private static final class DiscEventType {
static final DiscEventType DISCOVERED = new DiscEventType("Discovered");
static final DiscEventType DISCARDED = new DiscEventType("Discarded");
private final String name;
private DiscEventType(final String name) { this.name = name; }
public String toString() { return name; }
}
private static void logDiscoveryEvent(final DiscEventType type, final DiscoveryEvent e) {
final ServiceRegistrar[] regs = e.getRegistrars();
String regMsg = ", " + regs.length + " LUS's: [";
for (ServiceRegistrar reg : regs) {
regMsg += reg.getServiceID() + ", ";
}
regMsg = regMsg.substring(0, regMsg.lastIndexOf(", ")) + "]";
LOG.info("LUS " + type + regMsg);
}
// For unit tests only - begin
private void addDiscoveryListenerImpl(final DiscoveryListener discoveryListener) {
clientMgr.getDiscoveryManager().addDiscoveryListener(discoveryListener);
}
static void addDiscoveryListener(final DiscoveryListener discoveryListener) {
getDiscovery().addDiscoveryListenerImpl(discoveryListener);
}
private void removeDiscoveryListenerImpl(final DiscoveryListener discoveryListener) {
clientMgr.getDiscoveryManager().removeDiscoveryListener(discoveryListener);
}
static void removeDiscoveryListener(final DiscoveryListener discoveryListener) {
getDiscovery().removeDiscoveryListenerImpl(discoveryListener);
}
private boolean isDiscovered;
private void setDiscoveredImpl() {
isDiscovered = true;
}
private boolean isDiscoveredImpl() {
return isDiscovered;
}
static boolean isDiscovered() {
return getDiscovery().isDiscoveredImpl();
}
// For unit tests only - end
private static String appendEntries(final StringBuilder sb, final Entry[] entries) {
sb.append("\n\tEntries:\n\t");
sb.append(Arrays.asList(entries).toString().replaceAll("\\), ", "\\), \n\t")
.replaceAll(PropertyEntry.class.getName(), ""));
sb.append("\n");
return sb.toString();
}
public static String toStringEntries(final Entry[] entries) {
return appendEntries(new StringBuilder(), entries);
}
}