Index: jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/ext/ExternalDerbyProcess.java =================================================================== --- jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/ext/ExternalDerbyProcess.java (revision 0) +++ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/ext/ExternalDerbyProcess.java (revision 0) @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence.ext; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +import org.apache.derby.drda.NetworkServerControl; +import org.apache.derby.jdbc.EmbeddedDriver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ExternalDerbyProcess is a helper class for starting + * an external derby server on localhost, port 1527 (standard settings). + * + */ +public class ExternalDerbyProcess { + + public static Logger logger = LoggerFactory.getLogger(ExternalDerbyProcess.class); + + public static void setLogger(Logger aLogger) { + logger = aLogger; + } + + /** + * Returns the path to the JAR file that a certain class is located in. This only works + * if the classloader loaded this class from a JAR file. + */ + public static final String getJarFileForClass(Class clazz) { + // eg. /org/apache/derby/drda/NetworkServerControl.class + String classResource = "/" + clazz.getCanonicalName().replace(".", "/") + ".class"; + // eg. jar:file:/Users/alex/.m2/repository/org/apache/derby/derbynet/10.2.1.6/derbynet-10.2.1.6.jar!/org/apache/derby/drda/NetworkServerControl.class + String fullResourceURL = clazz.getResource(classResource).toString(); + // eg. /Users/alex/.m2/repository/org/apache/derby/derbynet/10.2.1.6/derbynet-10.2.1.6.jar + return fullResourceURL.replaceFirst("jar:file:([^!]+).*", "$1"); + } + + public static final String EXTERNAL_DERBY_JDBC_DRIVER = "org.apache.derby.jdbc.ClientDriver"; + + public static final String EXTERNAL_DERBY_USER = "cloud"; + + public static final String EXTERNAL_DERBY_PASSWORD = "scape"; + + private static final String DERBY_JAVA_CMD = + "-Dderby.drda.logConnections=true org.apache.derby.drda.NetworkServerControl start"; + + private static final int DERBY_STARTUP_TIME = 1000; // ms + + private static List processes = new ArrayList(); + + public static Process start() throws IOException, InterruptedException { + // Let's create a hand-made pid + final String prefix = "[derby server " + (processes.size() + 1) + "]: "; + + // derby server needs classpath with: + // org.apache.derby:derby + // org.apache.derby:derbynet + String derbyJar = getJarFileForClass(EmbeddedDriver.class); + String derbyNetJar = getJarFileForClass(NetworkServerControl.class); + + String classPath = System.getProperty("java.class.path"); + classPath += File.pathSeparatorChar + derbyJar; + classPath += File.pathSeparatorChar + derbyNetJar; + + String cmd = "java -cp " + classPath + " " + DERBY_JAVA_CMD; + logger.info(prefix + "Starting " + cmd); + + final Process p = Runtime.getRuntime().exec(cmd); + processes.add(p); + + // log stdout and stderr to our console + Thread stdoutThread = new Thread(new InputStream2ConsolePrinter(p.getInputStream(), prefix, false)); + Thread stderrThread = new Thread(new InputStream2ConsolePrinter(p.getErrorStream(), prefix, true)); + stdoutThread.start(); + stderrThread.start(); + + // write the exit code on the console when the process ends + Thread exitCodeThread = new Thread(new Runnable() { + public void run() { + try { + logger.debug(prefix + "Exit code: " + p.waitFor()); + } catch (InterruptedException e) { + logger.error("Interrupted while waiting for external derby process", e); + } + } + }); + exitCodeThread.start(); + + // wait for the server to start + Thread.sleep(DERBY_STARTUP_TIME); + + return p; + } + + public static void killAll() { + for (int i=0; i < processes.size(); i++) { + logger.debug("Killing derby server " + i); + ((Process) processes.get(i)).destroy(); + } + } + + private static class InputStream2ConsolePrinter implements Runnable { + + private BufferedReader input; + private String prefix; + private boolean toStdErr; + + public InputStream2ConsolePrinter(InputStream is, String prefix, boolean toStdErr) { + this.input = new BufferedReader(new InputStreamReader(is)); + this.prefix = prefix; + this.toStdErr = toStdErr; + } + + public void run() { + String line; + try { + while ((line = input.readLine()) != null) { + if (toStdErr) { + logger.warn(prefix + line); + } else { + logger.debug(prefix + line); + } + } + input.close(); + } catch (IOException e) { + } + } + + } + +} Index: jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/ext/ExternalDerbyTest.java =================================================================== --- jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/ext/ExternalDerbyTest.java (revision 0) +++ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/ext/ExternalDerbyTest.java (revision 0) @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.core.persistence.ext; + +import java.io.File; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.test.JUnitTest; +import org.apache.jackrabbit.test.RepositoryHelper; +import org.apache.jackrabbit.test.config.DynamicRepositoryHelper; +import org.apache.jackrabbit.test.config.PersistenceManagerConf; +import org.apache.jackrabbit.test.config.RepositoryConf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ExternalDerbyTest is an abstract base class for jackrabbit + * persistence manager tests that need to connect to an external derby + * server for testing connection problems, data loss etc., which can + * be easily done by manipulating the external process. + * + * This test class does not use {@link #setUp()} to setup the repository, but + * instead it is required to call {@link #startExternalDerby()} and + * {@link #startJackrabbitWithExternalDerby()}. + */ +public abstract class ExternalDerbyTest extends JUnitTest { + + /** + * Helper object to access repository transparently + */ + public static RepositoryHelper helper = new RepositoryHelper(); + + private static final String REPO_HOME = "target/repository"; + + protected final Logger logger = LoggerFactory.getLogger(JUnitTest.class); + + protected Process derbyProcess; + + protected void setUp() { + // do nothing here, ie. don't set up helper with a new repo + } + + protected void tearDown() { + killExternalDerby(); + } + + protected void startExternalDerby() throws Exception { + logger.debug("Starting external derby server..."); + + ExternalDerbyProcess.setLogger(logger); + derbyProcess = ExternalDerbyProcess.start(); + + try { + int exitCode = derbyProcess.exitValue(); + throw new Exception("Derby server for testing did not start properly, exit code is " + exitCode); + } catch (IllegalThreadStateException e) { + // happens when the process is still running, which is good + } + } + + protected void killExternalDerby() { + if (derbyProcess != null) { + try { + derbyProcess.exitValue(); + // if there is no exception, derby has quit already + return; + } catch (IllegalThreadStateException e) { + // happens when the process is still running + } + + logger.debug("Killing external derby server..."); + derbyProcess.destroy(); + } + } + + protected RepositoryConf createRepositoryConf() { + RepositoryConf conf = new RepositoryConf(); + + // set jdbc urls on PMs for external derby + // workspaces + PersistenceManagerConf pmc = conf.getWorkspaceConfTemplate().getPersistenceManagerConf(); + pmc.setParameter("url", "jdbc:derby://localhost/${wsp.home}/version/db/itemState;create=true"); + pmc.setParameter("driver", ExternalDerbyProcess.EXTERNAL_DERBY_JDBC_DRIVER); + pmc.setParameter("user", ExternalDerbyProcess.EXTERNAL_DERBY_USER); + pmc.setParameter("password", ExternalDerbyProcess.EXTERNAL_DERBY_PASSWORD); + // false is the default value anyway, but we want to make sure, the code does not block forever + pmc.setParameter("blockOnConnectionLoss", "false"); + + // versioning + pmc = conf.getVersioningConf().getPersistenceManagerConf(); + pmc.setParameter("url", "jdbc:derby://localhost/${rep.home}/db/itemState;create=true"); + pmc.setParameter("driver", ExternalDerbyProcess.EXTERNAL_DERBY_JDBC_DRIVER); + pmc.setParameter("user", ExternalDerbyProcess.EXTERNAL_DERBY_USER); + pmc.setParameter("password", ExternalDerbyProcess.EXTERNAL_DERBY_PASSWORD); + // false is the default value anyway, but we want to make sure, the code does not block forever + pmc.setParameter("blockOnConnectionLoss", "false"); + + return conf; + } + + protected void startJackrabbitWithExternalDerby() throws RepositoryException { + // clean up + shutdownJackrabbit(); + deleteDirectory(new File(REPO_HOME)); + + logger.debug("Starting jackrabbit..."); + helper = new DynamicRepositoryHelper(createRepositoryConf(), REPO_HOME); + } + + protected void shutdownJackrabbit() { + if (helper != null && helper instanceof DynamicRepositoryHelper) { + logger.debug("Stopping jackrabbit..."); + ((DynamicRepositoryHelper) helper).shutdown(); + } + } + + protected void jcrWorkA(Session session) throws RepositoryException { + Node rootNode = session.getRootNode(); + Node node = rootNode.addNode("test"); + node.setProperty("prop", "foobar"); + } + + protected void jcrWorkB(Session session) throws RepositoryException { + Node rootNode = session.getRootNode(); + Node node = rootNode.addNode("test2"); + node.setProperty("prop", "foobar"); + } + + public static boolean deleteDirectory(File path) { + if (path.exists()) { + File[] files = path.listFiles(); + for (int i = 0; i < files.length; i++) { + if (files[i].isDirectory()) { + deleteDirectory(files[i]); + } else { + files[i].delete(); + } + } + } + return (path.delete()); + } + +} Index: jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/TestAll.java =================================================================== --- jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/TestAll.java (revision 0) +++ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/TestAll.java (revision 0) @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all jackrabbit specific testcases for the + * persistence module. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("Persistence tests"); + + suite.addTestSuite(DatabaseConnectionFailureTest.class); + + return suite; + } +} Index: jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/DatabaseConnectionFailureTest.java =================================================================== --- jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/DatabaseConnectionFailureTest.java (revision 0) +++ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/DatabaseConnectionFailureTest.java (revision 0) @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.core.persistence.ext.ExternalDerbyTest; + +/** + * DatabaseConnectionFailureTest tests various situations in + * which the connection of a persistence manager or file system to its database + * is lost. If the server restarts properly, Jackrabbit should re-connect + * automatically and in the best case there is no data lost. + */ +public class DatabaseConnectionFailureTest extends ExternalDerbyTest { + + // this test takes about 2 mins and is not very important (it verifies + // that an RepositoryException is thrown if the database server behind + // the persistence manager is killed) +/* + // external derby process + public void testConnectionBroken() throws Exception { + startExternalDerby(); + + startJackrabbitWithExternalDerby(); + Session session = helper.getSuperuserSession(); + + // do something jcr-like + jcrWorkA(session); + session.save(); + + killExternalDerby(); + + // do something jcr-like => expect RepositoryException + jcrWorkB(session); + + long start = System.currentTimeMillis(); + try { + // with the auto-reconnect feature in Bundle PMs, this save will trigger + // a loop of connection trials that will all fail, because we killed + // the server. this typically takes about 2 mins before finally a + // RepositoryException is thrown. + session.save(); + + assertTrue("RepositoryException was expected (waiting some time is normal)", false); + } catch (RepositoryException e) { + // fine, exception is expected + } + long end = System.currentTimeMillis(); + logger.debug("time taken: " + (end - start)); + } +*/ + + // external derby process + public void testConnectionBrokenAndReconnect() throws Exception { + startExternalDerby(); + + startJackrabbitWithExternalDerby(); + Session session = helper.getSuperuserSession(); + + // do something jcr-like + jcrWorkA(session); + session.save(); + + killExternalDerby(); + + // RESTART derby + startExternalDerby(); + + // do something jcr-like => works again, maybe data corrupted + jcrWorkB(session); + + // an exception here means that the PM does not properly re-connect to the database + session.save(); + } + + // The following test cases are just ideas for testing an embedded derby +/* + // embedded derby + public void testConnectionClosed() throws Exception { + // start derby + // start jackrabbit + derby pm/file system + // do something jcr-like + // SHUTDOWN derby + // do something jcr-like => expect RepositoryException + } + + // embedded derby + public void testConnectionClosedAndReconnect() throws Exception { + // start derby + // start jackrabbit + derby pm/file system + // do something jcr-like + // SHUTDOWN derby + // RESTART derby + // do something jcr-like => everything should work normally + } +*/ +} Index: jackrabbit-core/pom.xml =================================================================== --- jackrabbit-core/pom.xml (revision 629174) +++ jackrabbit-core/pom.xml (working copy) @@ -327,6 +327,16 @@ slf4j-log4j12 test + + org.apache.derby + derbynet + test + + + org.apache.derby + derbyclient + test + Index: pom.xml =================================================================== --- pom.xml (revision 629174) +++ pom.xml (working copy) @@ -693,6 +693,16 @@ 10.2.1.6 + org.apache.derby + derbynet + 10.2.1.6 + + + org.apache.derby + derbyclient + 10.2.1.6 + + poi poi 2.5.1-final-20040804