diff --git a/config/alfresco/hazelcast/hazelcast-udp.xml b/config/alfresco/hazelcast/hazelcast-udp.xml new file mode 100644 index 0000000000..44cd2a8bbc --- /dev/null +++ b/config/alfresco/hazelcast/hazelcast-udp.xml @@ -0,0 +1,163 @@ + + + + ${alfresco.cluster.name} + ${alfresco.hazelcast.password} + + + 5701 + + + 224.2.2.3 + 54327 + + + 127.0.0.1 + + + my-access-key + my-secret-key + + us-west-1 + + hazelcast-sg + type + hz-nodes + + + + 10.10.1.* + + + + PBEWithMD5AndDES + + thesalt + + thepass + + 19 + + + + RSA/NONE/PKCS1PADDING + + thekeypass + + local + + JKS + + thestorepass + + keystore + + + + 16 + 64 + 60 + + + + 0 + + default + + + + 1 + + 0 + + 0 + + NONE + + 0 + + 25 + + hz.ADD_NEW_ENTRY + + + + + + \ No newline at end of file diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties index e5920d77ab..2f4491f67a 100644 --- a/config/alfresco/repository.properties +++ b/config/alfresco/repository.properties @@ -113,6 +113,14 @@ system.bootstrap.config_check.strict=true # Leave this empty to disable cluster entry alfresco.cluster.name= +# Hazelcast clustering configuration +# Password to join the cluster +alfresco.hazelcast.password=alfrescocluster +# Protocol used for member discovery (udp, tcp) +alfresco.hazelcast.protocol=udp +# Location of the Hazelcast configuration file +alfresco.hazelcast.configLocation=classpath:alfresco/hazelcast/hazelcast-${alfresco.hazelcast.protocol}.xml + # The EHCache RMI peer URL addresses to set in the ehcache-custom.xml file # Use this property to set the hostname of the current server. # This is only necessary if the cache peer URLs are generated with an invalid IP address for the local server. diff --git a/config/alfresco/webdav-context.xml b/config/alfresco/webdav-context.xml index 6b3b600d26..fbf9728ec5 100644 --- a/config/alfresco/webdav-context.xml +++ b/config/alfresco/webdav-context.xml @@ -23,22 +23,24 @@ ${system.webdav.servlet.enabled} - + - + + - - - - - - - - - - - - + + + + + ${alfresco.cluster.name} + ${alfresco.hazelcast.password} + + + + + + + \ No newline at end of file diff --git a/source/java/org/alfresco/repo/cluster/BuildSafeTestSuite.java b/source/java/org/alfresco/repo/cluster/BuildSafeTestSuite.java index 13f39e6b37..dfb4ecd079 100644 --- a/source/java/org/alfresco/repo/cluster/BuildSafeTestSuite.java +++ b/source/java/org/alfresco/repo/cluster/BuildSafeTestSuite.java @@ -34,6 +34,7 @@ import org.junit.runners.Suite.SuiteClasses; */ @RunWith(Suite.class) @SuiteClasses({ + org.alfresco.repo.cluster.HazelcastConfigFactoryBeanTest.class, org.alfresco.repo.cluster.HazelcastMessengerFactoryTest.class, org.alfresco.repo.cluster.HazelcastMessengerTest.class, org.alfresco.repo.cluster.JGroupsMessengerTest.class diff --git a/source/java/org/alfresco/repo/cluster/HazelcastConfigFactoryBean.java b/source/java/org/alfresco/repo/cluster/HazelcastConfigFactoryBean.java new file mode 100644 index 0000000000..405535583c --- /dev/null +++ b/source/java/org/alfresco/repo/cluster/HazelcastConfigFactoryBean.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.cluster; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.util.Properties; +import java.util.regex.Pattern; + +import org.apache.commons.io.IOUtils; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.Resource; + +import com.hazelcast.config.Config; +import com.hazelcast.config.InMemoryXmlConfig; + +/** + * FactoryBean used to create Hazelcast {@link Config} objects. A configuration file is supplied + * in the form of a Spring {@link Resource} and a set of {@link Properties} can also be provided. The + * XML file is processed so that property placeholders of the form ${property.name} are substitued for + * the corresponding property value before the XML is parsed into the Hazelcast configuration object. + * + * @author Matt Ward + */ +public class HazelcastConfigFactoryBean implements InitializingBean, FactoryBean +{ + private static final String PLACEHOLDER_END = "}"; + private static final String PLACEHOLDER_START = "${"; + private Resource configFile; + private Config config; + private Properties properties; + + + /** + * Set the Hazelcast XML configuration file to use. This will be merged with the supplied + * Properties and parsed to produce a final {@link Config} object. + * @param configFile the configFile to set + */ + public void setConfigFile(Resource configFile) + { + this.configFile = configFile; + } + + /** + * Used to supply the set of Properties that the configuration file can reference. + * + * @param properties the properties to set + */ + public void setProperties(Properties properties) + { + this.properties = properties; + } + + /** + * Spring {@link InitializingBean} lifecycle method. Substitutes property placeholders for their + * corresponding values and creates a {@link Config Hazelcast configuration} with the post-processed + * XML file - ready for the {@link #getObject()} factory method to be used to retrieve it. + */ + @Override + public void afterPropertiesSet() throws Exception + { + if (configFile == null) + { + throw new IllegalArgumentException("No configuration file specified."); + } + if (properties == null) + { + properties = new Properties(); + } + + // These configXML strings will be large and are therefore intended + // to be thrown away. We only want to keep the final Config object. + String rawConfigXML = getConfigFileContents(); + String configXML = substituteProperties(rawConfigXML); + config = new InMemoryXmlConfig(configXML); + } + + /** + * For the method parameter text, replaces all occurrences of placeholders having + * the form ${property.name} with the value of the property having the key "property.name". The + * properties are supplied using {@link #setProperties(Properties)}. + * + * @param text The String to apply property substitutions to. + * @return String after substitutions have been applied. + */ + private String substituteProperties(String text) + { + for (String propName : properties.stringPropertyNames()) + { + String propValue = properties.getProperty(propName); + String quotedPropName = Pattern.quote(PLACEHOLDER_START + propName + PLACEHOLDER_END); + text = text.replaceAll(quotedPropName, propValue); + } + + return text; + } + + /** + * Opens the configFile {@link Resource} and reads the contents into a String. + * + * @return the contents of the configFile resource. + */ + private String getConfigFileContents() + { + StringWriter writer = new StringWriter(); + InputStream inputStream = null; + try + { + inputStream = configFile.getInputStream(); + IOUtils.copy(inputStream, writer, "UTF-8"); + return writer.toString(); + } + catch (IOException e) + { + throw new RuntimeException("Couldn't read configuration: " + configFile, e); + } + finally + { + try + { + if (inputStream != null) + { + inputStream.close(); + } + } + catch (IOException e) + { + throw new RuntimeException("Couldn't close stream", e); + } + } + } + + /** + * FactoryBean's factory method. Returns the config with the property key/value + * substitutions in place. + */ + @Override + public Config getObject() throws Exception + { + return config; + } + + @Override + public Class getObjectType() + { + return Config.class; + } + + @Override + public boolean isSingleton() + { + return true; + } +} diff --git a/source/java/org/alfresco/repo/cluster/HazelcastConfigFactoryBeanTest.java b/source/java/org/alfresco/repo/cluster/HazelcastConfigFactoryBeanTest.java new file mode 100644 index 0000000000..5a897852cf --- /dev/null +++ b/source/java/org/alfresco/repo/cluster/HazelcastConfigFactoryBeanTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.cluster; + +import static org.junit.Assert.assertEquals; + +import java.util.Properties; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +import com.hazelcast.config.Config; + +/** + * Tests for the HazelcastConfigFactoryBean class. + * + * @author Matt Ward + */ +public class HazelcastConfigFactoryBeanTest +{ + private HazelcastConfigFactoryBean configFactory; + private Resource resource; + private Properties properties; + + @Before + public void setUp() throws Exception + { + configFactory = new HazelcastConfigFactoryBean(); + resource = new ClassPathResource("cluster-test/placeholder-test.xml"); + configFactory.setConfigFile(resource); + + properties = new Properties(); + properties.setProperty("alfresco.hazelcast.password", "let-me-in"); + properties.setProperty("alfresco.cluster.name", "cluster-name"); + configFactory.setProperties(properties); + + // Trigger the spring post-bean creation lifecycle method + configFactory.afterPropertiesSet(); + } + + + @Test + public void testConfigHasNewPropertyValues() throws Exception + { + // Invoke the factory method. + Config config = configFactory.getObject(); + + assertEquals("let-me-in", config.getGroupConfig().getPassword()); + assertEquals("cluster-name", config.getGroupConfig().getName()); + } +} diff --git a/source/java/org/alfresco/repo/cluster/HazelcastInstanceFactory.java b/source/java/org/alfresco/repo/cluster/HazelcastInstanceFactory.java new file mode 100644 index 0000000000..a55e6876cb --- /dev/null +++ b/source/java/org/alfresco/repo/cluster/HazelcastInstanceFactory.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.cluster; + +import com.hazelcast.config.Config; +import com.hazelcast.core.Hazelcast; +import com.hazelcast.core.HazelcastInstance; + +/** + * Provides a way of lazily creating HazelcastInstances for a given configuration. + * The HazelcastInstance will not be created until {@link #newInstance()} is called. + *

