/* (c) 2014 Open Source Geospatial Foundation - all rights reserved * (c) 2014 Boundless * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.cluster.hazelcast; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; import org.geoserver.catalog.Catalog; import org.geoserver.cluster.ClusterConfig; import org.geoserver.cluster.ClusterConfigWatcher; import org.geoserver.config.GeoServerPluginConfigurator; import org.geoserver.data.util.IOUtils; import org.geoserver.platform.GeoServerResourceLoader; import org.geoserver.platform.resource.Files; import org.geoserver.platform.resource.Paths; import org.geoserver.platform.resource.Resource; import org.geoserver.platform.resource.ResourceStore; import org.geoserver.platform.resource.Resources; import org.geotools.util.logging.Logging; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; import com.google.common.base.Optional; import com.hazelcast.config.Config; import com.hazelcast.config.XmlConfigBuilder; import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; /** * * @author Kevin Smith, OpenGeo * */ public class HzCluster implements GeoServerPluginConfigurator, DisposableBean, InitializingBean { protected static Logger LOGGER = Logging.getLogger("org.geoserver.cluster.hazelcast"); static final String CONFIG_DIRECTORY = "cluster"; static final String CONFIG_FILENAME = "cluster.properties"; static final String HAZELCAST_FILENAME = "hazelcast.xml"; HazelcastInstance hz; ResourceStore rl; ClusterConfigWatcher watcher; private Catalog rawCatalog; private static HzCluster CLUSTER; /** * Get a file from the cluster config directory. Create it by copying a template from the * classpath if it doesn't exist. * @param fileName Name of the file * @param scope Scope for looking up a default if the file doesn't exist. * * @throws IOException */ public Resource getConfigFile(String fileName, Class<?> scope) throws IOException { return getConfigFile(fileName, scope, this.rl); } protected Resource getConfigFile(String fileName, Class<?> scope, ResourceStore rl) throws IOException { Resource dir = rl.get(CONFIG_DIRECTORY); Resource file = dir.get(fileName); if (!Resources.exists(file)) { IOUtils.copy(scope.getResourceAsStream(fileName), file.out()); } return file; } static Optional<HzCluster> getInstanceIfAvailable(){ return Optional.fromNullable(CLUSTER); } /** * Is clustering enabled * */ public boolean isEnabled() { return hz!=null; } public boolean isRunning() { return isEnabled() && hz.getLifecycleService().isRunning(); } /** * Is session sharing enabled. Only true if clustering in general is enabled. * */ public boolean isSessionSharing() { return isEnabled() && Boolean.parseBoolean(getClusterConfig().getProperty("session_sharing", "true")); } /** * Is session sharing sticky. See Hazelcast documentation for details. * */ public boolean isStickySession() { return Boolean.parseBoolean(getClusterConfig().getProperty("session_sticky", "false")); } /** * @return milliseconds to wait for node ack notifications upon sending a config change event. * Defaults to 2000ms. */ public int getAckTimeoutMillis() { return getClusterConfig().getAckTimeoutMillis(); } /** * Get the HazelcastInstance being used for clustering * * @throws IllegalStateException if clustering is not enabled */ public HazelcastInstance getHz() { if(!isEnabled()) throw new IllegalStateException("Hazelcast Clustering has not been enabled."); return hz; } @Override public void afterPropertiesSet() throws Exception { Assert.notNull(this.rl); watcher = loadConfig(this.rl); if(watcher.get().isEnabled()){ hz = Hazelcast.newHazelcastInstance(loadHazelcastConfig(this.rl)); CLUSTER = this; } } @Override public void destroy() throws Exception { if (hz != null) { LOGGER.info("HzCluster.destroy() invoked, shutting down Hazelcast instance..."); CLUSTER = null; hz.getLifecycleService().shutdown(); hz = null; LOGGER.info("HzCluster.destroy(): Hazelcast instance shut down complete"); } } private Config loadHazelcastConfig(ResourceStore rl) throws IOException{ Resource hzf = getConfigFile(HAZELCAST_FILENAME, HzCluster.class, rl); try (InputStream hzIn = hzf.in()) { return new XmlConfigBuilder(hzIn).build(); } } /** * For Spring initialisation, don't call otherwise. * @param dd * @throws IOException */ public void setResourceStore(ResourceStore dd) throws IOException { rl=dd; } /** * For Spring initialisation, don't call otherwise. */ public void setRawCatalog(Catalog rawCatalog) throws IOException { this.rawCatalog = rawCatalog; } public Catalog getRawCatalog() { return rawCatalog; } ClusterConfigWatcher loadConfig(ResourceStore rl) throws IOException { Resource f = getConfigFile(HzCluster.CONFIG_FILENAME, HzCluster.class, rl); return new ClusterConfigWatcher(f); } ClusterConfigWatcher getConfigWatcher() { return watcher; } ClusterConfig getClusterConfig() { return watcher.get(); } @Override public List<Resource> getFileLocations() throws IOException { List<Resource> configurationFiles = new ArrayList<>(); configurationFiles.add(getConfigFile(HzCluster.CONFIG_FILENAME, HzCluster.class)); configurationFiles.add(getConfigFile(HAZELCAST_FILENAME, HzCluster.class)); return configurationFiles; } @Override public void saveConfiguration(GeoServerResourceLoader resourceLoader) throws IOException { for(Resource configFile : getFileLocations()) { Resource targetDir = Files.asResource(resourceLoader.findOrCreateDirectory(Paths.convert(rl.get("/").dir(), configFile.parent().dir()))); Resources.copy(configFile.file(), targetDir); } } @Override public void loadConfiguration(GeoServerResourceLoader resourceLoader) throws IOException { synchronized(hz) { try { destroy(); } catch (Exception e) { throw new IOException(e); } watcher = loadConfig(resourceLoader); if(watcher.get().isEnabled()){ hz = Hazelcast.newHazelcastInstance(loadHazelcastConfig(resourceLoader)); CLUSTER = this; } } } }