/* * (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed 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. * * Contributors: * Florent Guillaume */ package org.nuxeo.ecm.core; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import javax.inject.Inject; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.log4j.AppenderSkeleton; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.apache.log4j.spi.LoggingEvent; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.nuxeo.ecm.core.api.CoreInstance; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.DocumentRef; import org.nuxeo.ecm.core.api.NuxeoException; import org.nuxeo.ecm.core.api.PathRef; import org.nuxeo.ecm.core.api.impl.DocumentModelImpl; import org.nuxeo.ecm.core.api.security.SecurityConstants; import org.nuxeo.ecm.core.event.EventService; import org.nuxeo.ecm.core.model.Repository; import org.nuxeo.ecm.core.repository.RepositoryService; import org.nuxeo.ecm.core.storage.sql.listeners.DummyAsyncRetryListener; import org.nuxeo.ecm.core.test.CoreFeature; import org.nuxeo.ecm.core.test.annotations.Granularity; import org.nuxeo.ecm.core.test.annotations.RepositoryConfig; import org.nuxeo.runtime.test.runner.Features; import org.nuxeo.runtime.test.runner.FeaturesRunner; import org.nuxeo.runtime.test.runner.LocalDeploy; import org.nuxeo.runtime.transaction.TransactionHelper; import org.nuxeo.runtime.transaction.TransactionRuntimeException; @RunWith(FeaturesRunner.class) @Features(CoreFeature.class) @RepositoryConfig(cleanup = Granularity.METHOD) @LocalDeploy("org.nuxeo.ecm.core.test.tests:OSGI-INF/disable-schedulers.xml") public class TestSQLRepositoryJTAJCA { @SuppressWarnings("deprecation") private static final String ADMINISTRATOR = SecurityConstants.ADMINISTRATOR; @Inject protected CoreFeature coreFeature; @Inject protected RepositoryService repositoryService; @Inject protected EventService eventService; @Inject protected CoreSession session; protected void waitForAsyncCompletion() { nextTransaction(); eventService.waitForAsyncCompletion(); } protected void nextTransaction() { if (TransactionHelper.isTransactionActiveOrMarkedRollback()) { TransactionHelper.commitOrRollbackTransaction(); TransactionHelper.startTransaction(); } } /** * Test that connection sharing allows use of several sessions at the same time. */ @Test public void testSessionSharing() throws Exception { String repositoryName = session.getRepositoryName(); Repository repo = repositoryService.getRepository(repositoryName); session.getRootDocument(); // use the session at least once assertEquals(1, repo.getActiveSessionsCount()); try (CoreSession session2 = CoreInstance.openCoreSession(repositoryName, ADMINISTRATOR)) { assertEquals(1, repo.getActiveSessionsCount()); DocumentModel doc = new DocumentModelImpl("/", "doc", "Document"); doc = session.createDocument(doc); session.save(); // check that this is immediately seen from other connection // (underlying ManagedConnection is the same) assertTrue(session2.exists(new PathRef("/doc"))); } assertEquals(1, repo.getActiveSessionsCount()); } /** * Test that a commit implicitly does a save. */ @Test public void testSaveOnCommit() throws Exception { // first transaction DocumentModel doc = new DocumentModelImpl("/", "doc", "Document"); doc = session.createDocument(doc); // let commit do an implicit save nextTransaction(); Thread t = new Thread() { @Override public void run() { try { TransactionHelper.startTransaction(); try (CoreSession session2 = CoreInstance.openCoreSession(session.getRepositoryName(), ADMINISTRATOR)) { assertTrue(session2.exists(new PathRef("/doc"))); } finally { TransactionHelper.commitOrRollbackTransaction(); } } catch (Exception e) { fail(e.toString()); } } }; t.start(); t.join(); } protected static final Log log = LogFactory.getLog(TestSQLRepositoryJTAJCA.class); protected static class TxWarnChecker extends AppenderSkeleton { boolean seenWarn; @Override public void close() { } @Override public boolean requiresLayout() { return false; } /* * (non-Javadoc) * @see org.apache.log4j.AppenderSkeleton#append(org.apache.log4j.spi. LoggingEvent) */ @Override protected void append(LoggingEvent event) { if (!Level.WARN.equals(event.getLevel())) { return; } Object msg = event.getMessage(); if (msg instanceof String && (((String) msg).startsWith("Session invoked in a container without a transaction active"))) { seenWarn = true; } } } /** * Cannot use session after close if no tx. */ @Test public void testAccessWithoutTx() { TransactionHelper.commitOrRollbackTransaction(); TxWarnChecker checker = new TxWarnChecker(); Logger.getRootLogger().addAppender(checker); try { session.getRootDocument(); fail("should throw"); } catch (NuxeoException e) { String msg = e.getMessage(); assertTrue(msg, msg.contains("Cannot use a CoreSession outside a transaction")); } TransactionHelper.startTransaction(); } /** * Testing that if 2 modifications are done at the same time on the same document on 2 separate transactions, one is * rejected (TransactionRuntimeException) */ // not working as is @Ignore @Test public void testConcurrentModification() throws Exception { // first transaction DocumentModel doc = session.createDocumentModel("/", "doc", "Note"); doc.getProperty("dc:title").setValue("initial"); doc = session.createDocument(doc); // let commit do an implicit save nextTransaction(); // release cx coreFeature.releaseCoreSession(); final DocumentRef ref = new PathRef("/doc"); TransactionHelper.startTransaction(); // openSession(); doc = session.getDocument(ref); doc.getProperty("dc:title").setValue("first"); session.saveDocument(doc); Thread t = new Thread() { @Override public void run() { try { TransactionHelper.startTransaction(); try (CoreSession session2 = CoreInstance.openCoreSession(session.getRepositoryName(), ADMINISTRATOR)) { DocumentModel doc = session2.getDocument(ref); doc.getProperty("dc:title").setValue("second update"); session2.saveDocument(doc); } catch (Exception e) { log.error("Catched error while setting title", e); } finally { TransactionHelper.commitOrRollbackTransaction(); } } catch (Exception e) { fail(e.toString()); } } }; t.start(); t.join(); try { TransactionHelper.commitOrRollbackTransaction(); // release cx fail("expected TransactionRuntimeException"); } catch (TransactionRuntimeException e) { // expected } } @Test @LocalDeploy("org.nuxeo.ecm.core.storage.sql.test:OSGI-INF/test-listeners-async-retry-contrib.xml") public void testAsyncListenerRetry() throws Exception { DummyAsyncRetryListener.clear(); DocumentModel doc = session.createDocumentModel("/", "doc", "File"); doc.setProperty("dublincore", "title", "title1"); doc = session.createDocument(doc); session.save(); waitForAsyncCompletion(); assertEquals(2, DummyAsyncRetryListener.getCountHandled()); assertEquals(1, DummyAsyncRetryListener.getCountOk()); } @Test public void testAcquireThroughSessionId() throws Exception { DocumentModel file = session.createDocumentModel("/", "file", "File"); file = session.createDocument(file); session.save(); assertNotNull(file.getCoreSession()); } @Test public void testReconnectAfterClose() throws Exception { DocumentModel file = session.createDocumentModel("/", "file", "File"); file = session.createDocument(file); session.save(); CoreSession closedSession = session; waitForAsyncCompletion(); coreFeature.releaseCoreSession(); // use a closed session. because of tx, we can reconnect it assertNotNull(closedSession.getRootDocument()); // reopen session for rest of the code session = coreFeature.createCoreSession(); } @Test public void testReconnectAfterCommit() throws Exception { DocumentModel file = session.createDocumentModel("/", "file", "File"); file = session.createDocument(file); session.save(); TransactionHelper.commitOrRollbackTransaction(); TransactionHelper.startTransaction(); // keep existing CoreSession whose Session was implicitly closed // by commit // reconnect possible through tx assertNotNull(session.getRootDocument()); } @Test public void testReconnectAfterCloseNoTx() throws Exception { DocumentModel file = session.createDocumentModel("/", "file", "File"); file = session.createDocument(file); session.save(); CoreSession closedSession = session; waitForAsyncCompletion(); coreFeature.releaseCoreSession(); TransactionHelper.commitOrRollbackTransaction(); // no startTransaction // use a closed session -> exception try { closedSession.getRootDocument(); fail("should throw"); } catch (NuxeoException e) { String msg = e.getMessage(); assertTrue(msg, msg.contains("Cannot use a CoreSession outside a transaction")); } finally { TransactionHelper.startTransaction(); } } /** * DocumentModel.getCoreSession cannot reconnect through a sid that does not exist anymore. */ @Test public void testReconnectAfterCloseThroughSessionId() throws Exception { DocumentModel file = session.createDocumentModel("/", "file", "File"); file = session.createDocument(file); session.save(); session = coreFeature.reopenCoreSession(); assertNull(file.getCoreSession()); } @Test public void testMultiThreaded() throws Exception { assertNotNull(session.getRootDocument()); final CoreSession finalSession = session; Thread t = new Thread() { @Override public void run() { try { TransactionHelper.startTransaction(); try { assertNotNull(finalSession.getRootDocument()); } finally { TransactionHelper.commitOrRollbackTransaction(); } } catch (Exception e) { fail(e.toString()); } } }; t.start(); t.join(); assertNotNull(session.getRootDocument()); } @Test public void testMultiThreadedNeedsTx() throws Exception { assertNotNull(session.getRootDocument()); final CoreSession finalSession = session; final Exception[] threadException = new Exception[1]; Thread t = new Thread() { @Override public void run() { try { // no tx finalSession.getRootDocument(); } catch (Exception e) { threadException[0] = e; } } }; t.start(); t.join(); Exception e = threadException[0]; assertNotNull(e); assertTrue(e.getMessage(), e instanceof NuxeoException); } @Test public void testCloseFromOtherTx() throws Exception { assertNotNull(session.getRootDocument()); Thread t = new Thread() { @Override public void run() { try { coreFeature.releaseCoreSession(); } catch (Exception e) { fail(e.toString()); } } }; t.start(); t.join(); session = coreFeature.createCoreSession(); } @Test public void testPreemptiveTransactionTimeout() throws Exception { TransactionHelper.commitOrRollbackTransaction(); TransactionHelper.startTransaction(1); // 1s transaction Thread.sleep(2000); // 2s sleep try { // any use of the session after the timeout is enough to trigger the exception session.getRootDocument(); fail("should have preemptively timed out"); } catch (TransactionRuntimeException e) { assertEquals("Transaction has timed out", e.getMessage()); } try { TransactionHelper.commitOrRollbackTransaction(); fail("commit after timeout should raise an exception"); } catch (TransactionRuntimeException e) { assertEquals("Unable to commit: Transaction timeout", e.getMessage()); } TransactionHelper.startTransaction(); } }