+ * An intermediary class such as this is required in order to avoid starting + * Hazelcast instances when clustering is not configured/required. Otherwise + * simply by defining a HazelcastInstance bean clustering would spring into life. + * + * @author Matt Ward + */ +public class HazelcastInstanceFactory +{ + public Config config; + + public HazelcastInstance newInstance() + { + return Hazelcast.newHazelcastInstance(config); + } + + /** + * @param config the config to set + */ + public void setConfig(Config config) + { + this.config = config; + } +} diff --git a/source/java/org/alfresco/repo/webdav/LockStore.java b/source/java/org/alfresco/repo/webdav/LockStore.java index 2565c50ad1..22ef70ad89 100644 --- a/source/java/org/alfresco/repo/webdav/LockStore.java +++ b/source/java/org/alfresco/repo/webdav/LockStore.java @@ -28,7 +28,7 @@ import org.alfresco.service.cmr.repository.NodeRef; * the actual values should be examined as necessary. *

* Implementations of this interface should be fast, ideally an in-memory map. Implementations should also be thread- - * and cluster-safe. + * and cluster-safe (if used in a cluster). * * @author Matt Ward */ diff --git a/source/java/org/alfresco/repo/webdav/LockStoreFactoryImpl.java b/source/java/org/alfresco/repo/webdav/LockStoreFactoryImpl.java index 59521b07d1..bcb63393e6 100644 --- a/source/java/org/alfresco/repo/webdav/LockStoreFactoryImpl.java +++ b/source/java/org/alfresco/repo/webdav/LockStoreFactoryImpl.java @@ -20,14 +20,17 @@ package org.alfresco.repo.webdav; import java.util.concurrent.ConcurrentMap; +import org.alfresco.repo.cluster.HazelcastInstanceFactory; import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.util.PropertyCheck; import com.hazelcast.core.HazelcastInstance; /** * Default implementation of the {@link LockStoreFactory} interface. Creates {@link LockStore}s - * backed by a Hazelcast distributed {@link java.util.Map Map}. + * backed by a Hazelcast distributed Map if the clusterName property is set, + * otherwise it creates a non-clustered {@link SimpleLockStore}. * * @see LockStoreFactory * @see LockStoreImpl @@ -35,20 +38,42 @@ import com.hazelcast.core.HazelcastInstance; */ public class LockStoreFactoryImpl implements LockStoreFactory { - private HazelcastInstance hazelcast; + private static final String HAZELCAST_MAP_NAME = "webdav-locks"; + private HazelcastInstanceFactory hazelcastInstanceFactory; + private String clusterName; + /** + * This method should be used sparingly and the created {@link LockStore}s should be + * retained (this factory does not cache instances of them). + */ @Override - public LockStore getLockStore() + public synchronized LockStore getLockStore() { - ConcurrentMap map = hazelcast.getMap("webdav-locks"); - return new LockStoreImpl(map); + if (!PropertyCheck.isValidPropertyString(clusterName)) + { + return new SimpleLockStore(); + } + else + { + HazelcastInstance instance = hazelcastInstanceFactory.newInstance(); + ConcurrentMap map = instance.getMap(HAZELCAST_MAP_NAME); + return new LockStoreImpl(map); + } } /** - * @param hazelcast the hazelcast to set + * @param hazelcastInstanceFactory the factory that will create a HazelcastInstance if required. */ - public void setHazelcast(HazelcastInstance hazelcast) + public synchronized void setHazelcastInstanceFactory(HazelcastInstanceFactory hazelcastInstanceFactory) { - this.hazelcast = hazelcast; + this.hazelcastInstanceFactory = hazelcastInstanceFactory; + } + + /** + * @param clusterName the clusterName to set + */ + public synchronized void setClusterName(String clusterName) + { + this.clusterName = clusterName; } } diff --git a/source/java/org/alfresco/repo/webdav/SimpleLockStore.java b/source/java/org/alfresco/repo/webdav/SimpleLockStore.java new file mode 100644 index 0000000000..03976f8094 --- /dev/null +++ b/source/java/org/alfresco/repo/webdav/SimpleLockStore.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.webdav; + +import java.util.concurrent.ConcurrentHashMap; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * {@link LockStore} implementation for use in a non-clustered environment. + * + * @author Matt Ward + */ +public class SimpleLockStore extends LockStoreImpl +{ + public SimpleLockStore() + { + super(new ConcurrentHashMap()); + } +} diff --git a/source/test-resources/cluster-test/placeholder-test.xml b/source/test-resources/cluster-test/placeholder-test.xml new file mode 100644 index 0000000000..9bbcaab1a7 --- /dev/null +++ b/source/test-resources/cluster-test/placeholder-test.xml @@ -0,0 +1,166 @@ + + + + + ${alfresco.cluster.name} + ${alfresco.hazelcast.password} + + + 5701 + + + 224.2.2.3 + 54327 + + + 127.0.0.1 + + + my-access-key + my-secret-key + + us-west-1 + + hazelcast-sg + type + hz-nodes + + + + 10.10.1.* + + + + PBEWithMD5AndDES + + thesalt + + thepass + + 19 + + + + RSA/NONE/PKCS1PADDING + + thekeypass + + local + + JKS + + thestorepass + + keystore + + + + 16 + 64 + 60 + + + + 0 + + default + + + + 1 + + 0 + + 0 + + NONE + + 0 + + 25 + + hz.ADD_NEW_ENTRY + + + + + + \ No newline at end of file